mirror of
https://github.com/flutter/flutter
synced 2024-09-13 21:32:11 +00:00
nav drawer (#115668)
This commit is contained in:
parent
01c1e8e587
commit
0e57147db1
|
@ -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();
|
||||
|
|
42
dev/tools/gen_defaults/lib/drawer_template.dart
Normal file
42
dev/tools/gen_defaults/lib/drawer_template.dart
Normal 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)),
|
||||
);
|
||||
}
|
||||
''';
|
||||
}
|
60
dev/tools/gen_defaults/lib/navigation_drawer_template.dart
Normal file
60
dev/tools/gen_defaults/lib/navigation_drawer_template.dart
Normal 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")},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
''';
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
681
packages/flutter/lib/src/material/navigation_drawer.dart
Normal file
681
packages/flutter/lib/src/material/navigation_drawer.dart
Normal 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
|
255
packages/flutter/lib/src/material/navigation_drawer_theme.dart
Normal file
255
packages/flutter/lib/src/material/navigation_drawer_theme.dart
Normal 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;
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
263
packages/flutter/test/material/navigation_drawer_test.dart
Normal file
263
packages/flutter/test/material/navigation_drawer_test.dart
Normal 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);
|
||||
}
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue