add navigation bar component (#83047)
This commit is contained in:
parent
61bce1d83b
commit
f3049c7762
@ -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';
|
||||
|
1164
packages/flutter/lib/src/material/navigation_bar.dart
Normal file
1164
packages/flutter/lib/src/material/navigation_bar.dart
Normal file
File diff suppressed because it is too large
Load Diff
228
packages/flutter/lib/src/material/navigation_bar_theme.dart
Normal file
228
packages/flutter/lib/src/material/navigation_bar_theme.dart
Normal 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;
|
||||
}
|
@ -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));
|
||||
|
396
packages/flutter/test/material/navigation_bar_test.dart
Normal file
396
packages/flutter/test/material/navigation_bar_test.dart
Normal 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)),
|
||||
);
|
||||
}
|
254
packages/flutter/test/material/navigation_bar_theme_test.dart
Normal file
254
packages/flutter/test/material/navigation_bar_theme_test.dart
Normal 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;
|
||||
}
|
@ -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));
|
||||
|
Loading…
x
Reference in New Issue
Block a user