This commit is contained in:
hangyu 2022-11-18 15:10:05 -08:00 committed by GitHub
parent 01c1e8e587
commit 0e57147db1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1711 additions and 34 deletions

View file

@ -29,6 +29,7 @@ import 'package:gen_defaults/checkbox_template.dart';
import 'package:gen_defaults/color_scheme_template.dart';
import 'package:gen_defaults/dialog_template.dart';
import 'package:gen_defaults/divider_template.dart';
import 'package:gen_defaults/drawer_template.dart';
import 'package:gen_defaults/fab_template.dart';
import 'package:gen_defaults/filter_chip_template.dart';
import 'package:gen_defaults/icon_button_template.dart';
@ -36,6 +37,7 @@ import 'package:gen_defaults/input_chip_template.dart';
import 'package:gen_defaults/input_decorator_template.dart';
import 'package:gen_defaults/menu_template.dart';
import 'package:gen_defaults/navigation_bar_template.dart';
import 'package:gen_defaults/navigation_drawer_template.dart';
import 'package:gen_defaults/navigation_rail_template.dart';
import 'package:gen_defaults/popup_menu_template.dart';
import 'package:gen_defaults/progress_indicator_template.dart';
@ -144,6 +146,7 @@ Future<void> main(List<String> args) async {
DialogFullscreenTemplate('DialogFullscreen', '$materialLib/dialog.dart', tokens).updateFile();
DialogTemplate('Dialog', '$materialLib/dialog.dart', tokens).updateFile();
DividerTemplate('Divider', '$materialLib/divider.dart', tokens).updateFile();
DrawerTemplate('Drawer', '$materialLib/drawer.dart', tokens).updateFile();
FABTemplate('FAB', '$materialLib/floating_action_button.dart', tokens).updateFile();
FilterChipTemplate('ChoiceChip', '$materialLib/choice_chip.dart', tokens).updateFile();
FilterChipTemplate('FilterChip', '$materialLib/filter_chip.dart', tokens).updateFile();
@ -152,6 +155,7 @@ Future<void> main(List<String> args) async {
InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile();
MenuTemplate('Menu', '$materialLib/menu_anchor.dart', tokens).updateFile();
NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile();
NavigationDrawerTemplate('NavigationDrawer', '$materialLib/navigation_drawer.dart', tokens).updateFile();
NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile();
PopupMenuTemplate('PopupMenu', '$materialLib/popup_menu.dart', tokens).updateFile();
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();

View file

@ -0,0 +1,42 @@
// 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 DrawerTemplate extends TokenTemplate {
const DrawerTemplate(super.blockName, super.fileName, super.tokens);
@override
String generate() => '''
class _${blockName}DefaultsM3 extends DrawerThemeData {
const _${blockName}DefaultsM3(this.context)
: super(elevation: ${elevation("md.comp.navigation-drawer.modal.container")});
final BuildContext context;
@override
Color? get backgroundColor => ${componentColor("md.comp.navigation-drawer.container")};
@override
Color? get surfaceTintColor => ${colorOrTransparent("md.comp.navigation-drawer.container.surface-tint-layer.color")};
@override
Color? get shadowColor => ${colorOrTransparent("md.comp.navigation-drawer.container.shadow-color")};
// This don't appear to be tokens for this value, but it is
// shown in the spec.
@override
ShapeBorder? get shape => const RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(right: Radius.circular(16.0)),
);
// This don't appear to be tokens for this value, but it is
// shown in the spec.
@override
ShapeBorder? get endShape => const RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(left: Radius.circular(16.0)),
);
}
''';
}

View file

@ -0,0 +1,60 @@
// 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 NavigationDrawerTemplate extends TokenTemplate {
const NavigationDrawerTemplate(super.blockName, super.fileName, super.tokens);
@override
String generate() => '''
class _${blockName}DefaultsM3 extends NavigationDrawerThemeData {
const _${blockName}DefaultsM3(this.context)
: super(
elevation: ${elevation("md.comp.navigation-drawer.modal.container")},
tileHeight: ${tokens["md.comp.navigation-drawer.active-indicator.height"]},
indicatorShape: ${shape("md.comp.navigation-drawer.active-indicator")},
indicatorSize: const Size(${tokens["md.comp.navigation-drawer.active-indicator.width"]}, ${tokens["md.comp.navigation-drawer.active-indicator.height"]}),
);
final BuildContext context;
@override
Color? get backgroundColor => ${componentColor("md.comp.navigation-drawer.container")};
@override
Color? get surfaceTintColor => ${colorOrTransparent("md.comp.navigation-drawer.container.surface-tint-layer.color")};
@override
Color? get shadowColor => ${colorOrTransparent("md.comp.navigation-drawer.container.shadow-color")};
@override
Color? get indicatorColor => ${componentColor("md.comp.navigation-drawer.active-indicator")};
@override
MaterialStateProperty<IconThemeData?>? get iconTheme {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return IconThemeData(
size: ${tokens["md.comp.navigation-drawer.icon.size"]},
color: states.contains(MaterialState.selected)
? ${componentColor("md.comp.navigation-drawer.active.icon.")}
: ${componentColor("md.comp.navigation-drawer.inactive.icon")},
);
});
}
@override
MaterialStateProperty<TextStyle?>? get labelTextStyle {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
final TextStyle style = ${textStyle("md.comp.navigation-drawer.label-text")}!;
return style.apply(
color: states.contains(MaterialState.selected)
? ${componentColor("md.comp.navigation-drawer.active.label-text")}
: ${componentColor("md.comp.navigation-drawer.inactive.label-text")},
);
});
}
}
''';
}

View file

