Unverified Commit 443892bd authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

CupertinoButton & Bottom tab bar dark mode (#39765)

* CupertinoTabBar

* CupertinoButton

* update

* review
parent b6abf0ca
......@@ -12,7 +12,14 @@ import 'theme.dart';
// Standard iOS 10 tab bar height.
const double _kTabBarHeight = 50.0;
const Color _kDefaultTabBarBorderColor = Color(0x4C000000);
const Color _kDefaultTabBarBorderColor = CupertinoDynamicColor.withBrightness(
color: Color(0x4C000000),
darkColor: Color(0x29000000),
);
const Color _kDefaultTabBarInactiveColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFF999999),
darkColor: Color(0xFF757575),
);
/// An iOS-styled bottom navigation tab bar.
///
......@@ -52,7 +59,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
this.currentIndex = 0,
this.backgroundColor,
this.activeColor,
this.inactiveColor = CupertinoColors.inactiveGray,
this.inactiveColor = _kDefaultTabBarInactiveColor,
this.iconSize = 30.0,
this.border = const Border(
top: BorderSide(
......@@ -106,7 +113,8 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
/// The foreground color of the icon and title for the [BottomNavigationBarItem]s
/// in the unselected state.
///
/// Defaults to [CupertinoColors.inactiveGray] and cannot be null.
/// Defaults to a [CupertinoDynamicColor] that matches the disabled foreground
/// color of the native `UITabBar` component. Cannot be null.
final Color inactiveColor;
/// The size of all of the [BottomNavigationBarItem] icons.
......@@ -131,27 +139,46 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
bool opaque(BuildContext context) {
final Color backgroundColor =
this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor;
return backgroundColor.alpha == 0xFF;
return CupertinoDynamicColor.resolve(backgroundColor, context).alpha == 0xFF;
}
@override
Widget build(BuildContext context) {
final double bottomPadding = MediaQuery.of(context).padding.bottom;
final Color backgroundColor = CupertinoDynamicColor.resolve(
this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
context,
);
BorderSide resolveBorderSide(BorderSide side) {
return side == BorderSide.none
? side
: side.copyWith(color: CupertinoDynamicColor.resolve(side.color, context));
}
// Return the border as is when it's a subclass.
final Border resolvedBorder = border == null || border.runtimeType != Border
? border
: Border(
top: resolveBorderSide(border.top),
left: resolveBorderSide(border.left),
bottom: resolveBorderSide(border.bottom),
right: resolveBorderSide(border.right),
);
final Color inactive = CupertinoDynamicColor.resolve(inactiveColor, context);
Widget result = DecoratedBox(
decoration: BoxDecoration(
border: border,
color: backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
border: resolvedBorder,
color: backgroundColor,
),
child: SizedBox(
height: _kTabBarHeight + bottomPadding,
child: IconTheme.merge( // Default with the inactive state.
data: IconThemeData(
color: inactiveColor,
size: iconSize,
),
data: IconThemeData(color: inactive, size: iconSize),
child: DefaultTextStyle( // Default with the inactive state.
style: CupertinoTheme.of(context).textTheme.tabLabelTextStyle.copyWith(color: inactiveColor),
style: CupertinoTheme.of(context).textTheme.tabLabelTextStyle.copyWith(color: inactive),
child: Padding(
padding: EdgeInsets.only(bottom: bottomPadding),
child: Row(
......@@ -213,17 +240,12 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
}
List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
final List<Widget> components = <Widget>[
return <Widget>[
Expanded(
child: Center(child: active ? item.activeIcon : item.icon),
),
if (item.title != null) item.title,
];
if (item.title != null) {
components.add(item.title);
}
return components;
}
/// Change the active tab item's icon and title colors to active.
......@@ -231,7 +253,10 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
if (!active)
return item;
final Color activeColor = this.activeColor ?? CupertinoTheme.of(context).primaryColor;
final Color activeColor = CupertinoDynamicColor.resolve(
this.activeColor ?? CupertinoTheme.of(context).primaryColor,
context,
);
return IconTheme.merge(
data: IconThemeData(color: activeColor),
child: DefaultTextStyle.merge(
......
......@@ -5,13 +5,11 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
const Color _kDisabledBackground = Color(0xFFA9A9A9);
// Measured against iOS 12 in Xcode.
const Color _kDisabledForeground = Color(0xFFD1D1D1);
const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0);
const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric(
vertical: 14.0,
......@@ -84,8 +82,8 @@ class CupertinoButton extends StatefulWidget {
///
/// Ignored if the [CupertinoButton] doesn't also have a [color].
///
/// Defaults to a standard iOS disabled color when [color] is specified and
/// [disabledColor] is null.
/// Defaults to [CupertinoSystemColors.quaternarySystemFill] when [color] is
/// specified and [disabledColor] is null.
final Color disabledColor;
/// The callback that is called when the button is tapped or otherwise activated.
......@@ -206,15 +204,19 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
@override
Widget build(BuildContext context) {
final bool enabled = widget.enabled;
final Color primaryColor = CupertinoTheme.of(context).primaryColor;
final Color backgroundColor = widget.color ?? (widget._filled ? primaryColor : null);
final CupertinoThemeData themeData = CupertinoTheme.of(context);
final Color primaryColor = themeData.primaryColor;
final Color backgroundColor = widget.color == null
? (widget._filled ? primaryColor : null)
: CupertinoDynamicColor.resolve(widget.color, context);
final Color foregroundColor = backgroundColor != null
? CupertinoTheme.of(context).primaryContrastingColor
: enabled
? primaryColor
: _kDisabledForeground;
final TextStyle textStyle =
CupertinoTheme.of(context).textTheme.textStyle.copyWith(color: foregroundColor);
? themeData.primaryContrastingColor
: enabled
? primaryColor
: CupertinoDynamicColor.resolve(CupertinoSystemColors.of(context).placeholderText, context);
final TextStyle textStyle = themeData.textTheme.textStyle.copyWith(color: foregroundColor);
return GestureDetector(
behavior: HitTestBehavior.opaque,
......@@ -237,7 +239,7 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
decoration: BoxDecoration(
borderRadius: widget.borderRadius,
color: backgroundColor != null && !enabled
? widget.disabledColor ?? _kDisabledBackground
? CupertinoDynamicColor.resolve(widget.disabledColor ?? CupertinoSystemColors.of(context).quaternarySystemFill, context)
: backgroundColor,
),
child: Padding(
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../painting/mocks_for_image_cache.dart';
......@@ -68,6 +69,94 @@ void main() {
expect(actualActive.text.style.color, const Color(0xFF123456));
});
testWidgets('Active and inactive colors dark mode', (WidgetTester tester) async {
const CupertinoDynamicColor dynamicActiveColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFF000000),
darkColor: Color(0xFF000001),
);
const CupertinoDynamicColor dynamicInactiveColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFF000002),
darkColor: Color(0xFF000003),
);
await pumpWidgetWithBoilerplate(tester, MediaQuery(
data: const MediaQueryData(),
child: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 1'),
),
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 2'),
),
],
currentIndex: 1,
activeColor: dynamicActiveColor,
inactiveColor: dynamicInactiveColor,
),
));
RichText actualInactive = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(actualInactive.text.style.color.value, 0xFF000002);
RichText actualActive = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(actualActive.text.style.color.value, 0xFF000000);
final RenderDecoratedBox renderDecoratedBox = tester.renderObject(find.descendant(
of: find.byType(BackdropFilter),
matching: find.byType(DecoratedBox),
));
// Border color is resolved correctly.
final BoxDecoration decoration1 = renderDecoratedBox.decoration;
expect(decoration1.border.top.color.value, 0x4C000000);
// Switch to dark mode.
await pumpWidgetWithBoilerplate(tester, MediaQuery(
data: const MediaQueryData(platformBrightness: Brightness.dark),
child: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 1'),
),
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 2'),
),
],
currentIndex: 1,
activeColor: dynamicActiveColor,
inactiveColor: dynamicInactiveColor,
),
));
actualInactive = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(actualInactive.text.style.color.value, 0xFF000003);
actualActive = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(actualActive.text.style.color.value, 0xFF000001);
// Border color is resolved correctly.
final BoxDecoration decoration2 = renderDecoratedBox.decoration;
expect(decoration2.border.top.color.value, 0x29000000);
});
testWidgets('Tabs respects themes', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
......@@ -91,7 +180,7 @@ void main() {
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(actualInactive.text.style.color, CupertinoColors.inactiveGray);
expect(actualInactive.text.style.color.value, 0xFF999999);
RichText actualActive = tester.widget(find.descendant(
of: find.text('Tab 2'),
......@@ -122,7 +211,7 @@ void main() {
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(actualInactive.text.style.color, CupertinoColors.inactiveGray);
expect(actualInactive.text.style.color.value, 0xFF757575);
actualActive = tester.widget(find.descendant(
of: find.text('Tab 2'),
......
......@@ -228,6 +228,55 @@ void main() {
expect(boxDecoration.color, const Color(0x0000FF00));
});
testWidgets('Can specify dynamic colors', (WidgetTester tester) async {
const Color bgColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFF123456),
darkColor: Color(0xFF654321),
);
const Color inactive = CupertinoDynamicColor.withBrightness(
color: Color(0xFF111111),
darkColor: Color(0xFF222222),
);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(platformBrightness: Brightness.dark),
child: boilerplate(child: CupertinoButton(
child: const Text('Skeuomorph me'),
color: bgColor,
disabledColor: inactive,
onPressed: () { },
))
),
);
BoxDecoration boxDecoration = tester.widget<DecoratedBox>(
find.widgetWithText(DecoratedBox, 'Skeuomorph me')
).decoration;
expect(boxDecoration.color.value, 0xFF654321);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(platformBrightness: Brightness.light),
child: boilerplate(child: const CupertinoButton(
child: Text('Skeuomorph me'),
color: bgColor,
disabledColor: inactive,
onPressed: null,
))
),
);
boxDecoration = tester.widget<DecoratedBox>(
find.widgetWithText(DecoratedBox, 'Skeuomorph me')
).decoration;
// Disabled color.
expect(boxDecoration.color.value, 0xFF111111);
});
testWidgets('Button respects themes', (WidgetTester tester) async {
TextStyle textStyle;
......
......@@ -166,7 +166,7 @@ void main() {
matching: find.byType(RichText),
));
// Tab 2 should still be selected after changing theme.
expect(tab1.text.style.color, CupertinoColors.inactiveGray);
expect(tab1.text.style.color.value, 0xFF757575);
final RichText tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
......
......@@ -76,7 +76,7 @@ void main() {
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(tab2.text.style.color, CupertinoColors.inactiveGray);
expect(tab2.text.style.color.value, 0xFF999999);
await tester.tap(find.text('Tab 2'));
await tester.pump();
......@@ -86,7 +86,7 @@ void main() {
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(tab1.text.style.color, CupertinoColors.inactiveGray);
expect(tab1.text.style.color.value, 0xFF999999);
tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
......@@ -373,7 +373,7 @@ void main() {
matching: find.byType(RichText),
));
// Tab 2 should still be selected after changing theme.
expect(tab1.text.style.color, CupertinoColors.inactiveGray);
expect(tab1.text.style.color.value, 0xFF757575);
final RichText tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
......
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