mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
CupertinoButton & Bottom tab bar dark mode (#39765)
* CupertinoTabBar * CupertinoButton * update * review
This commit is contained in:
parent
b6abf0ca3d
commit
443892bd27
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user