@ -0,0 +1,178 @@
// 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.
// Flutter code sample for [NavigationDrawer] .
// Builds an adaptive navigation widget layout. When the screen width is less than
// 450, A [NavigationBar] will be displayed. Otherwise, a [NavigationRail] will be
// displayed on the left side, and also a button to open the [NavigationDrawer].
// All of these navigation widgets are built from an indentical list of data.
import 'package:flutter/material.dart';
class ExampleDestination {
const ExampleDestination(this.label, this.icon, this.selectedIcon);
final String label;
final Widget icon;
final Widget selectedIcon;
}
const List<ExampleDestination> destinations = <ExampleDestination>[
ExampleDestination('page 0', Icon(Icons.widgets_outlined), Icon(Icons.widgets)),
ExampleDestination('page 1', Icon(Icons.format_paint_outlined), Icon(Icons.format_paint)),
ExampleDestination('page 2', Icon(Icons.text_snippet_outlined), Icon(Icons.text_snippet)),
ExampleDestination('page 3', Icon(Icons.invert_colors_on_outlined), Icon(Icons.opacity)),
];
void main() {
runApp(
MaterialApp(
title: 'NavigationDrawer Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true),
home: const NavigationDrawerExample(),
),
);
}
class NavigationDrawerExample extends StatefulWidget {
const NavigationDrawerExample({super.key});
@override
State<NavigationDrawerExample> createState() => _NavigationDrawerExampleState();
}
class _NavigationDrawerExampleState extends State<NavigationDrawerExample> {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
int screenIndex = 0;
late bool showNavigationDrawer;
void handleScreenChanged(int selectedScreen) {
setState(() {
screenIndex = selectedScreen;
});
}
void openDrawer() {
scaffoldKey.currentState!.openEndDrawer();
}
Widget buildBottomBarScaffold(){
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('Page Index = $screenIndex'),
],
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: screenIndex,
onDestinationSelected: (int index) {
setState(() {
screenIndex = index;
});
},
destinations: destinations
.map((ExampleDestination destination) {
return NavigationDestination(
label: destination.label,
icon: destination.icon,
selectedIcon: destination.selectedIcon,
tooltip: destination.label,
);
})
.toList(),
),
);
}
Widget buildDrawerScaffold(BuildContext context){
return Scaffold(
key: scaffoldKey,
body: SafeArea(
bottom: false,
top: false,
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: NavigationRail(
minWidth: 50,
destinations: destinations
.map((ExampleDestination destination) {
return NavigationRailDestination(
label: Text(destination.label),
icon: destination.icon,
selectedIcon: destination.selectedIcon,
);
})
.toList(),
selectedIndex: screenIndex,
useIndicator: true,
onDestinationSelected: (int index) {
setState(() {
screenIndex = index;
});
},
),
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('Page Index = $screenIndex'),
ElevatedButton(
onPressed: openDrawer,
child: const Text('Open Drawer'),
),
],
),
),
],
),
),
endDrawer: NavigationDrawer(
onDestinationSelected: handleScreenChanged,
selectedIndex: screenIndex,
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
child: Text(
'Header',
style: Theme.of(context).textTheme.titleSmall,
),
),
...destinations
.map((ExampleDestination destination) {
return NavigationDrawerDestination(
label: Text(destination.label),
icon: destination.icon,
selectedIcon: destination.selectedIcon,
);
}),
const Padding(
padding: EdgeInsets.fromLTRB(28, 16, 28, 10),
child: Divider(),
),
],
),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
showNavigationDrawer = MediaQuery.of(context).size.width >= 450;
}
@override
Widget build(BuildContext context) {
return showNavigationDrawer ? buildDrawerScaffold(context) : buildBottomBarScaffold();
}
}

View file

@ -124,6 +124,8 @@ export 'src/material/menu_theme.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/navigation_bar.dart';
export 'src/material/navigation_bar_theme.dart';
export 'src/material/navigation_drawer.dart';
export 'src/material/navigation_drawer_theme.dart';
export 'src/material/navigation_rail.dart';
export 'src/material/navigation_rail_theme.dart';
export 'src/material/no_splash.dart';

View file

@ -253,6 +253,8 @@ class Drawer extends StatelessWidget {
label = semanticLabel ?? MaterialLocalizations.of(context).drawerLabel;
}
final bool useMaterial3 = Theme.of(context).useMaterial3;
final bool isDrawerStart = DrawerController.maybeOf(context)?.alignment != DrawerAlignment.end;
final DrawerThemeData defaults= useMaterial3 ? _DrawerDefaultsM3(context): _DrawerDefaultsM2(context);
return Semantics(
scopesRoute: true,
namesRoute: true,
@ -261,11 +263,13 @@ class Drawer extends StatelessWidget {
child: ConstrainedBox(
constraints: BoxConstraints.expand(width: width ?? drawerTheme.width ?? _kWidth),
child: Material(
color: backgroundColor ?? drawerTheme.backgroundColor,
elevation: elevation ?? drawerTheme.elevation ?? 16.0,
shadowColor: shadowColor ?? drawerTheme.shadowColor ?? (useMaterial3 ? Colors.transparent : Theme.of(context).shadowColor),
surfaceTintColor: surfaceTintColor ?? drawerTheme.surfaceTintColor ?? (useMaterial3 ? Theme.of(context).colorScheme.surfaceTint : null),
shape: shape ?? drawerTheme.shape,
color: backgroundColor ?? drawerTheme.backgroundColor ?? defaults.backgroundColor,
elevation: elevation ?? drawerTheme.elevation ?? defaults.elevation!,
shadowColor: shadowColor ?? drawerTheme.shadowColor ?? defaults.shadowColor,
surfaceTintColor: surfaceTintColor ?? drawerTheme.surfaceTintColor ?? defaults.surfaceTintColor,
shape: shape ?? (isDrawerStart
? (drawerTheme.shape ?? defaults.shape)
: (drawerTheme.endShape ?? defaults.endShape)),
child: child,
),
),
@ -277,6 +281,20 @@ class Drawer extends StatelessWidget {
/// opened or closed.
typedef DrawerCallback = void Function(bool isOpened);
class _DrawerControllerScope extends InheritedWidget {
const _DrawerControllerScope({
required this.controller,
required super.child,
});
final DrawerController controller;
@override
bool updateShouldNotify(_DrawerControllerScope old) {
return controller != old.controller;
}
}
/// Provides interactive behavior for [Drawer] widgets.
///
/// Rarely used directly. Drawer controllers are typically created automatically
@ -379,6 +397,62 @@ class DrawerController extends StatefulWidget {
/// application was killed.
final bool isDrawerOpen;
/// The closest instance of [DrawerController] that encloses the given
/// context, or null if none is found.
///
/// {@tool snippet} Typical usage is as follows:
///
/// ```dart
/// DrawerController? controller = DrawerController.maybeOf(context);
/// ```
/// {@end-tool}
///
/// Calling this method will create a dependency on the closest
/// [DrawerController] in the [context], if there is one.
///
/// See also:
///
/// * [DrawerController.of], which is similar to this method, but asserts
/// if no [DrawerController] ancestor is found.
static DrawerController? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_DrawerControllerScope>()?.controller;
}
/// The closest instance of [DrawerController] that encloses the given
/// context.
///
/// If no instance is found, this method will assert in debug mode and throw
/// an exception in release mode.
///
/// Calling this method will create a dependency on the closest
/// [DrawerController] in the [context].
///
/// {@tool snippet} Typical usage is as follows:
///
/// ```dart
/// DrawerController controller = DrawerController.of(context);
/// ```
/// {@end-tool}
static DrawerController of(BuildContext context) {
final DrawerController? controller = maybeOf(context);
assert(() {
if (controller == null) {
throw FlutterError(
'DrawerController.of() was called with a context that does not '
'contain a DrawerController widget.\n'
'No DrawerController widget ancestor could be found starting from '
'the context that was passed to DrawerController.of(). This can '
'happen because you are using a widget that looks for a DrawerController '
'ancestor, but no such ancestor exists.\n'
'The context used was:\n'
' $context',
);
}
return true;
}());
return controller!;
}
@override
DrawerControllerState createState() => DrawerControllerState();
}
@ -669,39 +743,42 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
}
assert(platformHasBackButton != null);
final Widget child = RepaintBoundary(
child: Stack(
children: <Widget>[
BlockSemantics(
child: ExcludeSemantics(
// On Android, the back button is used to dismiss a modal.
excluding: platformHasBackButton,
child: GestureDetector(
onTap: close,
child: Semantics(
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
child: Container( // The drawer's "scrim"
color: _scrimColorTween.evaluate(_controller),
final Widget child = _DrawerControllerScope(
controller: widget,
child: RepaintBoundary(
child: Stack(
children: <Widget>[
BlockSemantics(
child: ExcludeSemantics(
// On Android, the back button is used to dismiss a modal.
excluding: platformHasBackButton,
child: GestureDetector(
onTap: close,
child: Semantics(
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
child: Container( // The drawer's "scrim"
color: _scrimColorTween.evaluate(_controller),
),
),
),
),
),
),
Align(
alignment: _drawerOuterAlignment,
child: Align(
alignment: _drawerInnerAlignment,
widthFactor: _controller.value,
child: RepaintBoundary(
child: FocusScope(
key: _drawerKey,
node: _focusScopeNode,
child: widget.child,
Align(
alignment: _drawerOuterAlignment,
child: Align(
alignment: _drawerInnerAlignment,
widthFactor: _controller.value,
child: RepaintBoundary(
child: FocusScope(
key: _drawerKey,
node: _focusScopeNode,
child: widget.child,
),
),
),
),
),
],
],
),
),
);
@ -731,3 +808,55 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
);
}
}
class _DrawerDefaultsM2 extends DrawerThemeData {
const _DrawerDefaultsM2(this.context)
: super(elevation: 16.0);
final BuildContext context;
@override
Color? get shadowColor => Theme.of(context).shadowColor;
}
// BEGIN GENERATED TOKEN PROPERTIES - Drawer
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_141
class _DrawerDefaultsM3 extends DrawerThemeData {
const _DrawerDefaultsM3(this.context)
: super(elevation: 1.0);
final BuildContext context;
@override
Color? get backgroundColor => Theme.of(context).colorScheme.surface;
@override
Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
@override
Color? get shadowColor => Colors.transparent;
// This don't appear to be tokens for this value, but it is
// shown in the spec.
@override
ShapeBorder? get shape => const RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(right: Radius.circular(16.0)),
);
// This don't appear to be tokens for this value, but it is
// shown in the spec.
@override
ShapeBorder? get endShape => const RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(left: Radius.circular(16.0)),
);
}
// END GENERATED TOKEN PROPERTIES - Drawer

View file

@ -41,6 +41,7 @@ class DrawerThemeData with Diagnosticable {
this.shadowColor,
this.surfaceTintColor,
this.shape,
this.endShape,
this.width,
});
@ -62,6 +63,9 @@ class DrawerThemeData with Diagnosticable {
/// Overrides the default value of [Drawer.shape].
final ShapeBorder? shape;
/// Overrides the default value of [Drawer.shape] for a end drawer.
final ShapeBorder? endShape;
/// Overrides the default value of [Drawer.width].
final double? width;
@ -74,6 +78,7 @@ class DrawerThemeData with Diagnosticable {
Color? shadowColor,
Color? surfaceTintColor,
ShapeBorder? shape,
ShapeBorder? endShape,
double? width,
}) {
return DrawerThemeData(
@ -83,6 +88,7 @@ class DrawerThemeData with Diagnosticable {
shadowColor: shadowColor ?? this.shadowColor,
surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
shape: shape ?? this.shape,
endShape: endShape ?? this.endShape,
width: width ?? this.width,
);
}
@ -104,6 +110,7 @@ class DrawerThemeData with Diagnosticable {
shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
endShape: ShapeBorder.lerp(a?.endShape, b?.endShape, t),
width: lerpDouble(a?.width, b?.width, t),
);
}
@ -116,6 +123,7 @@ class DrawerThemeData with Diagnosticable {
shadowColor,
surfaceTintColor,
shape,
endShape,
width,
);
@ -134,6 +142,7 @@ class DrawerThemeData with Diagnosticable {
&& other.shadowColor == shadowColor
&& other.surfaceTintColor == surfaceTintColor
&& other.shape == shape
&& other.endShape == endShape
&& other.width == width;
}
@ -146,6 +155,7 @@ class DrawerThemeData with Diagnosticable {
properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('endShape', endShape, defaultValue: null));
properties.add(DoubleProperty('width', width, defaultValue: null));
}
}

View file

@ -393,7 +393,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
this.tooltip,
});
/// Builds the icon for an destination in a [NavigationBar].
/// Builds the icon for a destination in a [NavigationBar].
///
/// To animate between unselected and selected, build the icon based on
/// [_NavigationDestinationInfo.selectedAnimation]. When the animation is 0,
@ -405,7 +405,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
/// animation is decreasing or dismissed.
final WidgetBuilder buildIcon;
/// Builds the label for an destination in a [NavigationBar].
/// Builds the label for a destination in a [NavigationBar].
///
/// To animate between unselected and selected, build the icon based on
/// [_NavigationDestinationInfo.selectedAnimation]. When the animation is

