Unverified Commit f4caee6e authored by Mitchell Goodwin's avatar Mitchell Goodwin Committed by GitHub

Add adaptive Checkbox and CheckboxListTile (#123132)

Add adaptive Checkbox and CheckboxListTile
parent 785ea2a4
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/widgets.dart'; import 'package:flutter/cupertino.dart';
import 'checkbox_theme.dart'; import 'checkbox_theme.dart';
import 'color_scheme.dart'; import 'color_scheme.dart';
...@@ -18,6 +18,8 @@ import 'toggleable.dart'; ...@@ -18,6 +18,8 @@ import 'toggleable.dart';
// bool _throwShotAway = false; // bool _throwShotAway = false;
// late StateSetter setState; // late StateSetter setState;
enum _CheckboxType { material, adaptive }
/// A Material Design checkbox. /// A Material Design checkbox.
/// ///
/// The checkbox itself does not maintain any state. Instead, when the state of /// The checkbox itself does not maintain any state. Instead, when the state of
...@@ -88,7 +90,47 @@ class Checkbox extends StatefulWidget { ...@@ -88,7 +90,47 @@ class Checkbox extends StatefulWidget {
this.shape, this.shape,
this.side, this.side,
this.isError = false, this.isError = false,
}) : assert(tristate || value != null); }) : _checkboxType = _CheckboxType.material,
assert(tristate || value != null);
/// Creates an adaptive [Checkbox] based on whether the target platform is iOS
/// or macOS, following Material design's
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
///
/// On iOS and macOS, this constructor creates a [CupertinoCheckbox], which has
/// matching functionality and presentation as Material checkboxes, and are the
/// graphics expected on iOS. On other platforms, this creates a Material
/// design [Checkbox].
///
/// If a [CupertinoCheckbox] is created, the following parameters are ignored:
/// [mouseCursor], [hoverColor], [overlayColor], [splashRadius],
/// [materialTapTargetSize], [visualDensity], [isError]. However, [shape] and
/// [side] will still affect the [CupertinoCheckbox] and should be handled if
/// native fidelity is important.
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
const Checkbox.adaptive({
super.key,
required this.value,
this.tristate = false,
required this.onChanged,
this.mouseCursor,
this.activeColor,
this.fillColor,
this.checkColor,
this.focusColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.focusNode,
this.autofocus = false,
this.shape,
this.side,
this.isError = false,
}) : _checkboxType = _CheckboxType.adaptive,
assert(tristate || value != null);
/// Whether this checkbox is checked. /// Whether this checkbox is checked.
/// ///
...@@ -347,6 +389,8 @@ class Checkbox extends StatefulWidget { ...@@ -347,6 +389,8 @@ class Checkbox extends StatefulWidget {
/// The width of a checkbox widget. /// The width of a checkbox widget.
static const double width = 18.0; static const double width = 18.0;
final _CheckboxType _checkboxType;
@override @override
State<Checkbox> createState() => _CheckboxState(); State<Checkbox> createState() => _CheckboxState();
} }
...@@ -410,6 +454,35 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg ...@@ -410,6 +454,35 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (widget._checkboxType) {
case _CheckboxType.material:
break;
case _CheckboxType.adaptive:
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return CupertinoCheckbox(
value: value,
tristate: tristate,
onChanged: onChanged,
activeColor: widget.activeColor,
checkColor: widget.checkColor,
focusColor: widget.focusColor,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
side: widget.side,
shape: widget.shape,
);
}
}
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final CheckboxThemeData checkboxTheme = CheckboxTheme.of(context); final CheckboxThemeData checkboxTheme = CheckboxTheme.of(context);
final CheckboxThemeData defaults = Theme.of(context).useMaterial3 final CheckboxThemeData defaults = Theme.of(context).useMaterial3
......
...@@ -16,6 +16,8 @@ import 'theme_data.dart'; ...@@ -16,6 +16,8 @@ import 'theme_data.dart';
// late bool? _throwShotAway; // late bool? _throwShotAway;
// void setState(VoidCallback fn) { } // void setState(VoidCallback fn) { }
enum _CheckboxType { material, adaptive }
/// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label. /// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label.
/// ///
/// The entire list tile is interactive: tapping anywhere in the tile toggles /// The entire list tile is interactive: tapping anywhere in the tile toggles
...@@ -192,7 +194,51 @@ class CheckboxListTile extends StatelessWidget { ...@@ -192,7 +194,51 @@ class CheckboxListTile extends StatelessWidget {
this.selectedTileColor, this.selectedTileColor,
this.onFocusChange, this.onFocusChange,
this.enableFeedback, this.enableFeedback,
}) : assert(tristate || value != null), }) : _checkboxType = _CheckboxType.material,
assert(tristate || value != null),
assert(!isThreeLine || subtitle != null);
/// Creates a combination of a list tile and a platform adaptive checkbox.
///
/// The checkbox uses [Checkbox.adaptive] to show a [CupertinoCheckbox] for
/// iOS platforms, or [Checkbox] for all others.
///
/// All other properties are the same as [CheckboxListTile].
const CheckboxListTile.adaptive({
super.key,
required this.value,
required this.onChanged,
this.mouseCursor,
this.activeColor,
this.fillColor,
this.checkColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.focusNode,
this.autofocus = false,
this.shape,
this.side,
this.isError = false,
this.enabled,
this.tileColor,
this.title,
this.subtitle,
this.isThreeLine = false,
this.dense,
this.secondary,
this.selected = false,
this.controlAffinity = ListTileControlAffinity.platform,
this.contentPadding,
this.tristate = false,
this.checkboxShape,
this.selectedTileColor,
this.onFocusChange,
this.enableFeedback,
}) : _checkboxType = _CheckboxType.adaptive,
assert(tristate || value != null),
assert(!isThreeLine || subtitle != null); assert(!isThreeLine || subtitle != null);
/// Whether this checkbox is checked. /// Whether this checkbox is checked.
...@@ -406,6 +452,8 @@ class CheckboxListTile extends StatelessWidget { ...@@ -406,6 +452,8 @@ class CheckboxListTile extends StatelessWidget {
/// inoperative. /// inoperative.
final bool? enabled; final bool? enabled;
final _CheckboxType _checkboxType;
void _handleValueChange() { void _handleValueChange() {
assert(onChanged != null); assert(onChanged != null);
switch (value) { switch (value) {
...@@ -420,7 +468,29 @@ class CheckboxListTile extends StatelessWidget { ...@@ -420,7 +468,29 @@ class CheckboxListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget control = Checkbox( final Widget control;
switch (_checkboxType) {
case _CheckboxType.material:
control = Checkbox(
value: value,
onChanged: enabled ?? true ? onChanged : null,
mouseCursor: mouseCursor,
activeColor: activeColor,
fillColor: fillColor,
checkColor: checkColor,
hoverColor: hoverColor,
overlayColor: overlayColor,
splashRadius: splashRadius,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
autofocus: autofocus,
tristate: tristate,
shape: checkboxShape,
side: side,
isError: isError,
);
case _CheckboxType.adaptive:
control = Checkbox.adaptive(
value: value, value: value,
onChanged: enabled ?? true ? onChanged : null, onChanged: enabled ?? true ? onChanged : null,
mouseCursor: mouseCursor, mouseCursor: mouseCursor,
...@@ -437,6 +507,8 @@ class CheckboxListTile extends StatelessWidget { ...@@ -437,6 +507,8 @@ class CheckboxListTile extends StatelessWidget {
side: side, side: side,
isError: isError, isError: isError,
); );
}
Widget? leading, trailing; Widget? leading, trailing;
switch (controlAffinity) { switch (controlAffinity) {
case ListTileControlAffinity.leading: case ListTileControlAffinity.leading:
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -921,6 +922,38 @@ void main() { ...@@ -921,6 +922,38 @@ void main() {
); );
}); });
testWidgets('CheckboxListTile.adaptive shows the correct checkbox platform widget', (WidgetTester tester) async {
Widget buildApp(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return CheckboxListTile.adaptive(
value: false,
onChanged: (bool? newValue) {},
);
}),
),
),
);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoCheckbox), findsOneWidget);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoCheckbox), findsNothing);
}
});
group('feedback', () { group('feedback', () {
late FeedbackTester feedback; late FeedbackTester feedback;
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -1769,6 +1770,38 @@ void main() { ...@@ -1769,6 +1770,38 @@ void main() {
), ),
); );
}); });
testWidgets('Checkbox.adaptive shows the correct platform widget', (WidgetTester tester) async {
Widget buildApp(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Checkbox.adaptive(
value: false,
onChanged: (bool? newValue) {},
);
}),
),
),
);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoCheckbox), findsOneWidget);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoCheckbox), findsNothing);
}
});
} }
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor { class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment