add navigation bar component (#83047)

This commit is contained in:
MH Johnson 2021-09-26 11:03:04 -04:00 committed by GitHub
parent 61bce1d83b
commit f3049c7762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 2064 additions and 0 deletions

View File

@ -99,6 +99,8 @@ export 'src/material/material_localizations.dart';
export 'src/material/material_state.dart';
export 'src/material/material_state_mixin.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/navigation_bar.dart';
export 'src/material/navigation_bar_theme.dart';
export 'src/material/navigation_rail.dart';
export 'src/material/navigation_rail_theme.dart';
export 'src/material/no_splash.dart';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,228 @@
// 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 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'material_state.dart';
import 'navigation_bar.dart';
import 'theme.dart';
/// Defines default property values for descendant [NavigationBar]
/// widgets.
///
/// Descendant widgets obtain the current [NavigationBarThemeData] object
/// using `NavigationBarTheme.of(context)`. Instances of
/// [NavigationBarThemeData] can be customized with
/// [NavigationBarThemeData.copyWith].
///
/// Typically a [NavigationBarThemeData] is specified as part of the
/// overall [Theme] with [ThemeData.navigationBarTheme]. Alternatively, a
/// [NavigationBarTheme] inherited widget can be used to theme [NavigationBar]s
/// in a subtree of widgets.
///
/// All [NavigationBarThemeData] properties are `null` by default.
/// When null, the [NavigationBar] will provide its own defaults based on the
/// overall [Theme]'s textTheme and colorScheme. See the individual
/// [NavigationBar] properties for details.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
@immutable
class NavigationBarThemeData with Diagnosticable {
/// Creates a theme that can be used for [ThemeData.navigationBarTheme] and
/// [NavigationBarTheme].
const NavigationBarThemeData({
this.height,
this.backgroundColor,
this.indicatorColor,
this.labelTextStyle,
this.iconTheme,
this.labelBehavior,
});
/// Overrides the default value of [NavigationBar.height].
final double? height;
/// Overrides the default value of [NavigationBar.backgroundColor].
final Color? backgroundColor;
/// Overrides the default value of [NavigationBar]'s selection indicator.
final Color? indicatorColor;
/// The style to merge with the default text style for
/// [NavigationDestination] labels.
///
/// You can use this to specify a different style when the label is selected.
final MaterialStateProperty<TextStyle?>? labelTextStyle;
/// The theme to merge with the default icon theme for
/// [NavigationDestination] icons.
///
/// You can use this to specify a different icon theme when the icon is
/// selected.
final MaterialStateProperty<IconThemeData?>? iconTheme;
/// Overrides the default value of [NavigationBar.labelBehavior].
final NavigationDestinationLabelBehavior? labelBehavior;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
NavigationBarThemeData copyWith({
double? height,
Color? backgroundColor,
Color? indicatorColor,
MaterialStateProperty<TextStyle?>? labelTextStyle,
MaterialStateProperty<IconThemeData?>? iconTheme,
NavigationDestinationLabelBehavior? labelBehavior,
}) {
return NavigationBarThemeData(
height: height ?? this.height,
backgroundColor: backgroundColor ?? this.backgroundColor,
indicatorColor: indicatorColor ?? this.indicatorColor,
labelTextStyle: labelTextStyle ?? this.labelTextStyle,
iconTheme: iconTheme ?? this.iconTheme,
labelBehavior: labelBehavior ?? this.labelBehavior,
);
}
/// Linearly interpolate between two navigation rail themes.
///
/// If both arguments are null then null is returned.
///
/// {@macro dart.ui.shadow.lerp}
static NavigationBarThemeData? lerp(NavigationBarThemeData? a, NavigationBarThemeData? b, double t) {
assert(t != null);
if (a == null && b == null)
return null;
return NavigationBarThemeData(
height: lerpDouble(a?.height, b?.height, t),
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
indicatorColor: Color.lerp(a?.indicatorColor, b?.indicatorColor, 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,
);
}
@override
int get hashCode {
return hashValues(
height,
backgroundColor,
indicatorColor,
labelTextStyle,
iconTheme,
labelBehavior,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is NavigationBarThemeData
&& other.height == height
&& other.backgroundColor == backgroundColor
&& other.indicatorColor == indicatorColor
&& other.labelTextStyle == labelTextStyle
&& other.iconTheme == iconTheme
&& other.labelBehavior == labelBehavior;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('height', height, defaultValue: null));
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
properties.add(ColorProperty('indicatorColor', indicatorColor, 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));
}
static MaterialStateProperty<T>? _lerpProperties<T>(
MaterialStateProperty<T>? a,
MaterialStateProperty<T>? b,
double t,
T Function(T?, T?, double) lerpFunction,
) {
// Avoid creating a _LerpProperties object for a common case.
if (a == null && b == null)
return null;
return _LerpProperties<T>(a, b, t, lerpFunction);
}
}
class _LerpProperties<T> implements MaterialStateProperty<T> {
const _LerpProperties(this.a, this.b, this.t, this.lerpFunction);
final MaterialStateProperty<T>? a;
final MaterialStateProperty<T>? b;
final double t;
final T Function(T?, T?, double) lerpFunction;
@override
T resolve(Set<MaterialState> states) {
final T? resolvedA = a?.resolve(states);
final T? resolvedB = b?.resolve(states);
return lerpFunction(resolvedA, resolvedB, t);
}
}
/// An inherited widget that defines visual properties for [NavigationBar]s and
/// [NavigationDestination]s in this widget's subtree.
///
/// Values specified here are used for [NavigationBar] properties that are not
/// given an explicit non-null value.
///
/// See also:
///
/// * [ThemeData.navigationBarTheme], which describes the
/// [NavigationBarThemeData] in the overall theme for the application.
class NavigationBarTheme extends InheritedTheme {
/// Creates a navigation rail theme that controls the
/// [NavigationBarThemeData] properties for a [NavigationBar].
///
/// The data argument must not be null.
const NavigationBarTheme({
Key? key,
required this.data,
required Widget child,
}) : assert(data != null), super(key: key, child: child);
/// Specifies the background color, label text style, icon theme, and label
/// type values for descendant [NavigationBar] widgets.
final NavigationBarThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [NavigationBarTheme] widget, then
/// [ThemeData.navigationBarTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// NavigationBarTheme theme = NavigationBarTheme.of(context);
/// ```
static NavigationBarThemeData of(BuildContext context) {
final NavigationBarTheme? navigationBarTheme = context.dependOnInheritedWidgetOfExactType<NavigationBarTheme>();
return navigationBarTheme?.data ?? Theme.of(context).navigationBarTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return NavigationBarTheme(data: data, child: child);
}
@override
bool updateShouldNotify(NavigationBarTheme oldWidget) => data != oldWidget.data;
}

View File

@ -28,6 +28,7 @@ import 'floating_action_button_theme.dart';
import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart';
import 'navigation_bar_theme.dart';
import 'navigation_rail_theme.dart';
import 'outlined_button_theme.dart';
import 'page_transitions_theme.dart';
@ -317,6 +318,7 @@ class ThemeData with Diagnosticable {
ColorScheme? colorScheme,
DialogTheme? dialogTheme,
FloatingActionButtonThemeData? floatingActionButtonTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme,
Typography? typography,
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
@ -462,6 +464,7 @@ class ThemeData with Diagnosticable {
);
dialogTheme ??= const DialogTheme();
floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
navigationBarTheme ??= const NavigationBarThemeData();
navigationRailTheme ??= const NavigationRailThemeData();
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
snackBarTheme ??= const SnackBarThemeData();
@ -543,6 +546,7 @@ class ThemeData with Diagnosticable {
colorScheme: colorScheme,
dialogTheme: dialogTheme,
floatingActionButtonTheme: floatingActionButtonTheme,
navigationBarTheme: navigationBarTheme,
navigationRailTheme: navigationRailTheme,
typography: typography,
cupertinoOverrideTheme: cupertinoOverrideTheme,
@ -677,6 +681,7 @@ class ThemeData with Diagnosticable {
required this.colorScheme,
required this.dialogTheme,
required this.floatingActionButtonTheme,
required this.navigationBarTheme,
required this.navigationRailTheme,
required this.typography,
required this.cupertinoOverrideTheme,
@ -763,6 +768,7 @@ class ThemeData with Diagnosticable {
assert(colorScheme != null),
assert(dialogTheme != null),
assert(floatingActionButtonTheme != null),
assert(navigationBarTheme != null),
assert(navigationRailTheme != null),
assert(typography != null),
assert(snackBarTheme != null),
@ -1269,6 +1275,10 @@ class ThemeData with Diagnosticable {
/// [FloatingActionButton].
final FloatingActionButtonThemeData floatingActionButtonTheme;
/// A theme for customizing the background color, text style, and icon themes
/// of a [NavigationBar].
final NavigationBarThemeData navigationBarTheme;
/// A theme for customizing the background color, elevation, text style, and
/// icon themes of a [NavigationRail].
final NavigationRailThemeData navigationRailTheme;
@ -1485,6 +1495,7 @@ class ThemeData with Diagnosticable {
ColorScheme? colorScheme,
DialogTheme? dialogTheme,
FloatingActionButtonThemeData? floatingActionButtonTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme,
Typography? typography,
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
@ -1576,6 +1587,7 @@ class ThemeData with Diagnosticable {
colorScheme: (colorScheme ?? this.colorScheme).copyWith(brightness: brightness),
dialogTheme: dialogTheme ?? this.dialogTheme,
floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
typography: typography ?? this.typography,
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
@ -1737,6 +1749,7 @@ class ThemeData with Diagnosticable {
colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t),
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!,
navigationBarTheme: NavigationBarThemeData.lerp(a.navigationBarTheme, b.navigationBarTheme, t)!,
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!,
typography: Typography.lerp(a.typography, b.typography, t),
cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
@ -1826,6 +1839,7 @@ class ThemeData with Diagnosticable {
&& other.colorScheme == colorScheme
&& other.dialogTheme == dialogTheme
&& other.floatingActionButtonTheme == floatingActionButtonTheme
&& other.navigationBarTheme == navigationBarTheme
&& other.navigationRailTheme == navigationRailTheme
&& other.typography == typography
&& other.cupertinoOverrideTheme == cupertinoOverrideTheme
@ -1914,6 +1928,7 @@ class ThemeData with Diagnosticable {
colorScheme,
dialogTheme,
floatingActionButtonTheme,
navigationBarTheme,
navigationRailTheme,
typography,
cupertinoOverrideTheme,
@ -1999,6 +2014,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonThemeData', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationBarThemeData>('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailThemeData', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NoDefaultCupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, level: DiagnosticLevel.debug));

View File

@ -0,0 +1,396 @@
// 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 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Navigation bar updates destinations when tapped', (WidgetTester tester) async {
int mutatedIndex = -1;
final Widget widget = _buildWidget(
NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {
mutatedIndex = i;
},
),
);
await tester.pumpWidget(widget);
expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget);
await tester.tap(find.text('Alarm'));
expect(mutatedIndex, 1);
await tester.tap(find.text('AC'));
expect(mutatedIndex, 0);
});
testWidgets('NavigationBar can update background color', (WidgetTester tester) async {
const Color color = Colors.yellow;
await tester.pumpWidget(
_buildWidget(
NavigationBar(
backgroundColor: color,
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, equals(color));
});
testWidgets('NavigationBar adds bottom padding to height', (WidgetTester tester) async {
const double bottomPadding = 40.0;
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) {},
),
),
);
final double defaultSize = tester.getSize(find.byType(NavigationBar)).height;
expect(defaultSize, 80);
await tester.pumpWidget(
_buildWidget(
MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)),
child: NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
),
);
final double expectedHeight = defaultSize + bottomPadding;
expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight);
});
testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async {
const String label = 'A';
Widget buildApp({ required double textScaleFactor }) {
return MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
bottomNavigationBar: NavigationBar(
destinations: const <NavigationDestination>[
NavigationDestination(
label: label,
icon: Icon(Icons.ac_unit),
tooltip: label,
),
NavigationDestination(
label: 'B',
icon: Icon(Icons.battery_alert),
),
],
),
);
},
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp(textScaleFactor: 1.0));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(find.text(label), findsNWidgets(2));
// The default size of a tooltip with the text A.
const Size defaultTooltipSize = Size(14.0, 14.0);
expect(tester.getSize(find.text(label).last), defaultTooltipSize);
// The duration is needed to ensure the tooltip disappears.
await tester.pumpAndSettle(const Duration(seconds: 2));
await tester.pumpWidget(buildApp(textScaleFactor: 4.0));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(tester.getSize(find.text(label).last), Size(defaultTooltipSize.width * 4, defaultTooltipSize.height * 4));
});
testWidgets('Custom tooltips in NavigationBarDestination', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBar(
destinations: const <NavigationDestination>[
NavigationDestination(
label: 'A',
tooltip: 'A tooltip',
icon: Icon(Icons.ac_unit),
),
NavigationDestination(
label: 'B',
icon: Icon(Icons.battery_alert),
),
NavigationDestination(
label: 'C',
icon: Icon(Icons.cake),
tooltip: '',
),
],
),
),
),
);
expect(find.text('A'), findsOneWidget);
await tester.longPress(find.text('A'));
expect(find.byTooltip('A tooltip'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
await tester.longPress(find.text('B'));
expect(find.byTooltip('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
await tester.longPress(find.text('C'));
expect(find.byTooltip('C'), findsNothing);
});
testWidgets('Navigation bar semantics', (WidgetTester tester) async {
Widget _widget({int selectedIndex = 0}) {
return _buildWidget(
NavigationBar(
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
);
}
await tester.pumpWidget(_widget(selectedIndex: 0));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: false,
hasTapAction: true,
),
);
await tester.pumpWidget(_widget(selectedIndex: 1));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: false,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
});
testWidgets('Navigation bar semantics with some labels hidden', (WidgetTester tester) async {
Widget _widget({int selectedIndex = 0}) {
return _buildWidget(
NavigationBar(
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
);
}
await tester.pumpWidget(_widget(selectedIndex: 0));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: false,
hasTapAction: true,
),
);
await tester.pumpWidget(_widget(selectedIndex: 1));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: false,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
});
testWidgets('Navigation bar does not grow with text scale factor', (WidgetTester tester) async {
const int _animationMilliseconds = 800;
Widget _widget({double textScaleFactor = 1}) {
return _buildWidget(
MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: NavigationBar(
animationDuration: const Duration(milliseconds: _animationMilliseconds),
destinations: const <NavigationDestination>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
),
);
}
await tester.pumpWidget(_widget());
final double initialHeight = tester.getSize(find.byType(NavigationBar)).height;
await tester.pumpWidget(_widget(textScaleFactor: 2));
final double newHeight = tester.getSize(find.byType(NavigationBar)).height;
expect(newHeight, equals(initialHeight));
});
}
Widget _buildWidget(Widget child) {
return MaterialApp(
theme: ThemeData.light(),
home: Scaffold(
bottomNavigationBar: Center(
child: child,
),
),
);
}
Material _getMaterial(WidgetTester tester) {
return tester.firstWidget<Material>(
find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)),
);
}