View file

@ -0,0 +1,681 @@
// 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/widgets.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'drawer.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'material_state.dart';
import 'navigation_bar.dart';
import 'navigation_drawer_theme.dart';
import 'text_theme.dart';
import 'theme.dart';
/// Material Design Navigation Drawer component.
///
/// On top of [Drawer]s, Navigation drawers offer a persistent and convenient way to switch
/// between primary destinations in an app.
///
/// The style for the icons and text are not affected by parent
/// [DefaultTextStyle]s or [IconTheme]s but rather controlled by parameters or
/// the [NavigationDrawerThemeData].
///
/// The [children] are a list of widgets to be displayed in the drawer. These can be a
/// mixture of any widgets, but there is special handling for [NavigationDrawerDestination]s.
/// They are treated as a group and when one is selected, the [onDestinationSelected]
/// is called with the index into the group that corresponds to the selected destination.
///
/// {@tool dartpad}
/// This example shows a [NavigationDrawer] used within a [Scaffold]
/// widget. The [NavigationDrawer] has headline widget, divider widget and three
/// [NavigationDrawerDestination] widgets. The initial [selectedIndex] is 0.
/// The [onDestinationSelected] callback changes the selected item's index and displays
/// a corresponding widget in the body of the [Scaffold].
///
/// ** See code in examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [Scaffold.drawer], where one specifies a [Drawer] so that it can be
/// shown.
/// * [Scaffold.of], to obtain the current [ScaffoldState], which manages the
/// display and animation of the drawer.
/// * [ScaffoldState.openDrawer], which displays its [Drawer], if any.
/// * <https://material.io/design/components/navigation-drawer.html>
class NavigationDrawer extends StatelessWidget {
/// Creates a Material Design Navigation Drawer component.
const NavigationDrawer({
super.key,
required this.children,
this.backgroundColor,
this.shadowColor,
this.surfaceTintColor,
this.elevation,
this.onDestinationSelected,
this.selectedIndex = 0,
});
/// The background color of the [Material] that holds the [NavigationDrawer]'s
/// contents.
///
/// If this is null, then [NavigationDrawerThemeData.backgroundColor] is used.
/// If that is also null, then it falls back to [ColorScheme.surface].
final Color? backgroundColor;
/// The color used for the drop shadow to indicate elevation.
///
/// If null, [NavigationDrawerThemeData.shadowColor] is used. If that
/// is also null, the default value is [Colors.transparent] which
/// indicates that no drop shadow will be displayed.
///
/// See [Material.shadowColor] for more details on drop shadows.
final Color? shadowColor;
/// The surface tint of the [Material] that holds the [NavigationDrawer]'s
/// contents.
///
/// If this is null, then [NavigationDrawerThemeData.surfaceTintColor] is used.
/// If that is also null, then it falls back to [Material.surfaceTintColor]'s default.
final Color? surfaceTintColor;
/// The elevation of the [NavigationDrawer] itself.
///
/// If null, [NavigationDrawerThemeData.elevation] is used. If that
/// is also null, it will be 1.0.
final double? elevation;
/// Defines the appearance of the items within the navigation drawer.
///
/// The list contains [NavigationDrawerDestination] widgets and/or customized
/// widgets like headlines and dividers.
final List<Widget> children;
/// The index into destinations for the current selected
/// [NavigationDrawerDestination] or null if no destination is selected.
///
/// A valid [selectedIndex] satisfies 0 <= [selectedIndex] < number of [NavigationDrawerDestination].
/// For an invalid [selectedIndex] like `-1`, all desitinations will appear unselected.
final int? selectedIndex;
/// Called when one of the [NavigationDrawerDestination] children is selected.
///
/// This callback usually updates the int passed to [selectedIndex].
///
/// Upon updating [selectedIndex], the [NavigationDrawer] will be rebuilt.
final ValueChanged<int>? onDestinationSelected;
@override
Widget build(BuildContext context) {
final int totalNumberOfDestinations =
children.whereType<NavigationDrawerDestination>().toList().length;
int destinationIndex = 0;
final List<Widget> wrappedChildren = <Widget>[];
Widget wrapChild(Widget child, int index) => _SelectableAnimatedBuilder(
duration: const Duration(milliseconds: 500),
isSelected: index == selectedIndex,
builder: (BuildContext context, Animation<double> animation) {
return _NavigationDrawerDestinationInfo(
index: index,
totalNumberOfDestinations: totalNumberOfDestinations,
selectedAnimation: animation,
onTap: () {
if (onDestinationSelected != null) {
onDestinationSelected!(index);
}
},
child: child,
);
});
for (int i = 0; i < children.length; i++) {
if (children[i] is! NavigationDrawerDestination) {
wrappedChildren.add(children[i]);
} else {
wrappedChildren.add(wrapChild(children[i], destinationIndex));
destinationIndex += 1;
}
}
return Drawer(
backgroundColor: backgroundColor,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
elevation: elevation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: wrappedChildren,
),
);
}
}
/// A Material Design [NavigationDrawer] destination.
///
/// Displays an icon with a label, for use in [NavigationDrawer.children].
class NavigationDrawerDestination extends StatelessWidget {
/// Creates a navigation drawer destination.
const NavigationDrawerDestination({
super.key,
this.backgroundColor,
required this.icon,
this.selectedIcon,
required this.label,
});
/// Sets the color of the [Material] that holds all of the [Drawer]'s
/// contents.
///
/// If this is null, then [DrawerThemeData.backgroundColor] is used. If that
/// is also null, then it falls back to [Material]'s default.
final Color? backgroundColor;
/// The [Widget] (usually an [Icon]) that's displayed for this
/// [NavigationDestination].
///
/// The icon will use [NavigationDrawerThemeData.iconTheme]. If this is
/// null, the default [IconThemeData] would use a size of 24.0 and
/// [ColorScheme.onSurfaceVariant].
final Widget icon;
/// The optional [Widget] (usually an [Icon]) that's displayed when this
/// [NavigationDestination] is selected.
///
/// If [selectedIcon] is non-null, the destination will fade from
/// [icon] to [selectedIcon] when this destination goes from unselected to
/// selected.
///
/// The icon will use [NavigationDrawerThemeData.iconTheme] with
/// [MaterialState.selected]. If this is null, the default [IconThemeData]
/// would use a size of 24.0 and [ColorScheme.onSurfaceVariant].
final Widget? selectedIcon;
/// The text label that appears on the right of the icon
///
/// The accompanying [Text] widget will use
/// [NavigationDrawerThemeData.labelTextStyle]. If this are null, the default
/// text style would use [TextTheme.labelLarge] with [ColorScheme.onSurfaceVariant].
final Widget label;
@override
Widget build(BuildContext context) {
const Set<MaterialState> selectedState = <MaterialState>{
MaterialState.selected
};
const Set<MaterialState> unselectedState = <MaterialState>{};
final NavigationDrawerThemeData navigationDrawerTheme =
NavigationDrawerTheme.of(context);
final NavigationDrawerThemeData defaults =
_NavigationDrawerDefaultsM3(context);
final Animation<double> animation =
_NavigationDrawerDestinationInfo.of(context).selectedAnimation;
return _NavigationDestinationBuilder(
buildIcon: (BuildContext context) {
final Widget selectedIconWidget = IconTheme.merge(
data: navigationDrawerTheme.iconTheme?.resolve(selectedState) ??
defaults.iconTheme!.resolve(selectedState)!,
child: selectedIcon ?? icon,
);
final Widget unselectedIconWidget = IconTheme.merge(
data: navigationDrawerTheme.iconTheme?.resolve(unselectedState) ??
defaults.iconTheme!.resolve(unselectedState)!,
child: icon,
);
return _isForwardOrCompleted(animation)
? selectedIconWidget
: unselectedIconWidget;
},
buildLabel: (BuildContext context) {
final TextStyle? effectiveSelectedLabelTextStyle =
navigationDrawerTheme.labelTextStyle?.resolve(selectedState) ??
defaults.labelTextStyle!.resolve(selectedState);
final TextStyle? effectiveUnselectedLabelTextStyle =
navigationDrawerTheme.labelTextStyle?.resolve(unselectedState) ??
defaults.labelTextStyle!.resolve(unselectedState);
return DefaultTextStyle(
style: _isForwardOrCompleted(animation)
? effectiveSelectedLabelTextStyle!
: effectiveUnselectedLabelTextStyle!,
child: label,
);
},
);
}
}
/// Widget that handles the semantics and layout of a navigation drawer
/// destination.
///
/// Prefer [NavigationDestination] over this widget, as it is a simpler
/// (although less customizable) way to get navigation drawer destinations.
///
/// The icon and label of this destination are built with [buildIcon] and
/// [buildLabel]. They should build the unselected and selected icon and label
/// according to [_NavigationDrawerDestinationInfo.selectedAnimation], where an
/// animation value of 0 is unselected and 1 is selected.
///
/// See [NavigationDestination] for an example.
class _NavigationDestinationBuilder extends StatelessWidget {
/// Builds a destination (icon + label) to use in a Material 3 [NavigationDrawer].
const _NavigationDestinationBuilder({
required this.buildIcon,
required this.buildLabel,
});
/// Builds the icon for a destination in a [NavigationDrawer].
///
/// To animate between unselected and selected, build the icon based on
/// [_NavigationDrawerDestinationInfo.selectedAnimation]. When the animation is 0,
/// the destination is unselected, when the animation is 1, the destination is
/// selected.
///
/// The destination is considered selected as soon as the animation is
/// increasing or completed, and it is considered unselected as soon as the
/// animation is decreasing or dismissed.
final WidgetBuilder buildIcon;
/// Builds the label for a destination in a [NavigationDrawer].
///
/// To animate between unselected and selected, build the icon based on
/// [_NavigationDrawerDestinationInfo.selectedAnimation]. When the animation is
/// 0, the destination is unselected, when the animation is 1, the destination
/// is selected.
///
/// The destination is considered selected as soon as the animation is
/// increasing or completed, and it is considered unselected as soon as the
/// animation is decreasing or dismissed.
final WidgetBuilder buildLabel;
@override
Widget build(BuildContext context) {
final _NavigationDrawerDestinationInfo info = _NavigationDrawerDestinationInfo.of(context);
final NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerTheme.of(context);
final NavigationDrawerThemeData defaults = _NavigationDrawerDefaultsM3(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: _NavigationDestinationSemantics(
child: SizedBox(
height: navigationDrawerTheme.tileHeight ?? defaults.tileHeight,
child: InkWell(
highlightColor: Colors.transparent,
onTap: info.onTap,
borderRadius: const BorderRadius.all(Radius.circular(28.0)),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
NavigationIndicator(
animation: _NavigationDrawerDestinationInfo.of(context).selectedAnimation,
color: navigationDrawerTheme.indicatorColor ?? defaults.indicatorColor!,
shape: navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!,
width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width,
height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height,
),
Row(
children: <Widget>[
const SizedBox(width: 16),
buildIcon(context),
const SizedBox(width: 12),
buildLabel(context),
],
),
],
),
),
),
),
);
}
}
/// Semantics widget for a navigation drawer destination.
///
/// Requires a [_NavigationDrawerDestinationInfo] parent (normally provided by the
/// [NavigationDrawer] by default).
///
/// Provides localized semantic labels to the destination, for example, it will
/// read "Home, Tab 1 of 3".
///
/// Used by [_NavigationDestinationBuilder].
class _NavigationDestinationSemantics extends StatelessWidget {
/// Adds the appropriate semantics for navigation drawer destinations to the
/// [child].
const _NavigationDestinationSemantics({
required this.child,
});
/// The widget that should receive the destination semantics.
final Widget child;
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final _NavigationDrawerDestinationInfo destinationInfo = _NavigationDrawerDestinationInfo.of(context);
// The AnimationStatusBuilder will make sure that the semantics update to
// "selected" when the animation status changes.
return _StatusTransitionWidgetBuilder(
animation: destinationInfo.selectedAnimation,
builder: (BuildContext context, Widget? child) {
return Semantics(
selected: _isForwardOrCompleted(destinationInfo.selectedAnimation),
container: true,
child: child,
);
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
child,
Semantics(
label: localizations.tabLabel(
tabIndex: destinationInfo.index + 1,
tabCount: destinationInfo.totalNumberOfDestinations,
),
),
],
),
);
}
}
/// Widget that listens to an animation, and rebuilds when the animation changes
/// [AnimationStatus].
///
/// This can be more efficient than just using an [AnimatedBuilder] when you
/// only need to rebuild when the [Animation.status] changes, since
/// [AnimatedBuilder] rebuilds every time the animation ticks.
class _StatusTransitionWidgetBuilder extends StatusTransitionWidget {
/// Creates a widget that rebuilds when the given animation changes status.
const _StatusTransitionWidgetBuilder({
required super.animation,
required this.builder,
this.child,
});
/// Called every time the [animation] changes [AnimationStatus].
final TransitionBuilder builder;
/// The child widget to pass to the [builder].
///
/// If a [builder] callback's return value contains a subtree that does not
/// depend on the animation, it's more efficient to build that subtree once
/// instead of rebuilding it on every animation status change.
///
/// Using this pre-built child is entirely optional, but can improve
/// performance in some cases and is therefore a good practice.
///
/// See: [AnimatedBuilder.child]
final Widget? child;
@override
Widget build(BuildContext context) => builder(context, child);
}
/// Inherited widget for passing data from the [NavigationDrawer] to the
/// [NavigationDrawer.destinations] children widgets.
///
/// Useful for building navigation destinations using:
/// `_NavigationDrawerDestinationInfo.of(context)`.
class _NavigationDrawerDestinationInfo extends InheritedWidget {
/// Adds the information needed to build a navigation destination to the
/// [child] and descendants.
const _NavigationDrawerDestinationInfo({
required this.index,
required this.totalNumberOfDestinations,
required this.selectedAnimation,
required this.onTap,
required super.child,
});
/// Which destination index is this in the navigation drawer.
///
/// For example:
///
/// ```dart
/// const NavigationDrawer(
/// children: <Widget>[
/// Text('Headline'), // This doesn't have index.
/// NavigationDrawerDestination(
/// // This is destination index 0.
/// icon: Icon(Icons.surfing),
/// label: Text('Surfing'),
/// ),
/// NavigationDrawerDestination(
/// // This is destination index 1.
/// icon: Icon(Icons.support),
/// label: Text('Support'),
/// ),
/// NavigationDrawerDestination(
/// // This is destination index 2.
/// icon: Icon(Icons.local_hospital),
/// label: Text('Hospital'),
/// ),
/// ]
/// )
/// ```
///
/// This is required for semantics, so that each destination can have a label
/// "Tab 1 of 3", for example.
final int index;
/// How many total destinations are are in this navigation drawer.
///
/// This is required for semantics, so that each destination can have a label
/// "Tab 1 of 4", for example.
final int totalNumberOfDestinations;
/// Indicates whether or not this destination is selected, from 0 (unselected)
/// to 1 (selected).
final Animation<double> selectedAnimation;
/// The callback that should be called when this destination is tapped.
///
/// This is computed by calling [NavigationDrawer.onDestinationSelected]
/// with [index] passed in.
final VoidCallback onTap;
/// Returns a non null [_NavigationDrawerDestinationInfo].
///
/// This will return an error if called with no [_NavigationDrawerDestinationInfo]
/// ancestor.
///
/// Used by widgets that are implementing a navigation destination info to
/// get information like the selected animation and destination number.
static _NavigationDrawerDestinationInfo of(BuildContext context) {
final _NavigationDrawerDestinationInfo? result = context.dependOnInheritedWidgetOfExactType<_NavigationDrawerDestinationInfo>();
assert(
result != null,
'Navigation destinations need a _NavigationDrawerDestinationInfo parent, '
'which is usually provided by NavigationDrawer.',
);
return result!;
}
@override
bool updateShouldNotify(_NavigationDrawerDestinationInfo oldWidget) {
return index != oldWidget.index
|| totalNumberOfDestinations != oldWidget.totalNumberOfDestinations
|| selectedAnimation != oldWidget.selectedAnimation
|| onTap != oldWidget.onTap;
}
}
// Builder widget for widgets that need to be animated from 0 (unselected) to
// 1.0 (selected).
//
// This widget creates and manages an [AnimationController] that it passes down
// to the child through the [builder] function.
//
// When [isSelected] is `true`, the animation controller will animate from
// 0 to 1 (for [duration] time).
//
// When [isSelected] is `false`, the animation controller will animate from
// 1 to 0 (for [duration] time).
//
// If [isSelected] is updated while the widget is animating, the animation will
// be reversed until it is either 0 or 1 again.
//
// Usage:
// ```dart
// _SelectableAnimatedBuilder(
// isSelected: _isDrawerOpen,
// builder: (context, animation) {
// return AnimatedIcon(
// icon: AnimatedIcons.menu_arrow,
// progress: animation,
// semanticLabel: 'Show menu',
// );
// }
// )
// ```
class _SelectableAnimatedBuilder extends StatefulWidget {
/// Builds and maintains an [AnimationController] that will animate from 0 to
/// 1 and back depending on when [isSelected] is true.
const _SelectableAnimatedBuilder({
required this.isSelected,
this.duration = const Duration(milliseconds: 200),
required this.builder,
});
/// When true, the widget will animate an animation controller from 0 to 1.
///
/// The animation controller is passed to the child widget through [builder].
final bool isSelected;
/// How long the animation controller should animate for when [isSelected] is
/// updated.
///
/// If the animation is currently running and [isSelected] is updated, only
/// the [duration] left to finish the animation will be run.
final Duration duration;
/// Builds the child widget based on the current animation status.
///
/// When [isSelected] is updated to true, this builder will be called and the
/// animation will animate up to 1. When [isSelected] is updated to
/// `false`, this will be called and the animation will animate down to 0.
final Widget Function(BuildContext, Animation<double>) builder;
///
@override
_SelectableAnimatedBuilderState createState() => _SelectableAnimatedBuilderState();
}
/// State that manages the [AnimationController] that is passed to
/// [_SelectableAnimatedBuilder.builder].
class _SelectableAnimatedBuilderState extends State<_SelectableAnimatedBuilder>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.duration = widget.duration;
_controller.value = widget.isSelected ? 1.0 : 0.0;
}
@override
void didUpdateWidget(_SelectableAnimatedBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.duration != widget.duration) {
_controller.duration = widget.duration;
}
if (oldWidget.isSelected != widget.isSelected) {
if (widget.isSelected) {
_controller.forward();
} else {
_controller.reverse();
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.builder(
context,
_controller,
);
}
}
/// Returns `true` if this animation is ticking forward, or has completed,
/// based on [status].
bool _isForwardOrCompleted(Animation<double> animation) {
return animation.status == AnimationStatus.forward || animation.status == AnimationStatus.completed;
}
// BEGIN GENERATED TOKEN PROPERTIES - NavigationDrawer
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_141
class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData {
const _NavigationDrawerDefaultsM3(this.context)
: super(
elevation: 1.0,
tileHeight: 56.0,
indicatorShape: const StadiumBorder(),
indicatorSize: const Size(336.0, 56.0),
);
final BuildContext context;
@override
Color? get backgroundColor => Theme.of(context).colorScheme.surface;
@override
Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
@override
Color? get shadowColor => Colors.transparent;
@override
Color? get indicatorColor => Theme.of(context).colorScheme.secondaryContainer;
@override
MaterialStateProperty<IconThemeData?>? get iconTheme {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return IconThemeData(
size: 24.0,
color: states.contains(MaterialState.selected)
? null
: Theme.of(context).colorScheme.onSurfaceVariant,
);
});
}
@override
MaterialStateProperty<TextStyle?>? get labelTextStyle {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
final TextStyle style = Theme.of(context).textTheme.labelLarge!;
return style.apply(
color: states.contains(MaterialState.selected)
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant,
);
});
}
}
// END GENERATED TOKEN PROPERTIES - NavigationDrawer

