From 443892bd2717643e2fffbf902472aaeb377ed9a2 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 5 Sep 2019 10:06:45 -0700 Subject: [PATCH] CupertinoButton & Bottom tab bar dark mode (#39765) * CupertinoTabBar * CupertinoButton * update * review --- .../lib/src/cupertino/bottom_tab_bar.dart | 63 +++++++++---- .../flutter/lib/src/cupertino/button.dart | 30 +++--- .../test/cupertino/bottom_tab_bar_test.dart | 93 ++++++++++++++++++- .../flutter/test/cupertino/button_test.dart | 49 ++++++++++ .../cupertino/material/tab_scaffold_test.dart | 2 +- .../test/cupertino/tab_scaffold_test.dart | 6 +- 6 files changed, 204 insertions(+), 39 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart index 150948aad79..338872d28ca 100644 --- a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart +++ b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart @@ -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 _buildSingleTabItem(BottomNavigationBarItem item, bool active) { - final List components = [ + return [ 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( diff --git a/packages/flutter/lib/src/cupertino/button.dart b/packages/flutter/lib/src/cupertino/button.dart index b7203e7119f..627097816b1 100644 --- a/packages/flutter/lib/src/cupertino/button.dart +++ b/packages/flutter/lib/src/cupertino/button.dart @@ -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 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 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( diff --git a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart index 2f88b3339d3..a3d716ee096 100644 --- a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart +++ b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart @@ -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( + 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( + 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'), diff --git a/packages/flutter/test/cupertino/button_test.dart b/packages/flutter/test/cupertino/button_test.dart index a68ff9a85fe..2587f6b0e2c 100644 --- a/packages/flutter/test/cupertino/button_test.dart +++ b/packages/flutter/test/cupertino/button_test.dart @@ -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( + 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( + find.widgetWithText(DecoratedBox, 'Skeuomorph me') + ).decoration; + + // Disabled color. + expect(boxDecoration.color.value, 0xFF111111); + }); + testWidgets('Button respects themes', (WidgetTester tester) async { TextStyle textStyle; diff --git a/packages/flutter/test/cupertino/material/tab_scaffold_test.dart b/packages/flutter/test/cupertino/material/tab_scaffold_test.dart index 686704586a6..7b52ce7d6d8 100644 --- a/packages/flutter/test/cupertino/material/tab_scaffold_test.dart +++ b/packages/flutter/test/cupertino/material/tab_scaffold_test.dart @@ -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), diff --git a/packages/flutter/test/cupertino/tab_scaffold_test.dart b/packages/flutter/test/cupertino/tab_scaffold_test.dart index 2c86105d46d..a54696e5eea 100644 --- a/packages/flutter/test/cupertino/tab_scaffold_test.dart +++ b/packages/flutter/test/cupertino/tab_scaffold_test.dart @@ -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),