Unverified Commit 9d520853 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

ExpandIcon Custom Colors (#33148)

* Implement ExpandIcon custom color, expandedColor, and disabledColor

* Update to use pumpAndSettle instead of hard-coded duration

* Update colors to unfocused state, added dark mode test to active state

* Fix Colors.white30 doc opacity value

* Add links to Material Design specifications to color, expandedColor and disabledColor

* Update API docs to reference dark theme material page
parent 73798a15
...@@ -242,8 +242,6 @@ class Colors { ...@@ -242,8 +242,6 @@ class Colors {
/// Black with 45% opacity. /// Black with 45% opacity.
/// ///
/// Used for disabled icons.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.blacks.png) /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.blacks.png)
/// ///
/// See also: /// See also:
...@@ -254,7 +252,9 @@ class Colors { ...@@ -254,7 +252,9 @@ class Colors {
/// Black with 38% opacity. /// Black with 38% opacity.
/// ///
/// Used for the placeholder text in data tables in light themes. /// For light themes, i.e. when the Theme's [ThemeData.brightness] is
/// [Brightness.light], this color is used for disabled icons and for
/// placeholder text in [DataTable].
/// ///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.blacks.png) /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.blacks.png)
/// ///
...@@ -303,7 +303,7 @@ class Colors { ...@@ -303,7 +303,7 @@ class Colors {
/// * [Typography.white], which uses this color for its text styles. /// * [Typography.white], which uses this color for its text styles.
/// * [Theme.of], which allows you to select colors from the current theme /// * [Theme.of], which allows you to select colors from the current theme
/// rather than hard-coding colors in your build methods. /// rather than hard-coding colors in your build methods.
/// * [white70, white30, white12, white10], which are variants on this color /// * [white70, white60, white54, white30, white12, white10], which are variants on this color
/// but with different opacities. /// but with different opacities.
/// * [black], a solid black color. /// * [black], a solid black color.
/// * [transparent], a fully-transparent color. /// * [transparent], a fully-transparent color.
...@@ -320,11 +320,14 @@ class Colors { ...@@ -320,11 +320,14 @@ class Colors {
/// * [Typography.white], which uses this color for its text styles. /// * [Typography.white], which uses this color for its text styles.
/// * [Theme.of], which allows you to select colors from the current theme /// * [Theme.of], which allows you to select colors from the current theme
/// rather than hard-coding colors in your build methods. /// rather than hard-coding colors in your build methods.
/// * [white, white30, white12, white10], which are variants on this color /// * [white, white60, white54, white30, white12, white10], which are variants on this color
/// but with different opacities. /// but with different opacities.
static const Color white70 = Color(0xB3FFFFFF); static const Color white70 = Color(0xB3FFFFFF);
/// White with 54% opacity. /// White with 60% opacity.
///
/// Used for medium-emphasis text and hint text when [Theme.brightness] is
/// set to [Brightness.dark].
/// ///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.whites.png) /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.whites.png)
/// ///
...@@ -333,11 +336,23 @@ class Colors { ...@@ -333,11 +336,23 @@ class Colors {
/// * [ExpandIcon], which uses this color for dark themes. /// * [ExpandIcon], which uses this color for dark themes.
/// * [Theme.of], which allows you to select colors from the current theme /// * [Theme.of], which allows you to select colors from the current theme
/// rather than hard-coding colors in your build methods. /// rather than hard-coding colors in your build methods.
/// * [white, white30, white12, white10], which are variants on this color /// * [white, white54, white30, white12, white10], which are variants on this color
/// but with different opacities.
static const Color white60 = Color(0x99FFFFFF);
/// White with 54% opacity.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.whites.png)
///
/// See also:
///
/// * [Theme.of], which allows you to select colors from the current theme
/// rather than hard-coding colors in your build methods.
/// * [white, white60, white30, white12, white10], which are variants on this color
/// but with different opacities. /// but with different opacities.
static const Color white54 = Color(0x8AFFFFFF); static const Color white54 = Color(0x8AFFFFFF);
/// White with 32% opacity. /// White with 30% opacity.
/// ///
/// Used for disabled radio buttons and the text of disabled flat buttons in dark themes. /// Used for disabled radio buttons and the text of disabled flat buttons in dark themes.
/// ///
...@@ -348,7 +363,7 @@ class Colors { ...@@ -348,7 +363,7 @@ class Colors {
/// * [ThemeData.disabledColor], which uses this color by default in dark themes. /// * [ThemeData.disabledColor], which uses this color by default in dark themes.
/// * [Theme.of], which allows you to select colors from the current theme /// * [Theme.of], which allows you to select colors from the current theme
/// rather than hard-coding colors in your build methods. /// rather than hard-coding colors in your build methods.
/// * [white, white70, white12, white10], which are variants on this color /// * [white, white60, white54, white70, white12, white10], which are variants on this color
/// but with different opacities. /// but with different opacities.
static const Color white30 = Color(0x4DFFFFFF); static const Color white30 = Color(0x4DFFFFFF);
...@@ -360,7 +375,7 @@ class Colors { ...@@ -360,7 +375,7 @@ class Colors {
/// ///
/// See also: /// See also:
/// ///
/// * [white, white70, white30, white10], which are variants on this color /// * [white, white60, white54, white70, white30, white10], which are variants on this color
/// but with different opacities. /// but with different opacities.
static const Color white24 = Color(0x3DFFFFFF); static const Color white24 = Color(0x3DFFFFFF);
...@@ -372,7 +387,7 @@ class Colors { ...@@ -372,7 +387,7 @@ class Colors {
/// ///
/// See also: /// See also:
/// ///
/// * [white, white70, white30, white10], which are variants on this color /// * [white, white60, white54, white70, white30, white10], which are variants on this color
/// but with different opacities. /// but with different opacities.
static const Color white12 = Color(0x1FFFFFFF); static const Color white12 = Color(0x1FFFFFFF);
...@@ -382,7 +397,7 @@ class Colors { ...@@ -382,7 +397,7 @@ class Colors {
/// ///
/// See also: /// See also:
/// ///
/// * [white, white70, white30, white12], which are variants on this color /// * [white, white60, white54, white70, white30, white12], which are variants on this color
/// but with different opacities. /// but with different opacities.
/// * [transparent], a fully-transparent color, not far from this one. /// * [transparent], a fully-transparent color, not far from this one.
static const Color white10 = Color(0x1AFFFFFF); static const Color white10 = Color(0x1AFFFFFF);
......
...@@ -22,6 +22,10 @@ import 'theme.dart'; ...@@ -22,6 +22,10 @@ import 'theme.dart';
/// ///
/// See [IconButton] for a more general implementation of a pressable button /// See [IconButton] for a more general implementation of a pressable button
/// with an icon. /// with an icon.
///
/// See also:
///
/// * https://material.io/design/iconography/system-icons.html
class ExpandIcon extends StatefulWidget { class ExpandIcon extends StatefulWidget {
/// Creates an [ExpandIcon] with the given padding, and a callback that is /// Creates an [ExpandIcon] with the given padding, and a callback that is
/// triggered when the icon is pressed. /// triggered when the icon is pressed.
...@@ -31,6 +35,9 @@ class ExpandIcon extends StatefulWidget { ...@@ -31,6 +35,9 @@ class ExpandIcon extends StatefulWidget {
this.size = 24.0, this.size = 24.0,
@required this.onPressed, @required this.onPressed,
this.padding = const EdgeInsets.all(8.0), this.padding = const EdgeInsets.all(8.0),
this.color,
this.disabledColor,
this.expandedColor,
}) : assert(isExpanded != null), }) : assert(isExpanded != null),
assert(size != null), assert(size != null),
assert(padding != null), assert(padding != null),
...@@ -59,6 +66,35 @@ class ExpandIcon extends StatefulWidget { ...@@ -59,6 +66,35 @@ class ExpandIcon extends StatefulWidget {
/// This property must not be null. It defaults to 8.0 padding on all sides. /// This property must not be null. It defaults to 8.0 padding on all sides.
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
/// The color of the icon.
///
/// Defaults to [Colors.black54] when the theme's
/// [ThemeData.brightness] is [Brightness.light] and to
/// [Colors.white60] when it is [Brightness.dark]. This adheres to the
/// Material Design specifications for [icons](https://material.io/design/iconography/system-icons.html#color)
/// and for [dark theme](https://material.io/design/color/dark-theme.html#ui-application)
final Color color;
/// The color of the icon when it is disabled,
/// i.e. if [onPressed] is null.
///
/// Defaults to [Colors.black38] when the theme's
/// [ThemeData.brightness] is [Brightness.light] and to
/// [Colors.white30] when it is [Brightness.dark]. This adheres to the
/// Material Design specifications for [icons](https://material.io/design/iconography/system-icons.html#color)
/// and for [dark theme](https://material.io/design/color/dark-theme.html#ui-application)
final Color disabledColor;
/// The color of the icon when the icon is expanded.
///
/// Defaults to [Colors.black54] when the theme's
/// [ThemeData.brightness] is [Brightness.light] and to
/// [Colors.white] when it is [Brightness.dark]. This adheres to the
/// Material Design specifications for [icons](https://material.io/design/iconography/system-icons.html#color)
/// and for [dark theme](https://material.io/design/color/dark-theme.html#ui-application)
final Color expandedColor;
@override @override
_ExpandIconState createState() => _ExpandIconState(); _ExpandIconState createState() => _ExpandIconState();
} }
...@@ -104,19 +140,44 @@ class _ExpandIconState extends State<ExpandIcon> with SingleTickerProviderStateM ...@@ -104,19 +140,44 @@ class _ExpandIconState extends State<ExpandIcon> with SingleTickerProviderStateM
widget.onPressed(widget.isExpanded); widget.onPressed(widget.isExpanded);
} }
/// Default icon colors and opacities for when [Theme.brightness] is set to
/// [Brightness.light] are based on the
/// [Material Design system icon specifications](https://material.io/design/iconography/system-icons.html#color).
/// Icon colors and opacities for [Brightness.dark] are based on the
/// [Material Design dark theme specifications](https://material.io/design/color/dark-theme.html#ui-application)
Color get _iconColor {
if (widget.isExpanded && widget.expandedColor != null) {
return widget.expandedColor;
}
if (widget.color != null) {
return widget.color;
}
switch(Theme.of(context).brightness) {
case Brightness.light:
return Colors.black54;
case Brightness.dark:
return Colors.white60;
}
assert(false);
return null;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations = MaterialLocalizations.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData theme = Theme.of(context);
final String onTapHint = widget.isExpanded ? localizations.expandedIconTapHint : localizations.collapsedIconTapHint; final String onTapHint = widget.isExpanded ? localizations.expandedIconTapHint : localizations.collapsedIconTapHint;
return Semantics( return Semantics(
onTapHint: widget.onPressed == null ? null : onTapHint, onTapHint: widget.onPressed == null ? null : onTapHint,
child: IconButton( child: IconButton(
padding: widget.padding, padding: widget.padding,
color: theme.brightness == Brightness.dark ? Colors.white54 : Colors.black54, color: _iconColor,
disabledColor: widget.disabledColor,
onPressed: widget.onPressed == null ? null : _handlePressed, onPressed: widget.onPressed == null ? null : _handlePressed,
icon: RotationTransition( icon: RotationTransition(
turns: _iconTurns, turns: _iconTurns,
......
...@@ -5,65 +5,116 @@ ...@@ -5,65 +5,116 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
Widget wrap({ Widget child, ThemeData theme }) {
return MaterialApp(
theme: theme,
home: Center(
child: Material(child: child),
),
);
}
void main() { void main() {
testWidgets('ExpandIcon test', (WidgetTester tester) async { testWidgets('ExpandIcon test', (WidgetTester tester) async {
bool expanded = false; bool expanded = false;
IconTheme iconTheme;
// Light mode tests
await tester.pumpWidget(wrap(
child: ExpandIcon(
onPressed: (bool isExpanded) {
expanded = !expanded;
}
),
));
await tester.pumpAndSettle();
expect(expanded, isFalse);
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.black54));
await tester.tap(find.byType(ExpandIcon));
await tester.pumpAndSettle();
expect(expanded, isTrue);
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.black54));
await tester.pumpWidget( await tester.tap(find.byType(ExpandIcon));
wrap( await tester.pumpAndSettle();
child: ExpandIcon( expect(expanded, isFalse);
onPressed: (bool isExpanded) { iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expanded = !expanded; expect(iconTheme.data.color, equals(Colors.black54));
}
) // Dark mode tests
) await tester.pumpWidget(wrap(
); child: ExpandIcon(
onPressed: (bool isExpanded) {
expanded = !expanded;
},
),
theme: ThemeData(brightness: Brightness.dark),
));
await tester.pumpAndSettle();
expect(expanded, isFalse); expect(expanded, isFalse);
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.white60));
await tester.tap(find.byType(ExpandIcon)); await tester.tap(find.byType(ExpandIcon));
await tester.pumpAndSettle();
expect(expanded, isTrue); expect(expanded, isTrue);
await tester.pump(const Duration(milliseconds: 100)); iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.white60));
await tester.tap(find.byType(ExpandIcon)); await tester.tap(find.byType(ExpandIcon));
await tester.pumpAndSettle();
expect(expanded, isFalse); expect(expanded, isFalse);
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.white60));
}); });
testWidgets('ExpandIcon disabled', (WidgetTester tester) async { testWidgets('ExpandIcon disabled', (WidgetTester tester) async {
await tester.pumpWidget( IconTheme iconTheme;
wrap( // Light mode test
child: const ExpandIcon( await tester.pumpWidget(wrap(
onPressed: null child: const ExpandIcon(onPressed: null),
) ));
) await tester.pumpAndSettle();
);
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
final IconTheme iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.black38)); expect(iconTheme.data.color, equals(Colors.black38));
// Dark mode test
await tester.pumpWidget(wrap(
child: const ExpandIcon(onPressed: null),
theme: ThemeData(brightness: Brightness.dark),
));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.white30));
}); });
testWidgets('ExpandIcon test isExpanded does not trigger callback', (WidgetTester tester) async { testWidgets('ExpandIcon test isExpanded does not trigger callback', (WidgetTester tester) async {
bool expanded = false; bool expanded = false;
await tester.pumpWidget( await tester.pumpWidget(wrap(
wrap( child: ExpandIcon(
child: ExpandIcon( isExpanded: false,
isExpanded: false, onPressed: (bool isExpanded) {
onPressed: (bool isExpanded) { expanded = !expanded;
expanded = !expanded; },
}, ),
) ));
)
); await tester.pumpWidget(wrap(
child: ExpandIcon(
await tester.pumpWidget( isExpanded: true,
wrap( onPressed: (bool isExpanded) {
child: ExpandIcon( expanded = !expanded;
isExpanded: true, },
onPressed: (bool isExpanded) { ),
expanded = !expanded; ));
},
)
)
);
expect(expanded, isFalse); expect(expanded, isFalse);
}); });
...@@ -71,16 +122,14 @@ void main() { ...@@ -71,16 +122,14 @@ void main() {
testWidgets('ExpandIcon is rotated initially if isExpanded is true on first build', (WidgetTester tester) async { testWidgets('ExpandIcon is rotated initially if isExpanded is true on first build', (WidgetTester tester) async {
bool expanded = true; bool expanded = true;
await tester.pumpWidget( await tester.pumpWidget(wrap(
wrap( child: ExpandIcon(
child: ExpandIcon( isExpanded: expanded,
isExpanded: expanded, onPressed: (bool isExpanded) {
onPressed: (bool isExpanded) { expanded = !isExpanded;
expanded = !isExpanded; },
}, ),
) ));
)
);
final RotationTransition rotation = tester.firstWidget(find.byType(RotationTransition)); final RotationTransition rotation = tester.firstWidget(find.byType(RotationTransition));
expect(rotation.turns.value, 0.5); expect(rotation.turns.value, 0.5);
}); });
...@@ -89,10 +138,10 @@ void main() { ...@@ -89,10 +138,10 @@ void main() {
final SemanticsHandle handle = tester.ensureSemantics(); final SemanticsHandle handle = tester.ensureSemantics();
const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations();
await tester.pumpWidget(wrap( await tester.pumpWidget(wrap(
child: ExpandIcon( child: ExpandIcon(
isExpanded: true, isExpanded: true,
onPressed: (bool _) { }, onPressed: (bool _) { },
) ),
)); ));
expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics( expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics(
...@@ -107,7 +156,7 @@ void main() { ...@@ -107,7 +156,7 @@ void main() {
child: ExpandIcon( child: ExpandIcon(
isExpanded: false, isExpanded: false,
onPressed: (bool _) { }, onPressed: (bool _) { },
) ),
)); ));
expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics( expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics(
...@@ -119,12 +168,109 @@ void main() { ...@@ -119,12 +168,109 @@ void main() {
)); ));
handle.dispose(); handle.dispose();
}); });
}
Widget wrap({ Widget child }) { testWidgets('ExpandIcon uses custom icon color and expanded icon color', (WidgetTester tester) async {
return MaterialApp( bool expanded = false;
home: Center( IconTheme iconTheme;
child: Material(child: child),
), await tester.pumpWidget(wrap(
); child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return ExpandIcon(
isExpanded: expanded,
onPressed: (bool isExpanded) {
setState(() {
expanded = !isExpanded;
});
},
color: Colors.indigo,
);
}),
));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.indigo));
await tester.tap(find.byType(ExpandIcon));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.indigo));
expanded = false;
await tester.pumpWidget(wrap(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return ExpandIcon(
isExpanded: expanded,
onPressed: (bool isExpanded) {
setState(() {
expanded = !isExpanded;
});
},
color: Colors.indigo,
expandedColor: Colors.teal,
);
}),
));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.indigo));
await tester.tap(find.byType(ExpandIcon));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.teal));
await tester.tap(find.byType(ExpandIcon));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.indigo));
});
testWidgets('ExpandIcon uses custom disabled icon color', (WidgetTester tester) async {
IconTheme iconTheme;
await tester.pumpWidget(wrap(
child: const ExpandIcon(
isExpanded: false,
onPressed: null,
disabledColor: Colors.cyan,
),
));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.cyan));
await tester.pumpWidget(wrap(
child: const ExpandIcon(
isExpanded: false,
onPressed: null,
color: Colors.indigo,
disabledColor: Colors.cyan,
),
));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.cyan));
await tester.pumpWidget(wrap(
child: const ExpandIcon(
isExpanded: true,
onPressed: null,
disabledColor: Colors.cyan,
),
));
await tester.pumpWidget(wrap(
child: const ExpandIcon(
isExpanded: true,
onPressed: null,
expandedColor: Colors.teal,
disabledColor: Colors.cyan,
),
));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.cyan));
});
} }
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