View File

@ -0,0 +1,254 @@
// 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 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('copyWith, ==, hashCode basics', () {
expect(const NavigationBarThemeData(), const NavigationBarThemeData().copyWith());
expect(const NavigationBarThemeData().hashCode, const NavigationBarThemeData().copyWith().hashCode);
});
testWidgets('Default debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const NavigationBarThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('Custom debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
NavigationBarThemeData(
height: 200.0,
backgroundColor: const Color(0x00000099),
indicatorColor: const Color(0x00000098),
labelTextStyle: MaterialStateProperty.all(const TextStyle(fontSize: 7.0)),
iconTheme: MaterialStateProperty.all(const IconThemeData(color: Color(0x00000097))),
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
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))');
// Ignore instance address for IconThemeData.
expect(description[4].contains('iconTheme: MaterialStateProperty.all(IconThemeData'), isTrue);
expect(description[4].contains('(color: Color(0x00000097))'), isTrue);
expect(description[5], '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 Color indicatorColor = Color(0x00000002);
const double selectedIconSize = 25.0;
const double unselectedIconSize = 23.0;
const Color selectedIconColor = Color(0x00000003);
const Color unselectedIconColor = Color(0x00000004);
const double selectedIconOpacity = 0.99;
const double unselectedIconOpacity = 0.98;
const double selectedLabelFontSize = 13.0;
const double unselectedLabelFontSize = 11.0;
const NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBarTheme(
data: NavigationBarThemeData(
height: height,
backgroundColor: backgroundColor,
indicatorColor: indicatorColor,
iconTheme: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const IconThemeData(
size: selectedIconSize,
color: selectedIconColor,
opacity: selectedIconOpacity,
);
}
return const IconThemeData(
size: unselectedIconSize,
color: unselectedIconColor,
opacity: unselectedIconOpacity,
);
}),
labelTextStyle: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const TextStyle(fontSize: selectedLabelFontSize);
}
return const TextStyle(fontSize: unselectedLabelFontSize);
}),
labelBehavior: labelBehavior,
),
child: NavigationBar(
selectedIndex: 0,
destinations: _destinations(),
),
),
),
),
);
expect(_barHeight(tester), height);
expect(_barMaterial(tester).color, backgroundColor);
expect(_indicator(tester)?.color, indicatorColor);
expect(_selectedIconTheme(tester).size, selectedIconSize);
expect(_selectedIconTheme(tester).color, selectedIconColor);
expect(_selectedIconTheme(tester).opacity, selectedIconOpacity);
expect(_unselectedIconTheme(tester).size, unselectedIconSize);
expect(_unselectedIconTheme(tester).color, unselectedIconColor);
expect(_unselectedIconTheme(tester).opacity, unselectedIconOpacity);
expect(_selectedLabelStyle(tester).fontSize, selectedLabelFontSize);
expect(_unselectedLabelStyle(tester).fontSize, unselectedLabelFontSize);
expect(_labelBehavior(tester), labelBehavior);
});
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 NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBarTheme(
data: const NavigationBarThemeData(
height: 100.0,
backgroundColor: Color(0x00000099),
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
),
child: NavigationBar(
height: height,
backgroundColor: backgroundColor,
labelBehavior: labelBehavior,
selectedIndex: 0,
destinations: _destinations(),
),
),
),
),
);
expect(_barHeight(tester), height);
expect(_barMaterial(tester).color, backgroundColor);
expect(_labelBehavior(tester), labelBehavior);
});
}
List<NavigationDestination> _destinations() {
return const <NavigationDestination>[
NavigationDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: 'Abc',
),
NavigationDestination(
icon: Icon(Icons.star_border),
selectedIcon: Icon(Icons.star),
label: 'Def',
),
];
}
double _barHeight(WidgetTester tester) {
return tester.getRect(
find.byType(NavigationBar),
).height;
}
Material _barMaterial(WidgetTester tester) {
return tester.firstWidget<Material>(
find.descendant(
of: find.byType(NavigationBar),
matching: find.byType(Material),
),
);
}
BoxDecoration? _indicator(WidgetTester tester) {
return tester.firstWidget<Container>(
find.descendant(
of: find.byType(FadeTransition),
matching: find.byType(Container),
),
).decoration as BoxDecoration?;
}
IconThemeData _selectedIconTheme(WidgetTester tester) {
return _iconTheme(tester, Icons.favorite);
}
IconThemeData _unselectedIconTheme(WidgetTester tester) {
return _iconTheme(tester, Icons.star_border);
}
IconThemeData _iconTheme(WidgetTester tester, IconData icon) {
return tester.firstWidget<IconTheme>(
find.ancestor(
of: find.byIcon(icon),
matching: find.byType(IconTheme),
),
).data;
}
TextStyle _selectedLabelStyle(WidgetTester tester) {
return tester.widget<RichText>(
find.descendant(
of: find.text('Abc'),
matching: find.byType(RichText),
),
).text.style!;
}
TextStyle _unselectedLabelStyle(WidgetTester tester) {
return tester.widget<RichText>(
find.descendant(
of: find.text('Def'),
matching: find.byType(RichText),
),
).text.style!;
}
NavigationDestinationLabelBehavior _labelBehavior(WidgetTester tester) {
if (_opacityAboveLabel('Abc').evaluate().isNotEmpty && _opacityAboveLabel('Def').evaluate().isNotEmpty) {
return _labelOpacity(tester, 'Abc') == 1
? NavigationDestinationLabelBehavior.onlyShowSelected
: NavigationDestinationLabelBehavior.alwaysHide;
} else {
return NavigationDestinationLabelBehavior.alwaysShow;
}
}
Finder _opacityAboveLabel(String text) {
return find.ancestor(
of: find.text(text),
matching: find.byType(Opacity),
);
}
// Only valid when labelBehavior != alwaysShow.
double _labelOpacity(WidgetTester tester, String text) {
final Opacity opacityWidget = tester.widget<Opacity>(
find.ancestor(
of: find.text(text),
matching: find.byType(Opacity),
),
);
return opacityWidget.opacity;
}

