Migrated Checkbox to Material 3 - Added Error State (#111153)

This commit is contained in:
Qun Cheng 2022-09-08 14:42:29 -07:00 committed by GitHub
parent 4d3c122434
commit 98eac3f198
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 209 additions and 67 deletions

View file

@ -23,32 +23,14 @@ class _${blockName}DefaultsM3 extends CheckboxThemeData {
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return ${componentColor('md.comp.checkbox.selected.disabled.container')};
}
return ${componentColor('md.comp.checkbox.unselected.disabled.outline')}.withOpacity(${opacity('md.comp.checkbox.unselected.disabled.container.opacity')});
return ${componentColor('md.comp.checkbox.selected.disabled.container')};
}
if (states.contains(MaterialState.error)) {
return ${componentColor('md.comp.checkbox.unselected.error.outline')};
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.checkbox.selected.pressed.container')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.checkbox.selected.hover.container')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.checkbox.selected.focus.container')};
}
return ${componentColor('md.comp.checkbox.selected.container')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.checkbox.unselected.pressed.outline')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.checkbox.unselected.hover.outline')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.checkbox.unselected.focus.outline')};
}
return ${componentColor('md.comp.checkbox.unselected.outline')};
});
}
@ -63,14 +45,8 @@ class _${blockName}DefaultsM3 extends CheckboxThemeData {
return Colors.transparent; // No icons available when the checkbox is unselected.
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.checkbox.selected.pressed.icon')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.checkbox.selected.hover.icon')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.checkbox.selected.focus.icon')};
if (states.contains(MaterialState.error)) {
return ${componentColor('md.comp.checkbox.selected.error.icon')};
}
return ${componentColor('md.comp.checkbox.selected.icon')};
}
@ -81,6 +57,17 @@ class _${blockName}DefaultsM3 extends CheckboxThemeData {
@override
MaterialStateProperty<Color> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.error)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.checkbox.error.pressed.state-layer')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.checkbox.error.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.checkbox.error.focus.state-layer')}.withOpacity(0.12);
}
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.checkbox.selected.pressed.state-layer')};

View file

@ -0,0 +1,74 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for M3 Checkbox with error state
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Flutter Code Sample';
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4)),
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: MyStatefulWidget(),
),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool? isChecked = true;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Checkbox(
tristate: true,
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value;
});
},
),
Checkbox(
isError: true,
tristate: true,
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value;
});
},
),
Checkbox(
isError: true,
tristate: true,
value: isChecked,
onChanged: null,
),
],
);
}
}

View file

