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 {
/// Black with 45% opacity.
///
/// Used for disabled icons.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/Colors.blacks.png)
///
/// See also:
......@@ -254,7 +252,9 @@ class Colors {
/// 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)
///
......@@ -303,7 +303,7 @@ class Colors {
/// * [Typography.white], which uses this color for its text styles.
/// * [Theme.of], which allows you to select colors from the current theme
/// 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.
/// * [black], a solid black color.
/// * [transparent], a fully-transparent color.
......@@ -320,11 +320,14 @@ class Colors {
/// * [Typography.white], which uses this color for its text styles.
/// * [Theme.of], which allows you to select colors from the current theme
/// 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.
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)
///
......@@ -333,11 +336,23 @@ class Colors {
/// * [ExpandIcon], which uses this color for dark themes.
/// * [Theme.of], which allows you to select colors from the current theme
/// 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.
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.
///
......@@ -348,7 +363,7 @@ class Colors {
/// * [ThemeData.disabledColor], which uses this color by default in dark themes.
/// * [Theme.of], which allows you to select colors from the current theme
/// 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.
static const Color white30 = Color(0x4DFFFFFF);
......@@ -360,7 +375,7 @@ class Colors {
///
/// 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.
static const Color white24 = Color(0x3DFFFFFF);
......@@ -372,7 +387,7 @@ class Colors {
///
/// 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.
static const Color white12 = Color(0x1FFFFFFF);
......@@ -382,7 +397,7 @@ class Colors {
///
/// 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.
/// * [transparent], a fully-transparent color, not far from this one.
static const Color white10 = Color(0x1AFFFFFF);
......
......@@ -22,6 +22,10 @@ import 'theme.dart';
///
/// See [IconButton] for a more general implementation of a pressable button
/// with an icon.
///
/// See also:
///
/// * https://material.io/design/iconography/system-icons.html
class ExpandIcon extends StatefulWidget {
/// Creates an [ExpandIcon] with the given padding, and a callback that is
/// triggered when the icon is pressed.
......@@ -31,6 +35,9 @@ class ExpandIcon extends StatefulWidget {
this.size = 24.0,
@required this.onPressed,
this.padding = const EdgeInsets.all(8.0),
this.color,
this.disabledColor,
this.expandedColor,
}) : assert(isExpanded != null),
assert(size != null),
assert(padding != null),
......@@ -59,6 +66,35 @@ class ExpandIcon extends StatefulWidget {
/// This property must not be null. It defaults to 8.0 padding on all sides.
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
_ExpandIconState createState() => _ExpandIconState();
}
......@@ -104,19 +140,44 @@ class _ExpandIconState extends State<ExpandIcon> with SingleTickerProviderStateM
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
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData theme = Theme.of(context);
final String onTapHint = widget.isExpanded ? localizations.expandedIconTapHint : localizations.collapsedIconTapHint;
return Semantics(
onTapHint: widget.onPressed == null ? null : onTapHint,
child: IconButton(
padding: widget.padding,
color: theme.brightness == Brightness.dark ? Colors.white54 : Colors.black54,
color: _iconColor,
disabledColor: widget.disabledColor,
onPressed: widget.onPressed == null ? null : _handlePressed,
icon: RotationTransition(
turns: _iconTurns,
......
......@@ -5,65 +5,116 @@
import 'package:flutter/material.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() {
testWidgets('ExpandIcon test', (WidgetTester tester) async {
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(
wrap(
child: ExpandIcon(
onPressed: (bool isExpanded) {
expanded = !expanded;
}
)
)
);
await tester.tap(find.byType(ExpandIcon));
await tester.pumpAndSettle();
expect(expanded, isFalse);
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
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);
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.white60));
await tester.tap(find.byType(ExpandIcon));
await tester.pumpAndSettle();
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.pumpAndSettle();
expect(expanded, isFalse);
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
expect(iconTheme.data.color, equals(Colors.white60));
});
testWidgets('ExpandIcon disabled', (WidgetTester tester) async {
await tester.pumpWidget(
wrap(
child: const ExpandIcon(
onPressed: null
)
)
);
final IconTheme iconTheme = tester.firstWidget(find.byType(IconTheme).last);
IconTheme iconTheme;
// Light mode test
await tester.pumpWidget(wrap(
child: const ExpandIcon(onPressed: null),
));
await tester.pumpAndSettle();
iconTheme = tester.firstWidget(find.byType(IconTheme).last);
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 {
bool expanded = false;
await tester.pumpWidget(
wrap(
child: ExpandIcon(
isExpanded: false,
onPressed: (bool isExpanded) {
expanded = !expanded;
},
)
)
);
await tester.pumpWidget(
wrap(
child: ExpandIcon(
isExpanded: true,
onPressed: (bool isExpanded) {
expanded = !expanded;
},
)
)
);
await tester.pumpWidget(wrap(
child: ExpandIcon(
isExpanded: false,
onPressed: (bool isExpanded) {
expanded = !expanded;
},
),
));
await tester.pumpWidget(wrap(
child: ExpandIcon(
isExpanded: true,
onPressed: (bool isExpanded) {
expanded = !expanded;
},
),
));
expect(expanded, isFalse);
});
......@@ -71,16 +122,14 @@ void main() {
testWidgets('ExpandIcon is rotated initially if isExpanded is true on first build', (WidgetTester tester) async {
bool expanded = true;
await tester.pumpWidget(
wrap(
child: ExpandIcon(
isExpanded: expanded,
onPressed: (bool isExpanded) {
expanded = !isExpanded;
},
)
)
);
await tester.pumpWidget(wrap(
child: ExpandIcon(
isExpanded: expanded,
onPressed: (bool isExpanded) {
expanded = !isExpanded;
},
),
));
final RotationTransition rotation = tester.firstWidget(find.byType(RotationTransition));
expect(rotation.turns.value, 0.5);
});
......@@ -89,10 +138,10 @@ void main() {
final SemanticsHandle handle = tester.ensureSemantics();
const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations();
await tester.pumpWidget(wrap(
child: ExpandIcon(
isExpanded: true,
onPressed: (bool _) { },
)
child: ExpandIcon(
isExpanded: true,
onPressed: (bool _) { },
),
));
expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics(
......@@ -107,7 +156,7 @@ void main() {
child: ExpandIcon(
isExpanded: false,
onPressed: (bool _) { },
)
),
));
expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics(
......@@ -119,12 +168,109 @@ void main() {
));
handle.dispose();
});
}
Widget wrap({ Widget child }) {
return MaterialApp(
home: Center(
child: Material(child: child),
),
);
testWidgets('ExpandIcon uses custom icon color and expanded icon color', (WidgetTester tester) async {
bool expanded = false;
IconTheme iconTheme;
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