View File

@ -324,6 +324,7 @@ void main() {
colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
typography: Typography.material2018(platform: TargetPlatform.android),
cupertinoOverrideTheme: null,
@ -420,6 +421,7 @@ void main() {
colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.white),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
typography: Typography.material2018(platform: TargetPlatform.iOS),
cupertinoOverrideTheme: ThemeData.light().cupertinoOverrideTheme,
@ -497,6 +499,7 @@ void main() {
colorScheme: otherTheme.colorScheme,
dialogTheme: otherTheme.dialogTheme,
floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
navigationBarTheme: otherTheme.navigationBarTheme,
navigationRailTheme: otherTheme.navigationRailTheme,
typography: otherTheme.typography,
cupertinoOverrideTheme: otherTheme.cupertinoOverrideTheme,
@ -571,6 +574,7 @@ void main() {
expect(themeDataCopy.colorScheme, equals(otherTheme.colorScheme));
expect(themeDataCopy.dialogTheme, equals(otherTheme.dialogTheme));
expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
expect(themeDataCopy.navigationBarTheme, equals(otherTheme.navigationBarTheme));
expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme));
expect(themeDataCopy.typography, equals(otherTheme.typography));
expect(themeDataCopy.cupertinoOverrideTheme, equals(otherTheme.cupertinoOverrideTheme));