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/color_scheme_template.dart';
|
||||||
import 'package:gen_defaults/dialog_template.dart';
|
import 'package:gen_defaults/dialog_template.dart';
|
||||||
import 'package:gen_defaults/divider_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/fab_template.dart';
|
||||||
import 'package:gen_defaults/filter_chip_template.dart';
|
import 'package:gen_defaults/filter_chip_template.dart';
|
||||||
import 'package:gen_defaults/icon_button_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/input_decorator_template.dart';
|
||||||
import 'package:gen_defaults/menu_template.dart';
|
import 'package:gen_defaults/menu_template.dart';
|
||||||
import 'package:gen_defaults/navigation_bar_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/navigation_rail_template.dart';
|
||||||
import 'package:gen_defaults/popup_menu_template.dart';
|
import 'package:gen_defaults/popup_menu_template.dart';
|
||||||
import 'package:gen_defaults/progress_indicator_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();
|
DialogFullscreenTemplate('DialogFullscreen', '$materialLib/dialog.dart', tokens).updateFile();
|
||||||
DialogTemplate('Dialog', '$materialLib/dialog.dart', tokens).updateFile();
|
DialogTemplate('Dialog', '$materialLib/dialog.dart', tokens).updateFile();
|
||||||
DividerTemplate('Divider', '$materialLib/divider.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();
|
FABTemplate('FAB', '$materialLib/floating_action_button.dart', tokens).updateFile();
|
||||||
FilterChipTemplate('ChoiceChip', '$materialLib/choice_chip.dart', tokens).updateFile();
|
FilterChipTemplate('ChoiceChip', '$materialLib/choice_chip.dart', tokens).updateFile();
|
||||||
FilterChipTemplate('FilterChip', '$materialLib/filter_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();
|
InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile();
|
||||||
MenuTemplate('Menu', '$materialLib/menu_anchor.dart', tokens).updateFile();
|
MenuTemplate('Menu', '$materialLib/menu_anchor.dart', tokens).updateFile();
|
||||||
NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.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();
|
NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile();
|
||||||
PopupMenuTemplate('PopupMenu', '$materialLib/popup_menu.dart', tokens).updateFile();
|
PopupMenuTemplate('PopupMenu', '$materialLib/popup_menu.dart', tokens).updateFile();
|
||||||
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.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/mergeable_material.dart';
|
||||||
export 'src/material/navigation_bar.dart';
|
export 'src/material/navigation_bar.dart';
|
||||||
export 'src/material/navigation_bar_theme.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.dart';
|
||||||
export 'src/material/navigation_rail_theme.dart';
|
export 'src/material/navigation_rail_theme.dart';
|
||||||
export 'src/material/no_splash.dart';
|
export 'src/material/no_splash.dart';
|
||||||
|
@ -253,6 +253,8 @@ class Drawer extends StatelessWidget {
|
|||||||
label = semanticLabel ?? MaterialLocalizations.of(context).drawerLabel;
|
label = semanticLabel ?? MaterialLocalizations.of(context).drawerLabel;
|
||||||
}
|
}
|
||||||
final bool useMaterial3 = Theme.of(context).useMaterial3;
|
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(
|
return Semantics(
|
||||||
scopesRoute: true,
|
scopesRoute: true,
|
||||||
namesRoute: true,
|
namesRoute: true,
|
||||||
@ -261,11 +263,13 @@ class Drawer extends StatelessWidget {
|
|||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints.expand(width: width ?? drawerTheme.width ?? _kWidth),
|
constraints: BoxConstraints.expand(width: width ?? drawerTheme.width ?? _kWidth),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: backgroundColor ?? drawerTheme.backgroundColor,
|
color: backgroundColor ?? drawerTheme.backgroundColor ?? defaults.backgroundColor,
|
||||||
elevation: elevation ?? drawerTheme.elevation ?? 16.0,
|
elevation: elevation ?? drawerTheme.elevation ?? defaults.elevation!,
|
||||||
shadowColor: shadowColor ?? drawerTheme.shadowColor ?? (useMaterial3 ? Colors.transparent : Theme.of(context).shadowColor),
|
shadowColor: shadowColor ?? drawerTheme.shadowColor ?? defaults.shadowColor,
|
||||||
surfaceTintColor: surfaceTintColor ?? drawerTheme.surfaceTintColor ?? (useMaterial3 ? Theme.of(context).colorScheme.surfaceTint : null),
|
surfaceTintColor: surfaceTintColor ?? drawerTheme.surfaceTintColor ?? defaults.surfaceTintColor,
|
||||||
shape: shape ?? drawerTheme.shape,
|
shape: shape ?? (isDrawerStart
|
||||||
|
? (drawerTheme.shape ?? defaults.shape)
|
||||||
|
: (drawerTheme.endShape ?? defaults.endShape)),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -277,6 +281,20 @@ class Drawer extends StatelessWidget {
|
|||||||
/// opened or closed.
|
/// opened or closed.
|
||||||
typedef DrawerCallback = void Function(bool isOpened);
|
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.
|
/// Provides interactive behavior for [Drawer] widgets.
|
||||||
///
|
///
|
||||||
/// Rarely used directly. Drawer controllers are typically created automatically
|
/// Rarely used directly. Drawer controllers are typically created automatically
|
||||||
@ -379,6 +397,62 @@ class DrawerController extends StatefulWidget {
|
|||||||
/// application was killed.
|
/// application was killed.
|
||||||
final bool isDrawerOpen;
|
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
|
@override
|
||||||
DrawerControllerState createState() => DrawerControllerState();
|
DrawerControllerState createState() => DrawerControllerState();
|
||||||
}
|
}
|
||||||
@ -669,39 +743,42 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
|
|||||||
}
|
}
|
||||||
assert(platformHasBackButton != null);
|
assert(platformHasBackButton != null);
|
||||||
|
|
||||||
final Widget child = RepaintBoundary(
|
final Widget child = _DrawerControllerScope(
|
||||||
child: Stack(
|
controller: widget,
|
||||||
children: <Widget>[
|
child: RepaintBoundary(
|
||||||
BlockSemantics(
|
child: Stack(
|
||||||
child: ExcludeSemantics(
|
children: <Widget>[
|
||||||
// On Android, the back button is used to dismiss a modal.
|
BlockSemantics(
|
||||||
excluding: platformHasBackButton,
|
child: ExcludeSemantics(
|
||||||
child: GestureDetector(
|
// On Android, the back button is used to dismiss a modal.
|
||||||
onTap: close,
|
excluding: platformHasBackButton,
|
||||||
child: Semantics(
|
child: GestureDetector(
|
||||||
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
onTap: close,
|
||||||
child: Container( // The drawer's "scrim"
|
child: Semantics(
|
||||||
color: _scrimColorTween.evaluate(_controller),
|
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||||
|
child: Container( // The drawer's "scrim"
|
||||||
|
color: _scrimColorTween.evaluate(_controller),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Align(
|
||||||
Align(
|
alignment: _drawerOuterAlignment,
|
||||||
alignment: _drawerOuterAlignment,
|
child: Align(
|
||||||
child: Align(
|
alignment: _drawerInnerAlignment,
|
||||||
alignment: _drawerInnerAlignment,
|
widthFactor: _controller.value,
|
||||||
widthFactor: _controller.value,
|
child: RepaintBoundary(
|
||||||
child: RepaintBoundary(
|
child: FocusScope(
|
||||||
child: FocusScope(
|
key: _drawerKey,
|
||||||
key: _drawerKey,
|
node: _focusScopeNode,
|
||||||
node: _focusScopeNode,
|
child: widget.child,
|
||||||
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.shadowColor,
|
||||||
this.surfaceTintColor,
|
this.surfaceTintColor,
|
||||||
this.shape,
|
this.shape,
|
||||||
|
this.endShape,
|
||||||
this.width,
|
this.width,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -62,6 +63,9 @@ class DrawerThemeData with Diagnosticable {
|
|||||||
/// Overrides the default value of [Drawer.shape].
|
/// Overrides the default value of [Drawer.shape].
|
||||||
final ShapeBorder? 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].
|
/// Overrides the default value of [Drawer.width].
|
||||||
final double? width;
|
final double? width;
|
||||||
|
|
||||||
@ -74,6 +78,7 @@ class DrawerThemeData with Diagnosticable {
|
|||||||
Color? shadowColor,
|
Color? shadowColor,
|
||||||
Color? surfaceTintColor,
|
Color? surfaceTintColor,
|
||||||
ShapeBorder? shape,
|
ShapeBorder? shape,
|
||||||
|
ShapeBorder? endShape,
|
||||||
double? width,
|
double? width,
|
||||||
}) {
|
}) {
|
||||||
return DrawerThemeData(
|
return DrawerThemeData(
|
||||||
@ -83,6 +88,7 @@ class DrawerThemeData with Diagnosticable {
|
|||||||
shadowColor: shadowColor ?? this.shadowColor,
|
shadowColor: shadowColor ?? this.shadowColor,
|
||||||
surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
|
surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
|
||||||
shape: shape ?? this.shape,
|
shape: shape ?? this.shape,
|
||||||
|
endShape: endShape ?? this.endShape,
|
||||||
width: width ?? this.width,
|
width: width ?? this.width,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -104,6 +110,7 @@ class DrawerThemeData with Diagnosticable {
|
|||||||
shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
|
shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
|
||||||
surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
|
surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
|
||||||
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
|
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
|
||||||
|
endShape: ShapeBorder.lerp(a?.endShape, b?.endShape, t),
|
||||||
width: lerpDouble(a?.width, b?.width, t),
|
width: lerpDouble(a?.width, b?.width, t),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -116,6 +123,7 @@ class DrawerThemeData with Diagnosticable {
|
|||||||
shadowColor,
|
shadowColor,
|
||||||
surfaceTintColor,
|
surfaceTintColor,
|
||||||
shape,
|
shape,
|
||||||
|
endShape,
|
||||||
width,
|
width,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -134,6 +142,7 @@ class DrawerThemeData with Diagnosticable {
|
|||||||
&& other.shadowColor == shadowColor
|
&& other.shadowColor == shadowColor
|
||||||
&& other.surfaceTintColor == surfaceTintColor
|
&& other.surfaceTintColor == surfaceTintColor
|
||||||
&& other.shape == shape
|
&& other.shape == shape
|
||||||
|
&& other.endShape == endShape
|
||||||
&& other.width == width;
|
&& other.width == width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +155,7 @@ class DrawerThemeData with Diagnosticable {
|
|||||||
properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
|
properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
|
||||||
properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
|
properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, 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));
|
properties.add(DoubleProperty('width', width, defaultValue: null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -393,7 +393,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
|
|||||||
this.tooltip,
|
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
|
/// To animate between unselected and selected, build the icon based on
|
||||||
/// [_NavigationDestinationInfo.selectedAnimation]. When the animation is 0,
|
/// [_NavigationDestinationInfo.selectedAnimation]. When the animation is 0,
|
||||||
@ -405,7 +405,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
|
|||||||
/// animation is decreasing or dismissed.
|
/// animation is decreasing or dismissed.
|
||||||
final WidgetBuilder buildIcon;
|
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
|
/// To animate between unselected and selected, build the icon based on
|
||||||
/// [_NavigationDestinationInfo.selectedAnimation]. When the animation is
|
/// [_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_button_theme.dart';
|
||||||
import 'menu_theme.dart';
|
import 'menu_theme.dart';
|
||||||
import 'navigation_bar_theme.dart';
|
import 'navigation_bar_theme.dart';
|
||||||
|
import 'navigation_drawer_theme.dart';
|
||||||
import 'navigation_rail_theme.dart';
|
import 'navigation_rail_theme.dart';
|
||||||
import 'outlined_button_theme.dart';
|
import 'outlined_button_theme.dart';
|
||||||
import 'page_transitions_theme.dart';
|
import 'page_transitions_theme.dart';
|
||||||
@ -357,6 +358,7 @@ class ThemeData with Diagnosticable {
|
|||||||
MenuButtonThemeData? menuButtonTheme,
|
MenuButtonThemeData? menuButtonTheme,
|
||||||
MenuThemeData? menuTheme,
|
MenuThemeData? menuTheme,
|
||||||
NavigationBarThemeData? navigationBarTheme,
|
NavigationBarThemeData? navigationBarTheme,
|
||||||
|
NavigationDrawerThemeData? navigationDrawerTheme,
|
||||||
NavigationRailThemeData? navigationRailTheme,
|
NavigationRailThemeData? navigationRailTheme,
|
||||||
OutlinedButtonThemeData? outlinedButtonTheme,
|
OutlinedButtonThemeData? outlinedButtonTheme,
|
||||||
PopupMenuThemeData? popupMenuTheme,
|
PopupMenuThemeData? popupMenuTheme,
|
||||||
@ -610,6 +612,7 @@ class ThemeData with Diagnosticable {
|
|||||||
menuButtonTheme ??= const MenuButtonThemeData();
|
menuButtonTheme ??= const MenuButtonThemeData();
|
||||||
menuTheme ??= const MenuThemeData();
|
menuTheme ??= const MenuThemeData();
|
||||||
navigationBarTheme ??= const NavigationBarThemeData();
|
navigationBarTheme ??= const NavigationBarThemeData();
|
||||||
|
navigationDrawerTheme ??= const NavigationDrawerThemeData();
|
||||||
navigationRailTheme ??= const NavigationRailThemeData();
|
navigationRailTheme ??= const NavigationRailThemeData();
|
||||||
outlinedButtonTheme ??= const OutlinedButtonThemeData();
|
outlinedButtonTheme ??= const OutlinedButtonThemeData();
|
||||||
popupMenuTheme ??= const PopupMenuThemeData();
|
popupMenuTheme ??= const PopupMenuThemeData();
|
||||||
@ -706,6 +709,7 @@ class ThemeData with Diagnosticable {
|
|||||||
menuButtonTheme: menuButtonTheme,
|
menuButtonTheme: menuButtonTheme,
|
||||||
menuTheme: menuTheme,
|
menuTheme: menuTheme,
|
||||||
navigationBarTheme: navigationBarTheme,
|
navigationBarTheme: navigationBarTheme,
|
||||||
|
navigationDrawerTheme: navigationDrawerTheme,
|
||||||
navigationRailTheme: navigationRailTheme,
|
navigationRailTheme: navigationRailTheme,
|
||||||
outlinedButtonTheme: outlinedButtonTheme,
|
outlinedButtonTheme: outlinedButtonTheme,
|
||||||
popupMenuTheme: popupMenuTheme,
|
popupMenuTheme: popupMenuTheme,
|
||||||
@ -818,6 +822,7 @@ class ThemeData with Diagnosticable {
|
|||||||
required this.menuButtonTheme,
|
required this.menuButtonTheme,
|
||||||
required this.menuTheme,
|
required this.menuTheme,
|
||||||
required this.navigationBarTheme,
|
required this.navigationBarTheme,
|
||||||
|
required this.navigationDrawerTheme,
|
||||||
required this.navigationRailTheme,
|
required this.navigationRailTheme,
|
||||||
required this.outlinedButtonTheme,
|
required this.outlinedButtonTheme,
|
||||||
required this.popupMenuTheme,
|
required this.popupMenuTheme,
|
||||||
@ -988,6 +993,7 @@ class ThemeData with Diagnosticable {
|
|||||||
assert(menuButtonTheme != null),
|
assert(menuButtonTheme != null),
|
||||||
assert(menuTheme != null),
|
assert(menuTheme != null),
|
||||||
assert(navigationBarTheme != null),
|
assert(navigationBarTheme != null),
|
||||||
|
assert(navigationDrawerTheme != null),
|
||||||
assert(navigationRailTheme != null),
|
assert(navigationRailTheme != null),
|
||||||
assert(outlinedButtonTheme != null),
|
assert(outlinedButtonTheme != null),
|
||||||
assert(popupMenuTheme != null),
|
assert(popupMenuTheme != null),
|
||||||
@ -1586,6 +1592,10 @@ class ThemeData with Diagnosticable {
|
|||||||
/// of a [NavigationBar].
|
/// of a [NavigationBar].
|
||||||
final NavigationBarThemeData navigationBarTheme;
|
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
|
/// A theme for customizing the background color, elevation, text style, and
|
||||||
/// icon themes of a [NavigationRail].
|
/// icon themes of a [NavigationRail].
|
||||||
final NavigationRailThemeData navigationRailTheme;
|
final NavigationRailThemeData navigationRailTheme;
|
||||||
@ -1883,6 +1893,7 @@ class ThemeData with Diagnosticable {
|
|||||||
MenuButtonThemeData? menuButtonTheme,
|
MenuButtonThemeData? menuButtonTheme,
|
||||||
MenuThemeData? menuTheme,
|
MenuThemeData? menuTheme,
|
||||||
NavigationBarThemeData? navigationBarTheme,
|
NavigationBarThemeData? navigationBarTheme,
|
||||||
|
NavigationDrawerThemeData? navigationDrawerTheme,
|
||||||
NavigationRailThemeData? navigationRailTheme,
|
NavigationRailThemeData? navigationRailTheme,
|
||||||
OutlinedButtonThemeData? outlinedButtonTheme,
|
OutlinedButtonThemeData? outlinedButtonTheme,
|
||||||
PopupMenuThemeData? popupMenuTheme,
|
PopupMenuThemeData? popupMenuTheme,
|
||||||
@ -2046,6 +2057,7 @@ class ThemeData with Diagnosticable {
|
|||||||
menuButtonTheme: menuButtonTheme ?? this.menuButtonTheme,
|
menuButtonTheme: menuButtonTheme ?? this.menuButtonTheme,
|
||||||
menuTheme: menuTheme ?? this.menuTheme,
|
menuTheme: menuTheme ?? this.menuTheme,
|
||||||
navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
|
navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
|
||||||
|
navigationDrawerTheme: navigationDrawerTheme ?? this.navigationDrawerTheme,
|
||||||
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
|
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
|
||||||
outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme,
|
outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme,
|
||||||
popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme,
|
popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme,
|
||||||
@ -2251,6 +2263,7 @@ class ThemeData with Diagnosticable {
|
|||||||
menuButtonTheme: MenuButtonThemeData.lerp(a.menuButtonTheme, b.menuButtonTheme, t)!,
|
menuButtonTheme: MenuButtonThemeData.lerp(a.menuButtonTheme, b.menuButtonTheme, t)!,
|
||||||
menuTheme: MenuThemeData.lerp(a.menuTheme, b.menuTheme, t)!,
|
menuTheme: MenuThemeData.lerp(a.menuTheme, b.menuTheme, t)!,
|
||||||
navigationBarTheme: NavigationBarThemeData.lerp(a.navigationBarTheme, b.navigationBarTheme, 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)!,
|
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!,
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t)!,
|
outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t)!,
|
||||||
popupMenuTheme: PopupMenuThemeData.lerp(a.popupMenuTheme, b.popupMenuTheme, t)!,
|
popupMenuTheme: PopupMenuThemeData.lerp(a.popupMenuTheme, b.popupMenuTheme, t)!,
|
||||||
@ -2358,6 +2371,7 @@ class ThemeData with Diagnosticable {
|
|||||||
other.menuButtonTheme == menuButtonTheme &&
|
other.menuButtonTheme == menuButtonTheme &&
|
||||||
other.menuTheme == menuTheme &&
|
other.menuTheme == menuTheme &&
|
||||||
other.navigationBarTheme == navigationBarTheme &&
|
other.navigationBarTheme == navigationBarTheme &&
|
||||||
|
other.navigationDrawerTheme == navigationDrawerTheme &&
|
||||||
other.navigationRailTheme == navigationRailTheme &&
|
other.navigationRailTheme == navigationRailTheme &&
|
||||||
other.outlinedButtonTheme == outlinedButtonTheme &&
|
other.outlinedButtonTheme == outlinedButtonTheme &&
|
||||||
other.popupMenuTheme == popupMenuTheme &&
|
other.popupMenuTheme == popupMenuTheme &&
|
||||||
@ -2462,6 +2476,7 @@ class ThemeData with Diagnosticable {
|
|||||||
menuButtonTheme,
|
menuButtonTheme,
|
||||||
menuTheme,
|
menuTheme,
|
||||||
navigationBarTheme,
|
navigationBarTheme,
|
||||||
|
navigationDrawerTheme,
|
||||||
navigationRailTheme,
|
navigationRailTheme,
|
||||||
outlinedButtonTheme,
|
outlinedButtonTheme,
|
||||||
popupMenuTheme,
|
popupMenuTheme,
|
||||||
@ -2568,6 +2583,7 @@ class ThemeData with Diagnosticable {
|
|||||||
properties.add(DiagnosticsProperty<MenuButtonThemeData>('menuButtonTheme', menuButtonTheme, defaultValue: defaultData.menuButtonTheme, level: DiagnosticLevel.debug));
|
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<MenuThemeData>('menuTheme', menuTheme, defaultValue: defaultData.menuTheme, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<NavigationBarThemeData>('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, 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<NavigationRailThemeData>('navigationRailTheme', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<OutlinedButtonThemeData>('outlinedButtonTheme', outlinedButtonTheme, defaultValue: defaultData.outlinedButtonTheme, 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));
|
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).elevation, 16.0);
|
||||||
expect(_drawerMaterial(tester).shadowColor, useMaterial3 ? Colors.transparent : ThemeData().shadowColor);
|
expect(_drawerMaterial(tester).shadowColor, useMaterial3 ? Colors.transparent : ThemeData().shadowColor);
|
||||||
expect(_drawerMaterial(tester).surfaceTintColor, useMaterial3 ? ThemeData().colorScheme.surfaceTint : null);
|
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(_scrim(tester).color, Colors.black54);
|
||||||
expect(_drawerRenderBox(tester).size.width, 304.0);
|
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)),
|
menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
|
||||||
menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.black))),
|
menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.black))),
|
||||||
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black),
|
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black),
|
||||||
|
navigationDrawerTheme: const NavigationDrawerThemeData(backgroundColor: Colors.black),
|
||||||
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
|
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(foregroundColor: Colors.blue)),
|
outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(foregroundColor: Colors.blue)),
|
||||||
popupMenuTheme: const PopupMenuThemeData(color: Colors.black),
|
popupMenuTheme: const PopupMenuThemeData(color: Colors.black),
|
||||||
@ -913,6 +914,7 @@ void main() {
|
|||||||
menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
|
menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
|
||||||
menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.white))),
|
menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.white))),
|
||||||
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white),
|
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white),
|
||||||
|
navigationDrawerTheme: const NavigationDrawerThemeData(backgroundColor: Colors.white),
|
||||||
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
|
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
|
||||||
outlinedButtonTheme: const OutlinedButtonThemeData(),
|
outlinedButtonTheme: const OutlinedButtonThemeData(),
|
||||||
popupMenuTheme: const PopupMenuThemeData(color: Colors.white),
|
popupMenuTheme: const PopupMenuThemeData(color: Colors.white),
|
||||||
@ -1018,6 +1020,7 @@ void main() {
|
|||||||
menuButtonTheme: otherTheme.menuButtonTheme,
|
menuButtonTheme: otherTheme.menuButtonTheme,
|
||||||
menuTheme: otherTheme.menuTheme,
|
menuTheme: otherTheme.menuTheme,
|
||||||
navigationBarTheme: otherTheme.navigationBarTheme,
|
navigationBarTheme: otherTheme.navigationBarTheme,
|
||||||
|
navigationDrawerTheme: otherTheme.navigationDrawerTheme,
|
||||||
navigationRailTheme: otherTheme.navigationRailTheme,
|
navigationRailTheme: otherTheme.navigationRailTheme,
|
||||||
outlinedButtonTheme: otherTheme.outlinedButtonTheme,
|
outlinedButtonTheme: otherTheme.outlinedButtonTheme,
|
||||||
popupMenuTheme: otherTheme.popupMenuTheme,
|
popupMenuTheme: otherTheme.popupMenuTheme,
|
||||||
@ -1260,6 +1263,7 @@ void main() {
|
|||||||
'menuButtonTheme',
|
'menuButtonTheme',
|
||||||
'menuTheme',
|
'menuTheme',
|
||||||
'navigationBarTheme',
|
'navigationBarTheme',
|
||||||
|
'navigationDrawerTheme',
|
||||||
'navigationRailTheme',
|
'navigationRailTheme',
|
||||||
'outlinedButtonTheme',
|
'outlinedButtonTheme',
|
||||||
'popupMenuTheme',
|
'popupMenuTheme',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user