View file

@ -0,0 +1,255 @@
// 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_drawer.dart';
import 'theme.dart';
// Examples can assume:
// late BuildContext context;
/// Defines default property values for descendant [NavigationDrawer]
/// widgets.
///
/// Descendant widgets obtain the current [NavigationDrawerThemeData] object
/// using `NavigationDrawerTheme.of(context)`. Instances of
/// [NavigationDrawerThemeData] can be customized with
/// [NavigationDrawerThemeData.copyWith].
///
/// Typically a [NavigationDrawerThemeData] is specified as part of the
/// overall [Theme] with [ThemeData.navigationDrawerTheme]. Alternatively, a
/// [NavigationDrawerTheme] inherited widget can be used to theme [NavigationDrawer]s
/// in a subtree of widgets.
///
/// All [NavigationDrawerThemeData] properties are `null` by default.
/// When null, the [NavigationDrawer] will provide its own defaults based on the
/// overall [Theme]'s textTheme and colorScheme. See the individual
/// [NavigationDrawer] properties for details.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
@immutable
class NavigationDrawerThemeData with Diagnosticable {
/// Creates a theme that can be used for [ThemeData.navigationDrawerTheme] and
/// [NavigationDrawerTheme].
const NavigationDrawerThemeData({
this.tileHeight,
this.backgroundColor,
this.elevation,
this.shadowColor,
this.surfaceTintColor,
this.indicatorColor,
this.indicatorShape,
this.indicatorSize,
this.labelTextStyle,
this.iconTheme,
});
/// Overrides the default height of [NavigationDrawerDestination].
final double? tileHeight;
/// Overrides the default value of [NavigationDrawer.backgroundColor].
final Color? backgroundColor;
/// Overrides the default value of [NavigationDrawer.elevation].
final double? elevation;
/// Overrides the default value of [NavigationDrawer.shadowColor].
final Color? shadowColor;
/// Overrides the default value of [NavigationDrawer.surfaceTintColor].
final Color? surfaceTintColor;
/// Overrides the default value of [NavigationDrawer]'s selection indicator.
final Color? indicatorColor;
/// Overrides the default shape of the [NavigationDrawer]'s selection indicator.
final ShapeBorder? indicatorShape;
/// Overrides the default size of the [NavigationDrawer]'s selection indicator.
final Size? indicatorSize;
/// 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;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
NavigationDrawerThemeData copyWith({
double? tileHeight,
Color? backgroundColor,
double? elevation,
Color? shadowColor,
Color? surfaceTintColor,
Color? indicatorColor,
ShapeBorder? indicatorShape,
Size? indicatorSize,
MaterialStateProperty<TextStyle?>? labelTextStyle,
MaterialStateProperty<IconThemeData?>? iconTheme,
}) {
return NavigationDrawerThemeData(
tileHeight: tileHeight ?? this.tileHeight,
backgroundColor: backgroundColor ?? this.backgroundColor,
elevation: elevation ?? this.elevation,
shadowColor: shadowColor ?? this.shadowColor,
surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
indicatorColor: indicatorColor ?? this.indicatorColor,
indicatorShape: indicatorShape ?? this.indicatorShape,
indicatorSize: indicatorSize ?? this.indicatorSize,
labelTextStyle: labelTextStyle ?? this.labelTextStyle,
iconTheme: iconTheme ?? this.iconTheme,
);
}
/// Linearly interpolate between two navigation rail themes.
///
/// If both arguments are null then null is returned.
///
/// {@macro dart.ui.shadow.lerp}
static NavigationDrawerThemeData? lerp(
NavigationDrawerThemeData? a, NavigationDrawerThemeData? b, double t) {
assert(t != null);
if (a == null && b == null) {
return null;
}
return NavigationDrawerThemeData(
tileHeight: lerpDouble(a?.tileHeight, b?.tileHeight, t),
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
indicatorColor: Color.lerp(a?.indicatorColor, b?.indicatorColor, t),
indicatorShape: ShapeBorder.lerp(a?.indicatorShape, b?.indicatorShape, t),
indicatorSize: Size.lerp(a?.indicatorSize, a?.indicatorSize, t),
labelTextStyle: MaterialStateProperty.lerp<TextStyle?>(
a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp),
iconTheme: MaterialStateProperty.lerp<IconThemeData?>(
a?.iconTheme, b?.iconTheme, t, IconThemeData.lerp),
);
}
@override
int get hashCode => Object.hash(
tileHeight,
backgroundColor,
elevation,
shadowColor,
surfaceTintColor,
indicatorColor,
indicatorShape,
indicatorSize,
labelTextStyle,
iconTheme,
);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is NavigationDrawerThemeData &&
other.tileHeight == tileHeight &&
other.backgroundColor == backgroundColor &&
other.elevation == elevation &&
other.shadowColor == shadowColor &&
other.surfaceTintColor == surfaceTintColor &&
other.indicatorColor == indicatorColor &&
other.indicatorShape == indicatorShape &&
other.indicatorSize == indicatorSize &&
other.labelTextStyle == labelTextStyle &&
other.iconTheme == iconTheme;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(DoubleProperty('tileHeight', tileHeight, defaultValue: null));
properties.add(
ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
properties.add(ColorProperty('surfaceTintColor', surfaceTintColor,
defaultValue: null));
properties.add(
ColorProperty('indicatorColor', indicatorColor, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>(
'indicatorShape', indicatorShape,
defaultValue: null));
properties.add(DiagnosticsProperty<Size>('indicatorSize', indicatorSize,
defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>(
'labelTextStyle', labelTextStyle,
defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<IconThemeData?>>(
'iconTheme', iconTheme,
defaultValue: null));
}
}
/// An inherited widget that defines visual properties for [NavigationDrawer]s and
/// [NavigationDestination]s in this widget's subtree.
///
/// Values specified here are used for [NavigationDrawer] properties that are not
/// given an explicit non-null value.
///
/// See also:
///
/// * [ThemeData.navigationDrawerTheme], which describes the
/// [NavigationDrawerThemeData] in the overall theme for the application.
class NavigationDrawerTheme extends InheritedTheme {
/// Creates a navigation rail theme that controls the
/// [NavigationDrawerThemeData] properties for a [NavigationDrawer].
///
/// The data argument must not be null.
const NavigationDrawerTheme({
super.key,
required this.data,
required super.child,
}) : assert(data != null);
/// Specifies the background color, label text style, icon theme, and label
/// type values for descendant [NavigationDrawer] widgets.
final NavigationDrawerThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [NavigationDrawerTheme] widget, then
/// [ThemeData.navigationDrawerTheme] is used.
static NavigationDrawerThemeData of(BuildContext context) {
final NavigationDrawerTheme? navigationDrawerTheme =
context.dependOnInheritedWidgetOfExactType<NavigationDrawerTheme>();
return navigationDrawerTheme?.data ??
Theme.of(context).navigationDrawerTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return NavigationDrawerTheme(data: data, child: child);
}
@override
bool updateShouldNotify(NavigationDrawerTheme oldWidget) =>
data != oldWidget.data;
}

View file

@ -41,6 +41,7 @@ import 'menu_bar_theme.dart';
import 'menu_button_theme.dart';
import 'menu_theme.dart';
import 'navigation_bar_theme.dart';
import 'navigation_drawer_theme.dart';
import 'navigation_rail_theme.dart';
import 'outlined_button_theme.dart';
import 'page_transitions_theme.dart';
@ -357,6 +358,7 @@ class ThemeData with Diagnosticable {
MenuButtonThemeData? menuButtonTheme,
MenuThemeData? menuTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationDrawerThemeData? navigationDrawerTheme,
NavigationRailThemeData? navigationRailTheme,
OutlinedButtonThemeData? outlinedButtonTheme,
PopupMenuThemeData? popupMenuTheme,
@ -610,6 +612,7 @@ class ThemeData with Diagnosticable {
menuButtonTheme ??= const MenuButtonThemeData();
menuTheme ??= const MenuThemeData();
navigationBarTheme ??= const NavigationBarThemeData();
navigationDrawerTheme ??= const NavigationDrawerThemeData();
navigationRailTheme ??= const NavigationRailThemeData();
outlinedButtonTheme ??= const OutlinedButtonThemeData();
popupMenuTheme ??= const PopupMenuThemeData();
@ -706,6 +709,7 @@ class ThemeData with Diagnosticable {
menuButtonTheme: menuButtonTheme,
menuTheme: menuTheme,
navigationBarTheme: navigationBarTheme,
navigationDrawerTheme: navigationDrawerTheme,
navigationRailTheme: navigationRailTheme,
outlinedButtonTheme: outlinedButtonTheme,
popupMenuTheme: popupMenuTheme,
@ -818,6 +822,7 @@ class ThemeData with Diagnosticable {
required this.menuButtonTheme,
required this.menuTheme,
required this.navigationBarTheme,
required this.navigationDrawerTheme,
required this.navigationRailTheme,
required this.outlinedButtonTheme,
required this.popupMenuTheme,
@ -988,6 +993,7 @@ class ThemeData with Diagnosticable {
assert(menuButtonTheme != null),
assert(menuTheme != null),
assert(navigationBarTheme != null),
assert(navigationDrawerTheme != null),
assert(navigationRailTheme != null),
assert(outlinedButtonTheme != null),
assert(popupMenuTheme != null),
@ -1586,6 +1592,10 @@ class ThemeData with Diagnosticable {
/// of a [NavigationBar].
final NavigationBarThemeData navigationBarTheme;
/// A theme for customizing the background color, text style, and icon themes
/// of a [NavigationDrawer].
final NavigationDrawerThemeData navigationDrawerTheme;
/// A theme for customizing the background color, elevation, text style, and
/// icon themes of a [NavigationRail].
final NavigationRailThemeData navigationRailTheme;
@ -1883,6 +1893,7 @@ class ThemeData with Diagnosticable {
MenuButtonThemeData? menuButtonTheme,
MenuThemeData? menuTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationDrawerThemeData? navigationDrawerTheme,
NavigationRailThemeData? navigationRailTheme,
OutlinedButtonThemeData? outlinedButtonTheme,
PopupMenuThemeData? popupMenuTheme,
@ -2046,6 +2057,7 @@ class ThemeData with Diagnosticable {
menuButtonTheme: menuButtonTheme ?? this.menuButtonTheme,
menuTheme: menuTheme ?? this.menuTheme,
navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
navigationDrawerTheme: navigationDrawerTheme ?? this.navigationDrawerTheme,
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme,
popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme,
@ -2251,6 +2263,7 @@ class ThemeData with Diagnosticable {
menuButtonTheme: MenuButtonThemeData.lerp(a.menuButtonTheme, b.menuButtonTheme, t)!,
menuTheme: MenuThemeData.lerp(a.menuTheme, b.menuTheme, t)!,
navigationBarTheme: NavigationBarThemeData.lerp(a.navigationBarTheme, b.navigationBarTheme, t)!,
navigationDrawerTheme: NavigationDrawerThemeData.lerp(a.navigationDrawerTheme, b.navigationDrawerTheme, t)!,
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!,
outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t)!,
popupMenuTheme: PopupMenuThemeData.lerp(a.popupMenuTheme, b.popupMenuTheme, t)!,
@ -2358,6 +2371,7 @@ class ThemeData with Diagnosticable {
other.menuButtonTheme == menuButtonTheme &&
other.menuTheme == menuTheme &&
other.navigationBarTheme == navigationBarTheme &&
other.navigationDrawerTheme == navigationDrawerTheme &&
other.navigationRailTheme == navigationRailTheme &&
other.outlinedButtonTheme == outlinedButtonTheme &&
other.popupMenuTheme == popupMenuTheme &&
@ -2462,6 +2476,7 @@ class ThemeData with Diagnosticable {
menuButtonTheme,
menuTheme,
navigationBarTheme,
navigationDrawerTheme,
navigationRailTheme,
outlinedButtonTheme,
popupMenuTheme,
@ -2568,6 +2583,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<MenuButtonThemeData>('menuButtonTheme', menuButtonTheme, defaultValue: defaultData.menuButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<MenuThemeData>('menuTheme', menuTheme, defaultValue: defaultData.menuTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationBarThemeData>('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationDrawerThemeData>('navigationDrawerTheme', navigationDrawerTheme, defaultValue: defaultData.navigationDrawerTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailTheme', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<OutlinedButtonThemeData>('outlinedButtonTheme', outlinedButtonTheme, defaultValue: defaultData.outlinedButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<PopupMenuThemeData>('popupMenuTheme', popupMenuTheme, defaultValue: defaultData.popupMenuTheme, level: DiagnosticLevel.debug));

View file

@ -70,7 +70,40 @@ void main() {
expect(_drawerMaterial(tester).elevation, 16.0);
expect(_drawerMaterial(tester).shadowColor, useMaterial3 ? Colors.transparent : ThemeData().shadowColor);
expect(_drawerMaterial(tester).surfaceTintColor, useMaterial3 ? ThemeData().colorScheme.surfaceTint : null);
expect(_drawerMaterial(tester).shape, null);
expect(
_drawerMaterial(tester).shape,
useMaterial3
? const RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(right: Radius.circular(16.0)))
: null,
);
expect(_scrim(tester).color, Colors.black54);
expect(_drawerRenderBox(tester).size.width, 304.0);
});
testWidgets('Default values are used when no Drawer or DrawerThemeData properties are specified in end drawer', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final bool useMaterial3 = ThemeData().useMaterial3;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: scaffoldKey,
endDrawer: const Drawer(),
),
),
);
scaffoldKey.currentState!.openEndDrawer();
await tester.pumpAndSettle();
expect(_drawerMaterial(tester).color, null);
expect(_drawerMaterial(tester).elevation, 16.0);
expect(_drawerMaterial(tester).shadowColor, useMaterial3 ? Colors.transparent : ThemeData().shadowColor);
expect(_drawerMaterial(tester).surfaceTintColor, useMaterial3 ? ThemeData().colorScheme.surfaceTint : null);
expect(
_drawerMaterial(tester).shape,
useMaterial3
? const RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(16.0)))
: null,
);
expect(_scrim(tester).color, Colors.black54);
expect(_drawerRenderBox(tester).size.width, 304.0);
});

View file

@ -0,0 +1,263 @@
// 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 drawer updates destinations when tapped',
(WidgetTester tester) async {
int mutatedIndex = -1;
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final ThemeData theme= ThemeData.from(colorScheme: const ColorScheme.light());
widgetSetup(tester, 3000, windowHeight: 3000);
final Widget widget = _buildWidget(
scaffoldKey,
NavigationDrawer(
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
),
],
onDestinationSelected: (int i) {
mutatedIndex = i;
},
),
);
await tester.pumpWidget(widget);
scaffoldKey.currentState!.openDrawer();
await tester.pump();
expect(find.text('Headline'), findsOneWidget);
expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget);
await tester.pump(const Duration(seconds: 1)); // animation done
await tester.tap(find.text('Alarm'));
expect(mutatedIndex, 1);
await tester.tap(find.text('AC'));
expect(mutatedIndex, 0);
});
testWidgets('NavigationDrawer can update background color',
(WidgetTester tester) async {
const Color color = Colors.yellow;
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final ThemeData theme= ThemeData.from(colorScheme: const ColorScheme.light());
await tester.pumpWidget(
_buildWidget(
scaffoldKey,
NavigationDrawer(
backgroundColor: color,
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
),
],
onDestinationSelected: (int i) {},
),
),
);
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1)); // animation done
expect(_getMaterial(tester).color, equals(color));
});
testWidgets('NavigationDrawer can update elevation',
(WidgetTester tester) async {
const double elevation = 42.0;
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final ThemeData theme= ThemeData.from(colorScheme: const ColorScheme.light());
final NavigationDrawer drawer = NavigationDrawer(
elevation: elevation,
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
),
],
);
await tester.pumpWidget(
_buildWidget(
scaffoldKey,
drawer,
),
);
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1));
expect(_getMaterial(tester).elevation, equals(elevation));
});
testWidgets(
'NavigationDrawer uses proper defaults when no parameters are given',
(WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final ThemeData theme= ThemeData.from(colorScheme: const ColorScheme.light());
// M3 settings from the token database.
await tester.pumpWidget(
_buildWidget(
scaffoldKey,
Theme(
data: ThemeData.light().copyWith(useMaterial3: true),
child: NavigationDrawer(
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
),
],
onDestinationSelected: (int i) {},
),
),
),
);
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1));
expect(_getMaterial(tester).color, ThemeData().colorScheme.surface);
expect(_getMaterial(tester).surfaceTintColor,
ThemeData().colorScheme.surfaceTint);
expect(_getMaterial(tester).elevation, 1);
expect(_indicator(tester)?.color, const Color(0xff2196f3));
expect(_indicator(tester)?.shape, const StadiumBorder());
});
testWidgets('Navigation drawer semantics', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final ThemeData theme= ThemeData.from(colorScheme: const ColorScheme.light());
Widget widget({int selectedIndex = 0}) {
return _buildWidget(
scaffoldKey,
NavigationDrawer(
selectedIndex: selectedIndex,
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
),
],
),
);
}
await tester.pumpWidget(widget());
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1));
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,
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,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
});
}
Widget _buildWidget(GlobalKey<ScaffoldState> scaffoldKey, Widget child) {
return MaterialApp(
theme: ThemeData.light(),
home: Scaffold(
key: scaffoldKey,
drawer: child,
body: Container(),
),
);
}
Material _getMaterial(WidgetTester tester) {
return tester.firstWidget<Material>(
find.descendant(
of: find.byType(NavigationDrawer), 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?;
}
void widgetSetup(WidgetTester tester, double windowWidth,
{double? windowHeight}) {
final double height = windowHeight ?? 1000;
tester.binding.window.devicePixelRatioTestValue = 2;
final double dpi = tester.binding.window.devicePixelRatio;
tester.binding.window.physicalSizeTestValue =
Size(windowWidth * dpi, height * dpi);
}

View file

@ -794,6 +794,7 @@ void main() {
menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.black))),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black),
navigationDrawerTheme: const NavigationDrawerThemeData(backgroundColor: Colors.black),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(foregroundColor: Colors.blue)),
popupMenuTheme: const PopupMenuThemeData(color: Colors.black),
@ -913,6 +914,7 @@ void main() {
menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.white))),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white),
navigationDrawerTheme: const NavigationDrawerThemeData(backgroundColor: Colors.white),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
outlinedButtonTheme: const OutlinedButtonThemeData(),
popupMenuTheme: const PopupMenuThemeData(color: Colors.white),
@ -1018,6 +1020,7 @@ void main() {
menuButtonTheme: otherTheme.menuButtonTheme,
menuTheme: otherTheme.menuTheme,
navigationBarTheme: otherTheme.navigationBarTheme,
navigationDrawerTheme: otherTheme.navigationDrawerTheme,
navigationRailTheme: otherTheme.navigationRailTheme,
outlinedButtonTheme: otherTheme.outlinedButtonTheme,
popupMenuTheme: otherTheme.popupMenuTheme,
@ -1260,6 +1263,7 @@ void main() {
'menuButtonTheme',
'menuTheme',
'navigationBarTheme',
'navigationDrawerTheme',
'navigationRailTheme',
'outlinedButtonTheme',
'popupMenuTheme',