@ -87,6 +87,7 @@ class Checkbox extends StatefulWidget {
this.autofocus = false,
this.shape,
this.side,
this.isError = false,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(autofocus != null);
@ -332,6 +333,14 @@ class Checkbox extends StatefulWidget {
/// will be width 2.
final BorderSide? side;
/// True if this checkbox wants to show an error state.
///
/// The checkbox will have different default container color and check color when
/// this is true. This is only used when [ThemeData.useMaterial3] is set to true.
///
/// Must not be null. Defaults to false.
final bool isError;
/// The width of a checkbox widget.
static const double width = 18.0;
@ -427,8 +436,9 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
// Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between.
final Set<MaterialState> activeStates = states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected);
final Set<MaterialState> errorState = states..add(MaterialState.error);
final Set<MaterialState> activeStates = widget.isError ? (errorState..add(MaterialState.selected)) : states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = widget.isError ? (errorState..remove(MaterialState.selected)) : states..remove(MaterialState.selected);
final Color? activeColor = widget.fillColor?.resolve(activeStates)
?? _widgetFillColor.resolve(activeStates)
?? checkboxTheme.fillColor?.resolve(activeStates);
@ -440,14 +450,14 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
final Color effectiveInactiveColor = inactiveColor
?? defaults.fillColor!.resolve(inactiveStates)!;
final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
final Set<MaterialState> focusedStates = widget.isError ? (errorState..add(MaterialState.focused)) : states..add(MaterialState.focused);
Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor
?? checkboxTheme.overlayColor?.resolve(focusedStates)
?? defaults.overlayColor!.resolve(focusedStates)!;
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
final Set<MaterialState> hoveredStates = widget.isError ? (errorState..add(MaterialState.hovered)) : states..add(MaterialState.hovered);
Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor
?? checkboxTheme.overlayColor?.resolve(hoveredStates)
?? defaults.overlayColor!.resolve(hoveredStates)!;
@ -464,9 +474,19 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
?? inactiveColor?.withAlpha(kRadialReactionAlpha)
?? defaults.overlayColor!.resolve(inactivePressedStates)!;
if (downPosition != null) {
effectiveHoverOverlayColor = states.contains(MaterialState.selected)
? effectiveActivePressedOverlayColor
: effectiveInactivePressedOverlayColor;
effectiveFocusOverlayColor = states.contains(MaterialState.selected)
? effectiveActivePressedOverlayColor
: effectiveInactivePressedOverlayColor;
}
final Set<MaterialState> checkStates = widget.isError ? (states..add(MaterialState.error)) : states;
final Color effectiveCheckColor = widget.checkColor
?? checkboxTheme.checkColor?.resolve(states)
?? defaults.checkColor!.resolve(states)!;
?? checkboxTheme.checkColor?.resolve(checkStates)
?? defaults.checkColor!.resolve(checkStates)!;
final double effectiveSplashRadius = widget.splashRadius
?? checkboxTheme.splashRadius
@ -758,32 +778,14 @@ class _CheckboxDefaultsM3 extends CheckboxThemeData {
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return _colors.onSurface.withOpacity(0.38);
}
return _colors.onSurface.withOpacity(0.38);
}
if (states.contains(MaterialState.error)) {
return _colors.error;
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.primary;
}
if (states.contains(MaterialState.hovered)) {
return _colors.primary;
}
if (states.contains(MaterialState.focused)) {
return _colors.primary;
}
return _colors.primary;
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface;
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface;
}
return _colors.onSurface;
});
}
@ -798,14 +800,8 @@ class _CheckboxDefaultsM3 extends CheckboxThemeData {
return Colors.transparent; // No icons available when the checkbox is unselected.
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.onPrimary;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onPrimary;
}
if (states.contains(MaterialState.focused)) {
return _colors.onPrimary;
if (states.contains(MaterialState.error)) {
return _colors.onError;
}
return _colors.onPrimary;
}
@ -816,6 +812,17 @@ class _CheckboxDefaultsM3 extends CheckboxThemeData {
@override
MaterialStateProperty<Color> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.error)) {
if (states.contains(MaterialState.pressed)) {
return _colors.error.withOpacity(0.12);
}
if (states.contains(MaterialState.hovered)) {
return _colors.error.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.error.withOpacity(0.12);
}
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface.withOpacity(0.12);

View file

@ -1261,6 +1261,7 @@ class ThemeData with Diagnosticable {
/// - [ActionChip] (used for Assist and Suggestion chips),
/// - [FilterChip], [ChoiceChip] (used for single selection filter chips),
/// - [InputChip]
/// * Checkbox: [Checkbox]
/// * Dialogs: [Dialog], [AlertDialog]
/// * Lists: [ListTile]
/// * Navigation bar: [NavigationBar] (new, replacing [BottomNavigationBar])

View file

@ -1088,6 +1088,7 @@ void main() {
reason: 'Default active pressed Checkbox should have overlay color from default fillColor',
);
await tester.pumpWidget(Container()); // reset test
await tester.pumpWidget(buildCheckbox(focused: true));
await tester.pumpAndSettle();
@ -1220,6 +1221,7 @@ void main() {
reason: 'Active pressed Checkbox should have overlay color: $activePressedOverlayColor',
);
await tester.pumpWidget(Container()); // reset test
await tester.pumpWidget(buildCheckbox(focused: true));
await tester.pumpAndSettle();
@ -1550,6 +1552,77 @@ void main() {
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tapTooltip), findsOneWidget);
});
testWidgets('Checkbox has default error color when isError is set to true - M3', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
Widget buildApp({bool autoFocus = true}) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Checkbox(
isError: true,
value: value,
onChanged: (bool? newValue) {
setState(() {
value = newValue;
});
},
autofocus: autoFocus,
focusNode: focusNode,
);
}),
),
),
);
}
// Focused
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: theme.colorScheme.error.withOpacity(0.12))..path(color: theme.colorScheme.error)..path(color: theme.colorScheme.onError)
);
// Default color
await tester.pumpWidget(Container());
await tester.pumpWidget(buildApp(autoFocus: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..path(color: theme.colorScheme.error)..path(color: theme.colorScheme.onError)
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: theme.colorScheme.error.withOpacity(0.08))
..path(color: theme.colorScheme.error)
);
// Start pressing
final TestGesture gestureLongPress = await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pump();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: theme.colorScheme.error.withOpacity(0.12))
..path(color: theme.colorScheme.error)
);
await gestureLongPress.up();
await tester.pump();
});
}
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {