mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Migrate NavigationBar to M3 tokens. (#98285)
This commit is contained in:
parent
ccaf51562f
commit
ca2a751661
@ -18,6 +18,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:gen_defaults/fab_template.dart';
|
||||
import 'package:gen_defaults/navigation_bar_template.dart';
|
||||
import 'package:gen_defaults/typography_template.dart';
|
||||
|
||||
Map<String, dynamic> _readTokenFile(String fileName) {
|
||||
@ -64,5 +65,6 @@ Future<void> main(List<String> args) async {
|
||||
tokens['colorsDark'] = _readTokenFile('color_dark.json');
|
||||
|
||||
FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile();
|
||||
NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile();
|
||||
TypographyTemplate('$materialLib/typography.dart', tokens).updateFile();
|
||||
}
|
||||
|
@ -33,6 +33,10 @@
|
||||
"bottomRight": 0.0
|
||||
},
|
||||
|
||||
"md.sys.shape.corner.full": {
|
||||
"family": "SHAPE_FAMILY_CIRCULAR"
|
||||
},
|
||||
|
||||
"md.sys.shape.corner.large": {
|
||||
"family": "SHAPE_FAMILY_ROUNDED_CORNERS",
|
||||
"topLeft": 16.0,
|
||||
|
57
dev/tools/gen_defaults/lib/navigation_bar_template.dart
Normal file
57
dev/tools/gen_defaults/lib/navigation_bar_template.dart
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'template.dart';
|
||||
|
||||
class NavigationBarTemplate extends TokenTemplate {
|
||||
const NavigationBarTemplate(String fileName, Map<String, dynamic> tokens) : super(fileName, tokens);
|
||||
|
||||
@override
|
||||
String generate() => '''
|
||||
// Generated version ${tokens["version"]}
|
||||
class _TokenDefaultsM3 extends NavigationBarThemeData {
|
||||
_TokenDefaultsM3(BuildContext context)
|
||||
: _theme = Theme.of(context),
|
||||
_colors = Theme.of(context).colorScheme,
|
||||
super(
|
||||
height: ${tokens["md.comp.navigation-bar.container.height"]},
|
||||
elevation: ${elevation("md.comp.navigation-bar.container")},
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
);
|
||||
|
||||
final ThemeData _theme;
|
||||
final ColorScheme _colors;
|
||||
|
||||
// With Material 3, the NavigationBar uses an overlay blend for the
|
||||
// default color regardless of light/dark mode. This should be handled
|
||||
// in the Material widget based off of elevation, but for now we will do
|
||||
// it here in the defaults.
|
||||
@override Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.${color("md.comp.navigation-bar.container")}, _colors.primary, ${elevation("md.comp.navigation-bar.container")});
|
||||
|
||||
@override MaterialStateProperty<IconThemeData?>? get iconTheme {
|
||||
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
return IconThemeData(
|
||||
size: ${tokens["md.comp.navigation-bar.icon.size"]},
|
||||
color: states.contains(MaterialState.selected)
|
||||
? _colors.${color("md.comp.navigation-bar.active.icon")}
|
||||
: _colors.${color("md.comp.navigation-bar.inactive.icon")},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override Color? get indicatorColor => _colors.${color("md.comp.navigation-bar.active-indicator")};
|
||||
@override ShapeBorder? get indicatorShape => ${shape("md.comp.navigation-bar.active-indicator")};
|
||||
|
||||
@override MaterialStateProperty<TextStyle?>? get labelTextStyle {
|
||||
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
final TextStyle style = _theme.textTheme.${textStyle("md.comp.navigation-bar.label-text")}!;
|
||||
return style.apply(color: states.contains(MaterialState.selected)
|
||||
? _colors.${color("md.comp.navigation-bar.active.label-text")}
|
||||
: _colors.${color("md.comp.navigation-bar.inactive.label-text")}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
''';
|
||||
}
|
@ -76,17 +76,24 @@ abstract class TokenTemplate {
|
||||
|
||||
/// Generate a shape constant for the given component token.
|
||||
///
|
||||
/// Currently only supports "SHAPE_FAMILY_ROUNDED_CORNERS" which it
|
||||
/// maps to a [RoundedRectangleBorder] expression.
|
||||
/// Currently supports family:
|
||||
/// - "SHAPE_FAMILY_ROUNDED_CORNERS" which maps to [RoundedRectangleBorder].
|
||||
/// - "SHAPE_FAMILY_CIRCULAR" which maps to a [StadiumBorder].
|
||||
String shape(String componentToken) {
|
||||
// TODO(darrenaustin): handle more than just rounded rectangle shapes
|
||||
final Map<String, dynamic> shape = tokens[tokens['$componentToken.shape']!]! as Map<String, dynamic>;
|
||||
return 'const RoundedRectangleBorder(borderRadius: '
|
||||
'BorderRadius.only('
|
||||
'topLeft: Radius.circular(${shape['topLeft']}), '
|
||||
'topRight: Radius.circular(${shape['topRight']}), '
|
||||
'bottomLeft: Radius.circular(${shape['bottomLeft']}), '
|
||||
'bottomRight: Radius.circular(${shape['bottomRight']})))';
|
||||
switch (shape['family']) {
|
||||
case 'SHAPE_FAMILY_ROUNDED_CORNERS':
|
||||
return 'const RoundedRectangleBorder(borderRadius: '
|
||||
'BorderRadius.only('
|
||||
'topLeft: Radius.circular(${shape['topLeft']}), '
|
||||
'topRight: Radius.circular(${shape['topRight']}), '
|
||||
'bottomLeft: Radius.circular(${shape['bottomLeft']}), '
|
||||
'bottomRight: Radius.circular(${shape['bottomRight']})))';
|
||||
case 'SHAPE_FAMILY_CIRCULAR':
|
||||
return 'const StadiumBorder()';
|
||||
}
|
||||
print('Unsupported shape family type: ${shape['family']} for $componentToken');
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Generate a [TextTheme] text style name for the given component token.
|
||||
|
@ -101,16 +101,21 @@ static final String tokenBar = 'bar';
|
||||
test('Templates can get proper shapes from given data', () {
|
||||
const Map<String, dynamic> tokens = <String, dynamic>{
|
||||
'foo.shape': 'shape.large',
|
||||
'bar.shape': 'shape.full',
|
||||
'shape.large': <String, dynamic>{
|
||||
'family': 'SHAPE_FAMILY_ROUNDED_CORNERS',
|
||||
'topLeft': 1.0,
|
||||
'topRight': 2.0,
|
||||
'bottomLeft': 3.0,
|
||||
'bottomRight': 4.0,
|
||||
}
|
||||
},
|
||||
'shape.full': <String, dynamic>{
|
||||
'family': 'SHAPE_FAMILY_CIRCULAR',
|
||||
},
|
||||
};
|
||||
final TestTemplate template = TestTemplate('foobar.dart', tokens);
|
||||
expect(template.shape('foo'), 'const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(1.0), topRight: Radius.circular(2.0), bottomLeft: Radius.circular(3.0), bottomRight: Radius.circular(4.0)))');
|
||||
expect(template.shape('bar'), 'const StadiumBorder()');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,7 @@ class NavigationBar extends StatelessWidget {
|
||||
required this.destinations,
|
||||
this.onDestinationSelected,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.height,
|
||||
this.labelBehavior,
|
||||
}) : assert(destinations != null && destinations.length >= 2),
|
||||
@ -111,6 +112,13 @@ class NavigationBar extends StatelessWidget {
|
||||
/// [ColorScheme.onSurface] using an [ElevationOverlay].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The elevation of the [NavigationBar] itself.
|
||||
///
|
||||
/// If null, [NavigationBarThemeData.elevation] is used. If that
|
||||
/// is also null, then if [ThemeData.useMaterial3] is true then it will
|
||||
/// be 3.0 otherwise 0.0.
|
||||
final double? elevation;
|
||||
|
||||
/// The height of the [NavigationBar] itself.
|
||||
///
|
||||
/// If this is used in [Scaffold.bottomNavigationBar] and the scaffold is
|
||||
@ -145,20 +153,20 @@ class NavigationBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
final NavigationBarThemeData defaults = _defaultsFor(context);
|
||||
|
||||
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
|
||||
final double effectiveHeight = height ?? navigationBarTheme.height ?? 80;
|
||||
final double effectiveHeight = height ?? navigationBarTheme.height ?? defaults.height!;
|
||||
final NavigationDestinationLabelBehavior effectiveLabelBehavior = labelBehavior
|
||||
?? navigationBarTheme.labelBehavior
|
||||
?? NavigationDestinationLabelBehavior.alwaysShow;
|
||||
?? defaults.labelBehavior!;
|
||||
final double additionalBottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return Material(
|
||||
// With Material 3, the NavigationBar uses an overlay blend for the
|
||||
// default color regardless of light/dark mode.
|
||||
color: backgroundColor
|
||||
?? navigationBarTheme.backgroundColor
|
||||
?? ElevationOverlay.colorWithOverlay(colorScheme.surface, colorScheme.onSurface, 3.0),
|
||||
?? navigationBarTheme.backgroundColor
|
||||
?? defaults.backgroundColor!,
|
||||
elevation: elevation ?? navigationBarTheme.elevation ?? defaults.elevation!,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: additionalBottomPadding),
|
||||
child: MediaQuery.removePadding(
|
||||
@ -275,25 +283,25 @@ class NavigationDestination extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
const Set<MaterialState> selectedState = <MaterialState>{MaterialState.selected};
|
||||
const Set<MaterialState> unselectedState = <MaterialState>{};
|
||||
|
||||
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
|
||||
final NavigationBarThemeData defaults = _defaultsFor(context);
|
||||
final Animation<double> animation = _NavigationDestinationInfo.of(context).selectedAnimation;
|
||||
|
||||
return _NavigationDestinationBuilder(
|
||||
label: label,
|
||||
tooltip: tooltip,
|
||||
buildIcon: (BuildContext context) {
|
||||
final IconThemeData defaultIconTheme = IconThemeData(
|
||||
size: 24,
|
||||
color: colorScheme.onSurface,
|
||||
);
|
||||
final Widget selectedIconWidget = IconTheme.merge(
|
||||
data: navigationBarTheme.iconTheme?.resolve(<MaterialState>{MaterialState.selected}) ?? defaultIconTheme,
|
||||
data: navigationBarTheme.iconTheme?.resolve(selectedState)
|
||||
?? defaults.iconTheme!.resolve(selectedState)!,
|
||||
child: selectedIcon ?? icon,
|
||||
);
|
||||
final Widget unselectedIconWidget = IconTheme.merge(
|
||||
data: navigationBarTheme.iconTheme?.resolve(<MaterialState>{}) ?? defaultIconTheme,
|
||||
data: navigationBarTheme.iconTheme?.resolve(unselectedState)
|
||||
?? defaults.iconTheme!.resolve(unselectedState)!,
|
||||
child: icon,
|
||||
);
|
||||
|
||||
@ -302,25 +310,25 @@ class NavigationDestination extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
NavigationIndicator(
|
||||
animation: animation,
|
||||
color: navigationBarTheme.indicatorColor,
|
||||
color: navigationBarTheme.indicatorColor ?? defaults.indicatorColor!,
|
||||
shape: navigationBarTheme.indicatorShape ?? defaults.indicatorShape!
|
||||
),
|
||||
_StatusTransitionWidgetBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return _isForwardOrCompleted(animation)
|
||||
? selectedIconWidget
|
||||
: unselectedIconWidget;
|
||||
? selectedIconWidget
|
||||
: unselectedIconWidget;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
buildLabel: (BuildContext context) {
|
||||
final TextStyle? defaultTextStyle = theme.textTheme.overline?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
);
|
||||
final TextStyle? effectiveSelectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(<MaterialState>{MaterialState.selected}) ?? defaultTextStyle;
|
||||
final TextStyle? effectiveUnselectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(<MaterialState>{}) ?? defaultTextStyle;
|
||||
final TextStyle? effectiveSelectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(selectedState)
|
||||
?? defaults.labelTextStyle!.resolve(selectedState);
|
||||
final TextStyle? effectiveUnselectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(unselectedState)
|
||||
?? defaults.labelTextStyle!.resolve(unselectedState);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _ClampTextScaleFactor(
|
||||
@ -525,6 +533,7 @@ class NavigationIndicator extends StatelessWidget {
|
||||
this.width = 64,
|
||||
this.height = 32,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(16)),
|
||||
this.shape,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Determines the scale of the indicator.
|
||||
@ -538,25 +547,33 @@ class NavigationIndicator extends StatelessWidget {
|
||||
/// If null, defaults to [ColorScheme.secondary].
|
||||
final Color? color;
|
||||
|
||||
/// The width of the container that holds in the indicator.
|
||||
/// The width of this indicator.
|
||||
///
|
||||
/// Defaults to `64`.
|
||||
final double width;
|
||||
|
||||
/// The height of the container that holds in the indicator.
|
||||
/// The height of this indicator.
|
||||
///
|
||||
/// Defaults to `32`.
|
||||
final double height;
|
||||
|
||||
/// The radius of the container that holds in the indicator.
|
||||
/// The border radius of the shape of the indicator.
|
||||
///
|
||||
/// This is used to create a [RoundedRectangleBorder] shape for the indicator.
|
||||
/// This is ignored if [shape] is non-null.
|
||||
///
|
||||
/// Defaults to `BorderRadius.circular(16)`.
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
/// The shape of the indicator.
|
||||
///
|
||||
/// If non-null this is used as the shape used to draw the background
|
||||
/// of the indicator. If null then a [RoundedRectangleBorder] with the
|
||||
/// [borderRadius] is used.
|
||||
final ShapeBorder? shape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
@ -594,9 +611,9 @@ class NavigationIndicator extends StatelessWidget {
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
color: color ?? colorScheme.secondary.withOpacity(.24),
|
||||
decoration: ShapeDecoration(
|
||||
shape: shape ?? RoundedRectangleBorder(borderRadius: borderRadius),
|
||||
color: color ?? Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -1171,3 +1188,90 @@ bool _isForwardOrCompleted(Animation<double> animation) {
|
||||
return animation.status == AnimationStatus.forward
|
||||
|| animation.status == AnimationStatus.completed;
|
||||
}
|
||||
|
||||
NavigationBarThemeData _defaultsFor(BuildContext context) {
|
||||
return Theme.of(context).useMaterial3 ? _TokenDefaultsM3(context) : _Defaults(context);
|
||||
}
|
||||
|
||||
class _Defaults extends NavigationBarThemeData {
|
||||
_Defaults(BuildContext context)
|
||||
: _theme = Theme.of(context),
|
||||
_colors = Theme.of(context).colorScheme,
|
||||
super(
|
||||
height: 80.0,
|
||||
elevation: 0.0,
|
||||
indicatorShape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
);
|
||||
|
||||
final ThemeData _theme;
|
||||
final ColorScheme _colors;
|
||||
|
||||
// With Material 3, the NavigationBar uses an overlay blend for the
|
||||
// default color regardless of light/dark mode.
|
||||
@override Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.surface, _colors.onSurface, 3.0);
|
||||
|
||||
@override MaterialStateProperty<IconThemeData?>? get iconTheme {
|
||||
return MaterialStateProperty.all(IconThemeData(
|
||||
size: 24,
|
||||
color: _colors.onSurface,
|
||||
));
|
||||
}
|
||||
|
||||
@override Color? get indicatorColor => _colors.secondary.withOpacity(0.24);
|
||||
|
||||
@override MaterialStateProperty<TextStyle?>? get labelTextStyle => MaterialStateProperty.all(_theme.textTheme.overline!.copyWith(color: _colors.onSurface));
|
||||
}
|
||||
|
||||
// BEGIN GENERATED TOKEN PROPERTIES
|
||||
|
||||
// Generated code to the end of this file. Do not edit by hand.
|
||||
// These defaults are generated from the Material Design Token
|
||||
// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// Generated version v0_81
|
||||
class _TokenDefaultsM3 extends NavigationBarThemeData {
|
||||
_TokenDefaultsM3(BuildContext context)
|
||||
: _theme = Theme.of(context),
|
||||
_colors = Theme.of(context).colorScheme,
|
||||
super(
|
||||
height: 80.0,
|
||||
elevation: 3.0,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
);
|
||||
|
||||
final ThemeData _theme;
|
||||
final ColorScheme _colors;
|
||||
|
||||
// With Material 3, the NavigationBar uses an overlay blend for the
|
||||
// default color regardless of light/dark mode. This should be handled
|
||||
// in the Material widget based off of elevation, but for now we will do
|
||||
// it here in the defaults.
|
||||
@override Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.surface, _colors.primary, 3.0);
|
||||
|
||||
@override MaterialStateProperty<IconThemeData?>? get iconTheme {
|
||||
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
return IconThemeData(
|
||||
size: 24.0,
|
||||
color: states.contains(MaterialState.selected)
|
||||
? _colors.onSecondaryContainer
|
||||
: _colors.onSurfaceVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override Color? get indicatorColor => _colors.secondaryContainer;
|
||||
@override ShapeBorder? get indicatorShape => const StadiumBorder();
|
||||
|
||||
@override MaterialStateProperty<TextStyle?>? get labelTextStyle {
|
||||
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
final TextStyle style = _theme.textTheme.labelMedium!;
|
||||
return style.apply(color: states.contains(MaterialState.selected)
|
||||
? _colors.onSurface
|
||||
: _colors.onSurfaceVariant
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES
|
||||
|
@ -41,7 +41,9 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
const NavigationBarThemeData({
|
||||
this.height,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.indicatorColor,
|
||||
this.indicatorShape,
|
||||
this.labelTextStyle,
|
||||
this.iconTheme,
|
||||
this.labelBehavior,
|
||||
@ -53,9 +55,15 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
/// Overrides the default value of [NavigationBar.backgroundColor].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Overrides the default value of [NavigationBar.elevation].
|
||||
final double? elevation;
|
||||
|
||||
/// Overrides the default value of [NavigationBar]'s selection indicator.
|
||||
final Color? indicatorColor;
|
||||
|
||||
/// Overrides the default shape of the [NavigationBar]'s selection indicator.
|
||||
final ShapeBorder? indicatorShape;
|
||||
|
||||
/// The style to merge with the default text style for
|
||||
/// [NavigationDestination] labels.
|
||||
///
|
||||
@ -77,7 +85,9 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
NavigationBarThemeData copyWith({
|
||||
double? height,
|
||||
Color? backgroundColor,
|
||||
double? elevation,
|
||||
Color? indicatorColor,
|
||||
ShapeBorder? indicatorShape,
|
||||
MaterialStateProperty<TextStyle?>? labelTextStyle,
|
||||
MaterialStateProperty<IconThemeData?>? iconTheme,
|
||||
NavigationDestinationLabelBehavior? labelBehavior,
|
||||
@ -85,7 +95,9 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
return NavigationBarThemeData(
|
||||
height: height ?? this.height,
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
elevation: elevation ?? this.elevation,
|
||||
indicatorColor: indicatorColor ?? this.indicatorColor,
|
||||
indicatorShape: indicatorShape ?? this.indicatorShape,
|
||||
labelTextStyle: labelTextStyle ?? this.labelTextStyle,
|
||||
iconTheme: iconTheme ?? this.iconTheme,
|
||||
labelBehavior: labelBehavior ?? this.labelBehavior,
|
||||
@ -104,7 +116,9 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
return NavigationBarThemeData(
|
||||
height: lerpDouble(a?.height, b?.height, t),
|
||||
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
|
||||
elevation: lerpDouble(a?.elevation, b?.elevation, t),
|
||||
indicatorColor: Color.lerp(a?.indicatorColor, b?.indicatorColor, t),
|
||||
indicatorShape: ShapeBorder.lerp(a?.indicatorShape, b?.indicatorShape, t),
|
||||
labelTextStyle: _lerpProperties<TextStyle?>(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp),
|
||||
iconTheme: _lerpProperties<IconThemeData?>(a?.iconTheme, b?.iconTheme, t, IconThemeData.lerp),
|
||||
labelBehavior: t < 0.5 ? a?.labelBehavior : b?.labelBehavior,
|
||||
@ -116,7 +130,9 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
return hashValues(
|
||||
height,
|
||||
backgroundColor,
|
||||
elevation,
|
||||
indicatorColor,
|
||||
indicatorShape,
|
||||
labelTextStyle,
|
||||
iconTheme,
|
||||
labelBehavior,
|
||||
@ -132,7 +148,9 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
return other is NavigationBarThemeData
|
||||
&& other.height == height
|
||||
&& other.backgroundColor == backgroundColor
|
||||
&& other.elevation == elevation
|
||||
&& other.indicatorColor == indicatorColor
|
||||
&& other.indicatorShape == indicatorShape
|
||||
&& other.labelTextStyle == labelTextStyle
|
||||
&& other.iconTheme == iconTheme
|
||||
&& other.labelBehavior == labelBehavior;
|
||||
@ -143,7 +161,9 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DoubleProperty('height', height, defaultValue: null));
|
||||
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
|
||||
properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
|
||||
properties.add(ColorProperty('indicatorColor', indicatorColor, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<ShapeBorder>('indicatorShape', indicatorShape, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>('labelTextStyle', labelTextStyle, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<MaterialStateProperty<IconThemeData?>>('iconTheme', iconTheme, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<NavigationDestinationLabelBehavior>('labelBehavior', labelBehavior, defaultValue: null));
|
||||
|
@ -1150,6 +1150,7 @@ class ThemeData with Diagnosticable {
|
||||
/// Components that have been migrated to Material 3 are:
|
||||
///
|
||||
/// * [FloatingActionButton]
|
||||
/// * [NavigationBar]
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
@ -63,6 +63,31 @@ void main() {
|
||||
expect(_getMaterial(tester).color, equals(color));
|
||||
});
|
||||
|
||||
testWidgets('NavigationBar can update elevation', (WidgetTester tester) async {
|
||||
const double elevation = 42.0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildWidget(
|
||||
NavigationBar(
|
||||
elevation: elevation,
|
||||
destinations: const <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.ac_unit),
|
||||
label: 'AC',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.access_alarm),
|
||||
label: 'Alarm',
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (int i) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(_getMaterial(tester).elevation, equals(elevation));
|
||||
});
|
||||
|
||||
testWidgets('NavigationBar adds bottom padding to height', (WidgetTester tester) async {
|
||||
const double bottomPadding = 40.0;
|
||||
|
||||
@ -112,6 +137,61 @@ void main() {
|
||||
expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight);
|
||||
});
|
||||
|
||||
testWidgets('NavigationBar uses proper defaults when no parameters are given', (WidgetTester tester) async {
|
||||
// Pre-M3 settings that were hand coded.
|
||||
await tester.pumpWidget(
|
||||
_buildWidget(
|
||||
NavigationBar(
|
||||
destinations: const <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.ac_unit),
|
||||
label: 'AC',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.access_alarm),
|
||||
label: 'Alarm',
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (int i) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(_getMaterial(tester).color, const Color(0xffeaeaea));
|
||||
expect(_getMaterial(tester).elevation, 0);
|
||||
expect(tester.getSize(find.byType(NavigationBar)).height, 80);
|
||||
expect(_indicator(tester)?.color, const Color(0x3d2196f3));
|
||||
expect(_indicator(tester)?.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)));
|
||||
|
||||
// M3 settings from the token database.
|
||||
await tester.pumpWidget(
|
||||
_buildWidget(
|
||||
Theme(
|
||||
data: ThemeData.light().copyWith(useMaterial3: true),
|
||||
child: NavigationBar(
|
||||
destinations: const <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.ac_unit),
|
||||
label: 'AC',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.access_alarm),
|
||||
label: 'Alarm',
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (int i) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(_getMaterial(tester).color, const Color(0xffecf6fe));
|
||||
expect(_getMaterial(tester).elevation, 3);
|
||||
expect(tester.getSize(find.byType(NavigationBar)).height, 80);
|
||||
expect(_indicator(tester)?.color, const Color(0xff2196f3));
|
||||
expect(_indicator(tester)?.shape, const StadiumBorder());
|
||||
});
|
||||
|
||||
testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async {
|
||||
const String label = 'A';
|
||||
|
||||
@ -390,3 +470,12 @@ Material _getMaterial(WidgetTester tester) {
|
||||
find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)),
|
||||
);
|
||||
}
|
||||
|
||||
ShapeDecoration? _indicator(WidgetTester tester) {
|
||||
return tester.firstWidget<Container>(
|
||||
find.descendant(
|
||||
of: find.byType(FadeTransition),
|
||||
matching: find.byType(Container),
|
||||
),
|
||||
).decoration as ShapeDecoration?;
|
||||
}
|
||||
|
@ -29,7 +29,9 @@ void main() {
|
||||
NavigationBarThemeData(
|
||||
height: 200.0,
|
||||
backgroundColor: const Color(0x00000099),
|
||||
elevation: 20.0,
|
||||
indicatorColor: const Color(0x00000098),
|
||||
indicatorShape: const CircleBorder(),
|
||||
labelTextStyle: MaterialStateProperty.all(const TextStyle(fontSize: 7.0)),
|
||||
iconTheme: MaterialStateProperty.all(const IconThemeData(color: Color(0x00000097))),
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
||||
@ -42,20 +44,24 @@ void main() {
|
||||
|
||||
expect(description[0], 'height: 200.0');
|
||||
expect(description[1], 'backgroundColor: Color(0x00000099)');
|
||||
expect(description[2], 'indicatorColor: Color(0x00000098)');
|
||||
expect(description[3], 'labelTextStyle: MaterialStateProperty.all(TextStyle(inherit: true, size: 7.0))');
|
||||
expect(description[2], 'elevation: 20.0');
|
||||
expect(description[3], 'indicatorColor: Color(0x00000098)');
|
||||
expect(description[4], 'indicatorShape: CircleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none))');
|
||||
expect(description[5], 'labelTextStyle: MaterialStateProperty.all(TextStyle(inherit: true, size: 7.0))');
|
||||
|
||||
// Ignore instance address for IconThemeData.
|
||||
expect(description[4].contains('iconTheme: MaterialStateProperty.all(IconThemeData'), isTrue);
|
||||
expect(description[4].contains('(color: Color(0x00000097))'), isTrue);
|
||||
expect(description[6].contains('iconTheme: MaterialStateProperty.all(IconThemeData'), isTrue);
|
||||
expect(description[6].contains('(color: Color(0x00000097))'), isTrue);
|
||||
|
||||
expect(description[5], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide');
|
||||
expect(description[7], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide');
|
||||
});
|
||||
|
||||
testWidgets('NavigationBarThemeData values are used when no NavigationBar properties are specified', (WidgetTester tester) async {
|
||||
const double height = 200.0;
|
||||
const Color backgroundColor = Color(0x00000001);
|
||||
const double elevation = 42.0;
|
||||
const Color indicatorColor = Color(0x00000002);
|
||||
const ShapeBorder indicatorShape = CircleBorder();
|
||||
const double selectedIconSize = 25.0;
|
||||
const double unselectedIconSize = 23.0;
|
||||
const Color selectedIconColor = Color(0x00000003);
|
||||
@ -73,7 +79,9 @@ void main() {
|
||||
data: NavigationBarThemeData(
|
||||
height: height,
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorShape: indicatorShape,
|
||||
iconTheme: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return const IconThemeData(
|
||||
@ -106,7 +114,9 @@ void main() {
|
||||
|
||||
expect(_barHeight(tester), height);
|
||||
expect(_barMaterial(tester).color, backgroundColor);
|
||||
expect(_barMaterial(tester).elevation, elevation);
|
||||
expect(_indicator(tester)?.color, indicatorColor);
|
||||
expect(_indicator(tester)?.shape, indicatorShape);
|
||||
expect(_selectedIconTheme(tester).size, selectedIconSize);
|
||||
expect(_selectedIconTheme(tester).color, selectedIconColor);
|
||||
expect(_selectedIconTheme(tester).opacity, selectedIconOpacity);
|
||||
@ -121,6 +131,7 @@ void main() {
|
||||
testWidgets('NavigationBar values take priority over NavigationBarThemeData values when both properties are specified', (WidgetTester tester) async {
|
||||
const double height = 200.0;
|
||||
const Color backgroundColor = Color(0x00000001);
|
||||
const double elevation = 42.0;
|
||||
const NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
|
||||
|
||||
await tester.pumpWidget(
|
||||
@ -129,11 +140,13 @@ void main() {
|
||||
bottomNavigationBar: NavigationBarTheme(
|
||||
data: const NavigationBarThemeData(
|
||||
height: 100.0,
|
||||
elevation: 18.0,
|
||||
backgroundColor: Color(0x00000099),
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
||||
),
|
||||
child: NavigationBar(
|
||||
height: height,
|
||||
elevation: elevation,
|
||||
backgroundColor: backgroundColor,
|
||||
labelBehavior: labelBehavior,
|
||||
destinations: _destinations(),
|
||||
@ -145,6 +158,7 @@ void main() {
|
||||
|
||||
expect(_barHeight(tester), height);
|
||||
expect(_barMaterial(tester).color, backgroundColor);
|
||||
expect(_barMaterial(tester).elevation, elevation);
|
||||
expect(_labelBehavior(tester), labelBehavior);
|
||||
});
|
||||
}
|
||||
@ -179,13 +193,13 @@ Material _barMaterial(WidgetTester tester) {
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration? _indicator(WidgetTester tester) {
|
||||
ShapeDecoration? _indicator(WidgetTester tester) {
|
||||
return tester.firstWidget<Container>(
|
||||
find.descendant(
|
||||
of: find.byType(FadeTransition),
|
||||
matching: find.byType(Container),
|
||||
),
|
||||
).decoration as BoxDecoration?;
|
||||
).decoration as ShapeDecoration?;
|
||||
}
|
||||
|
||||
IconThemeData _selectedIconTheme(WidgetTester tester) {
|
||||
|
@ -263,13 +263,13 @@ Material _railMaterial(WidgetTester tester) {
|
||||
}
|
||||
|
||||
|
||||
BoxDecoration? _indicatorDecoration(WidgetTester tester) {
|
||||
ShapeDecoration? _indicatorDecoration(WidgetTester tester) {
|
||||
return tester.firstWidget<Container>(
|
||||
find.descendant(
|
||||
of: find.byType(NavigationIndicator),
|
||||
matching: find.byType(Container),
|
||||
),
|
||||
).decoration as BoxDecoration?;
|
||||
).decoration as ShapeDecoration?;
|
||||
}
|
||||
|
||||
IconThemeData _selectedIconTheme(WidgetTester tester) {
|
||||
|
Loading…
Reference in New Issue
Block a user