1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/services.dart';
6/// @docImport 'bottom_navigation_bar.dart';
7/// @docImport 'navigation_rail.dart';
8/// @docImport 'scaffold.dart';
9library;
10
11import 'dart:ui';
12
13import 'package:flutter/foundation.dart';
14import 'package:flutter/widgets.dart';
15
16import 'color_scheme.dart';
17import 'colors.dart';
18import 'elevation_overlay.dart';
19import 'ink_decoration.dart';
20import 'ink_well.dart';
21import 'material.dart';
22import 'material_localizations.dart';
23import 'material_state.dart';
24import 'navigation_bar_theme.dart';
25import 'text_theme.dart';
26import 'theme.dart';
27import 'tooltip.dart';
28
29const double _kIndicatorHeight = 32;
30const double _kIndicatorWidth = 64;
31const double _kMaxLabelTextScaleFactor = 1.3;
32
33// Examples can assume:
34// late BuildContext context;
35// late bool _isDrawerOpen;
36
37/// Material 3 Navigation Bar component.
38///
39/// {@youtube 560 315 https://www.youtube.com/watch?v=DVGYddFaLv0}
40///
41/// Navigation bars offer a persistent and convenient way to switch between
42/// primary destinations in an app.
43///
44/// This widget does not adjust its size with the [ThemeData.visualDensity].
45///
46/// The [MediaQueryData.textScaler] does not adjust the size of this widget but
47/// rather the size of the [Tooltip]s displayed on long presses of the
48/// destinations.
49///
50/// The style for the icons and text are not affected by parent
51/// [DefaultTextStyle]s or [IconTheme]s but rather controlled by parameters or
52/// the [NavigationBarThemeData].
53///
54/// This widget holds a collection of destinations (usually
55/// [NavigationDestination]s).
56///
57/// {@tool dartpad}
58/// This example shows a [NavigationBar] as it is used within a [Scaffold]
59/// widget. The [NavigationBar] has three [NavigationDestination] widgets and
60/// the initial [selectedIndex] is set to index 0. The [onDestinationSelected]
61/// callback changes the selected item's index and displays a corresponding
62/// widget in the body of the [Scaffold].
63///
64/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.0.dart **
65/// {@end-tool}
66///
67/// {@tool dartpad}
68/// This example showcases [NavigationBar] label behaviors. When tapping on one
69/// of the label behavior options, the [labelBehavior] of the [NavigationBar]
70/// will be updated.
71///
72/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.1.dart **
73/// {@end-tool}
74///
75/// {@tool dartpad}
76/// This example shows a [NavigationBar] within a main [Scaffold]
77/// widget that's used to control the visibility of destination pages.
78/// Each destination has its own scaffold and a nested navigator that
79/// provides local navigation. The example's [NavigationBar] has four
80/// [NavigationDestination] widgets with different color schemes. Its
81/// [onDestinationSelected] callback changes the selected
82/// destination's index and displays a corresponding page with its own
83/// local navigator and scaffold - all within the body of the main
84/// scaffold. The destination pages are organized in a [Stack] and
85/// switching destinations fades out the current page and
86/// fades in the new one. Destinations that aren't visible or animating
87/// are kept [Offstage].
88///
89/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.2.dart **
90/// {@end-tool}
91/// See also:
92///
93/// * [NavigationDestination]
94/// * [BottomNavigationBar]
95/// * <https://api.flutter.dev/flutter/material/NavigationDestination-class.html>
96/// * <https://m3.material.io/components/navigation-bar>
97class NavigationBar extends StatelessWidget {
98 /// Creates a Material 3 Navigation Bar component.
99 ///
100 /// The value of [destinations] must be a list of two or more
101 /// [NavigationDestination] values.
102 // TODO(goderbauer): This class cannot be const constructed, https://github.com/dart-lang/linter/issues/3366.
103 // ignore: prefer_const_constructors_in_immutables
104 NavigationBar({
105 super.key,
106 this.animationDuration,
107 this.selectedIndex = 0,
108 required this.destinations,
109 this.onDestinationSelected,
110 this.backgroundColor,
111 this.elevation,
112 this.shadowColor,
113 this.surfaceTintColor,
114 this.indicatorColor,
115 this.indicatorShape,
116 this.height,
117 this.labelBehavior,
118 this.overlayColor,
119 this.labelTextStyle,
120 this.labelPadding,
121 this.maintainBottomViewPadding = false,
122 }) : assert(destinations.length >= 2),
123 assert(0 <= selectedIndex && selectedIndex < destinations.length);
124
125 /// Determines the transition time for each destination as it goes between
126 /// selected and unselected.
127 final Duration? animationDuration;
128
129 /// Determines which one of the [destinations] is currently selected.
130 ///
131 /// When this is updated, the destination (from [destinations]) at
132 /// [selectedIndex] goes from unselected to selected.
133 final int selectedIndex;
134
135 /// The list of destinations (usually [NavigationDestination]s) in this
136 /// [NavigationBar].
137 ///
138 /// When [selectedIndex] is updated, the destination from this list at
139 /// [selectedIndex] will animate from 0 (unselected) to 1.0 (selected). When
140 /// the animation is increasing or completed, the destination is considered
141 /// selected, when the animation is decreasing or dismissed, the destination
142 /// is considered unselected.
143 final List<Widget> destinations;
144
145 /// Called when one of the [destinations] is selected.
146 ///
147 /// This callback usually updates the int passed to [selectedIndex].
148 ///
149 /// Upon updating [selectedIndex], the [NavigationBar] will be rebuilt.
150 final ValueChanged<int>? onDestinationSelected;
151
152 /// The color of the [NavigationBar] itself.
153 ///
154 /// If null, [NavigationBarThemeData.backgroundColor] is used. If that
155 /// is also null, then if [ThemeData.useMaterial3] is true, the value is
156 /// [ColorScheme.surfaceContainer]. If that is false, the default blends [ColorScheme.surface]
157 /// and [ColorScheme.onSurface] using an [ElevationOverlay].
158 final Color? backgroundColor;
159
160 /// The elevation of the [NavigationBar] itself.
161 ///
162 /// If null, [NavigationBarThemeData.elevation] is used. If that
163 /// is also null, then if [ThemeData.useMaterial3] is true then it will
164 /// be 3.0 otherwise 0.0.
165 final double? elevation;
166
167 /// The color used for the drop shadow to indicate elevation.
168 ///
169 /// If null, [NavigationBarThemeData.shadowColor] is used. If that
170 /// is also null, the default value is [Colors.transparent] which
171 /// indicates that no drop shadow will be displayed.
172 ///
173 /// See [Material.shadowColor] for more details on drop shadows.
174 final Color? shadowColor;
175
176 /// The color used as an overlay on [backgroundColor] to indicate elevation.
177 ///
178 /// This is not recommended for use. [Material 3 spec](https://m3.material.io/styles/color/the-color-system/color-roles)
179 /// introduced a set of tone-based surfaces and surface containers in its [ColorScheme],
180 /// which provide more flexibility. The intention is to eventually remove surface tint color from
181 /// the framework.
182 ///
183 /// If null, [NavigationBarThemeData.surfaceTintColor] is used. If that
184 /// is also null, the default value is [Colors.transparent].
185 ///
186 /// See [Material.surfaceTintColor] for more details on how this
187 /// overlay is applied.
188 final Color? surfaceTintColor;
189
190 /// The color of the [indicatorShape] when this destination is selected.
191 ///
192 /// If null, [NavigationBarThemeData.indicatorColor] is used. If that
193 /// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.secondaryContainer]
194 /// is used. Otherwise, [ColorScheme.secondary] with an opacity of 0.24 is used.
195 final Color? indicatorColor;
196
197 /// The shape of the selected indicator.
198 ///
199 /// If null, [NavigationBarThemeData.indicatorShape] is used. If that
200 /// is also null and [ThemeData.useMaterial3] is true, [StadiumBorder] is used.
201 /// Otherwise, [RoundedRectangleBorder] with a circular border radius of 16 is used.
202 final ShapeBorder? indicatorShape;
203
204 /// The height of the [NavigationBar] itself.
205 ///
206 /// If this is used in [Scaffold.bottomNavigationBar] and the scaffold is
207 /// full-screen, the safe area padding is also added to the height
208 /// automatically.
209 ///
210 /// The height does not adjust with [ThemeData.visualDensity] or
211 /// [MediaQueryData.textScaler] as this component loses usability at
212 /// larger and smaller sizes due to the truncating of labels or smaller tap
213 /// targets.
214 ///
215 /// If null, [NavigationBarThemeData.height] is used. If that
216 /// is also null, the default is 80.
217 final double? height;
218
219 /// Defines how the [destinations]' labels will be laid out and when they'll
220 /// be displayed.
221 ///
222 /// Can be used to show all labels, show only the selected label, or hide all
223 /// labels.
224 ///
225 /// If null, [NavigationBarThemeData.labelBehavior] is used. If that
226 /// is also null, the default is
227 /// [NavigationDestinationLabelBehavior.alwaysShow].
228 final NavigationDestinationLabelBehavior? labelBehavior;
229
230 /// The highlight color that's typically used to indicate that
231 /// the [NavigationDestination] is focused, hovered, or pressed.
232 final MaterialStateProperty<Color?>? overlayColor;
233
234 //// The text style of the label.
235 ///
236 /// If null, [NavigationBarThemeData.labelTextStyle] is used. If that
237 /// is also null, the default text style is [TextTheme.labelMedium] with
238 /// [ColorScheme.onSurface] when the destination is selected, and
239 /// [ColorScheme.onSurfaceVariant] when the destination is unselected, and
240 /// [ColorScheme.onSurfaceVariant] with an opacity of 0.38 when the
241 /// destination is disabled.
242 ///
243 /// If [ThemeData.useMaterial3] is false, then the default text style is
244 /// [TextTheme.labelSmall] with [ColorScheme.onSurface].
245 final MaterialStateProperty<TextStyle?>? labelTextStyle;
246
247 /// The padding around the [NavigationDestination.label] widget.
248 ///
249 /// When [labelPadding] is null, [NavigationBarThemeData.labelPadding]
250 /// is used. If that is also null, the default padding is 4 pixels on
251 /// the top.
252 final EdgeInsetsGeometry? labelPadding;
253
254 /// Specifies whether the underlying [SafeArea] should maintain the bottom
255 /// [MediaQueryData.viewPadding] instead of the bottom [MediaQueryData.padding].
256 ///
257 /// When true, this will prevent the [NavigationBar] from shifting when opening a
258 /// software keyboard due to the change in the padding value, especially when the
259 /// app uses [SystemUiMode.edgeToEdge], which renders the system bars over the
260 /// application instead of outside it.
261 ///
262 /// Defaults to false.
263 ///
264 /// See also:
265 ///
266 /// * [SafeArea.maintainBottomViewPadding], which specifies whether the [SafeArea]
267 /// should maintain the bottom [MediaQueryData.viewPadding].
268 /// * [SystemUiMode.edgeToEdge], which sets a fullscreen display with status and
269 /// navigation elements rendered over the application.
270 final bool maintainBottomViewPadding;
271
272 VoidCallback _handleTap(int index) {
273 return onDestinationSelected != null ? () => onDestinationSelected!(index) : () {};
274 }
275
276 @override
277 Widget build(BuildContext context) {
278 final NavigationBarThemeData defaults = _defaultsFor(context);
279
280 final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
281 final double effectiveHeight = height ?? navigationBarTheme.height ?? defaults.height!;
282 final NavigationDestinationLabelBehavior effectiveLabelBehavior =
283 labelBehavior ?? navigationBarTheme.labelBehavior ?? defaults.labelBehavior!;
284
285 return Material(
286 color: backgroundColor ?? navigationBarTheme.backgroundColor ?? defaults.backgroundColor!,
287 elevation: elevation ?? navigationBarTheme.elevation ?? defaults.elevation!,
288 shadowColor: shadowColor ?? navigationBarTheme.shadowColor ?? defaults.shadowColor,
289 surfaceTintColor:
290 surfaceTintColor ?? navigationBarTheme.surfaceTintColor ?? defaults.surfaceTintColor,
291 child: SafeArea(
292 maintainBottomViewPadding: maintainBottomViewPadding,
293 child: Semantics(
294 role: SemanticsRole.tabBar,
295 explicitChildNodes: true,
296 container: true,
297 child: SizedBox(
298 height: effectiveHeight,
299 child: Row(
300 children: <Widget>[
301 for (int i = 0; i < destinations.length; i++)
302 Expanded(
303 child: MergeSemantics(
304 child: Semantics(
305 role: SemanticsRole.tab,
306 selected: i == selectedIndex,
307 child: _SelectableAnimatedBuilder(
308 duration: animationDuration ?? const Duration(milliseconds: 500),
309 isSelected: i == selectedIndex,
310 builder: (BuildContext context, Animation<double> animation) {
311 return _NavigationDestinationInfo(
312 index: i,
313 selectedIndex: selectedIndex,
314 totalNumberOfDestinations: destinations.length,
315 selectedAnimation: animation,
316 labelBehavior: effectiveLabelBehavior,
317 indicatorColor: indicatorColor,
318 indicatorShape: indicatorShape,
319 overlayColor: overlayColor,
320 onTap: _handleTap(i),
321 labelTextStyle: labelTextStyle,
322 labelPadding: labelPadding,
323 child: destinations[i],
324 );
325 },
326 ),
327 ),
328 ),
329 ),
330 ],
331 ),
332 ),
333 ),
334 ),
335 );
336 }
337}
338
339/// Specifies when each [NavigationDestination]'s label should appear.
340///
341/// This is used to determine the behavior of [NavigationBar]'s destinations.
342enum NavigationDestinationLabelBehavior {
343 /// Always shows all of the labels under each navigation bar destination,
344 /// selected and unselected.
345 alwaysShow,
346
347 /// Never shows any of the labels under the navigation bar destinations,
348 /// regardless of selected vs unselected.
349 alwaysHide,
350
351 /// Only shows the labels of the selected navigation bar destination.
352 ///
353 /// When a destination is unselected, the label will be faded out, and the
354 /// icon will be centered.
355 ///
356 /// When a destination is selected, the label will fade in and the label and
357 /// icon will slide up so that they are both centered.
358 onlyShowSelected,
359}
360
361/// A Material 3 [NavigationBar] destination.
362///
363/// Displays a label below an icon. Use with [NavigationBar.destinations].
364///
365/// See also:
366///
367/// * [NavigationBar], for an interactive code sample.
368class NavigationDestination extends StatelessWidget {
369 /// Creates a navigation bar destination with an icon and a label, to be used
370 /// in the [NavigationBar.destinations].
371 const NavigationDestination({
372 super.key,
373 required this.icon,
374 this.selectedIcon,
375 required this.label,
376 this.tooltip,
377 this.enabled = true,
378 });
379
380 /// The [Widget] (usually an [Icon]) that's displayed for this
381 /// [NavigationDestination].
382 ///
383 /// The icon will use [NavigationBarThemeData.iconTheme]. If this is
384 /// null, the default [IconThemeData] would use a size of 24.0 and
385 /// [ColorScheme.onSurface].
386 final Widget icon;
387
388 /// The optional [Widget] (usually an [Icon]) that's displayed when this
389 /// [NavigationDestination] is selected.
390 ///
391 /// If [selectedIcon] is non-null, the destination will fade from
392 /// [icon] to [selectedIcon] when this destination goes from unselected to
393 /// selected.
394 ///
395 /// The icon will use [NavigationBarThemeData.iconTheme] with
396 /// [WidgetState.selected]. If this is null, the default [IconThemeData]
397 /// would use a size of 24.0 and [ColorScheme.onSurface].
398 final Widget? selectedIcon;
399
400 /// The text label that appears below the icon of this
401 /// [NavigationDestination].
402 ///
403 /// The accompanying [Text] widget will use [NavigationBarThemeData.labelTextStyle].
404 /// If this is null, the default text style will use [TextTheme.labelMedium] with
405 /// [ColorScheme.onSurface] when the destination is selected and
406 /// [ColorScheme.onSurfaceVariant] when the destination is unselected. If
407 /// [ThemeData.useMaterial3] is false, then the default text style will use
408 /// [TextTheme.labelSmall] with [ColorScheme.onSurface].
409 final String label;
410
411 /// The text to display in the tooltip for this [NavigationDestination], when
412 /// the user long presses the destination.
413 ///
414 /// If [tooltip] is an empty string, no tooltip will be used.
415 ///
416 /// Defaults to null, in which case the [label] text will be used.
417 final String? tooltip;
418
419 /// Indicates that this destination is selectable.
420 ///
421 /// Defaults to true.
422 final bool enabled;
423
424 @override
425 Widget build(BuildContext context) {
426 final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
427 const Set<MaterialState> selectedState = <MaterialState>{MaterialState.selected};
428 const Set<MaterialState> unselectedState = <MaterialState>{};
429 const Set<MaterialState> disabledState = <MaterialState>{MaterialState.disabled};
430
431 final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
432 final NavigationBarThemeData defaults = _defaultsFor(context);
433 final Animation<double> animation = info.selectedAnimation;
434
435 return _NavigationDestinationBuilder(
436 label: label,
437 tooltip: tooltip,
438 enabled: enabled,
439 buildIcon: (BuildContext context) {
440 final IconThemeData selectedIconTheme =
441 navigationBarTheme.iconTheme?.resolve(selectedState) ??
442 defaults.iconTheme!.resolve(selectedState)!;
443 final IconThemeData unselectedIconTheme =
444 navigationBarTheme.iconTheme?.resolve(unselectedState) ??
445 defaults.iconTheme!.resolve(unselectedState)!;
446 final IconThemeData disabledIconTheme =
447 navigationBarTheme.iconTheme?.resolve(disabledState) ??
448 defaults.iconTheme!.resolve(disabledState)!;
449
450 final Widget selectedIconWidget = IconTheme.merge(
451 data: enabled ? selectedIconTheme : disabledIconTheme,
452 child: selectedIcon ?? icon,
453 );
454 final Widget unselectedIconWidget = IconTheme.merge(
455 data: enabled ? unselectedIconTheme : disabledIconTheme,
456 child: icon,
457 );
458
459 return Stack(
460 alignment: Alignment.center,
461 children: <Widget>[
462 NavigationIndicator(
463 animation: animation,
464 color:
465 info.indicatorColor ??
466 navigationBarTheme.indicatorColor ??
467 defaults.indicatorColor!,
468 shape:
469 info.indicatorShape ??
470 navigationBarTheme.indicatorShape ??
471 defaults.indicatorShape!,
472 ),
473 _StatusTransitionWidgetBuilder(
474 animation: animation,
475 builder: (BuildContext context, Widget? child) {
476 return animation.isForwardOrCompleted ? selectedIconWidget : unselectedIconWidget;
477 },
478 ),
479 ],
480 );
481 },
482 buildLabel: (BuildContext context) {
483 final TextStyle? effectiveSelectedLabelTextStyle =
484 info.labelTextStyle?.resolve(selectedState) ??
485 navigationBarTheme.labelTextStyle?.resolve(selectedState) ??
486 defaults.labelTextStyle!.resolve(selectedState);
487 final TextStyle? effectiveUnselectedLabelTextStyle =
488 info.labelTextStyle?.resolve(unselectedState) ??
489 navigationBarTheme.labelTextStyle?.resolve(unselectedState) ??
490 defaults.labelTextStyle!.resolve(unselectedState);
491 final TextStyle? effectiveDisabledLabelTextStyle =
492 info.labelTextStyle?.resolve(disabledState) ??
493 navigationBarTheme.labelTextStyle?.resolve(disabledState) ??
494 defaults.labelTextStyle!.resolve(disabledState);
495 final EdgeInsetsGeometry labelPadding =
496 info.labelPadding ?? navigationBarTheme.labelPadding ?? defaults.labelPadding!;
497
498 final TextStyle? textStyle =
499 enabled
500 ? animation.isForwardOrCompleted
501 ? effectiveSelectedLabelTextStyle
502 : effectiveUnselectedLabelTextStyle
503 : effectiveDisabledLabelTextStyle;
504
505 return Padding(
506 padding: labelPadding,
507 child: MediaQuery.withClampedTextScaling(
508 // Set maximum text scale factor to _kMaxLabelTextScaleFactor for the
509 // label to keep the visual hierarchy the same even with larger font
510 // sizes. To opt out, wrap the [label] widget in a [MediaQuery] widget
511 // with a different `TextScaler`.
512 maxScaleFactor: _kMaxLabelTextScaleFactor,
513 child: Text(label, style: textStyle),
514 ),
515 );
516 },
517 );
518 }
519}
520
521/// Widget that handles the semantics and layout of a navigation bar
522/// destination.
523///
524/// Prefer [NavigationDestination] over this widget, as it is a simpler
525/// (although less customizable) way to get navigation bar destinations.
526///
527/// The icon and label of this destination are built with [buildIcon] and
528/// [buildLabel]. They should build the unselected and selected icon and label
529/// according to [_NavigationDestinationInfo.selectedAnimation], where an
530/// animation value of 0 is unselected and 1 is selected.
531///
532/// See [NavigationDestination] for an example.
533class _NavigationDestinationBuilder extends StatefulWidget {
534 /// Builds a destination (icon + label) to use in a Material 3 [NavigationBar].
535 const _NavigationDestinationBuilder({
536 required this.buildIcon,
537 required this.buildLabel,
538 required this.label,
539 this.tooltip,
540 this.enabled = true,
541 });
542
543 /// Builds the icon for a destination in a [NavigationBar].
544 ///
545 /// To animate between unselected and selected, build the icon based on
546 /// [_NavigationDestinationInfo.selectedAnimation]. When the animation is 0,
547 /// the destination is unselected, when the animation is 1, the destination is
548 /// selected.
549 ///
550 /// The destination is considered selected as soon as the animation is
551 /// increasing or completed, and it is considered unselected as soon as the
552 /// animation is decreasing or dismissed.
553 final WidgetBuilder buildIcon;
554
555 /// Builds the label for a destination in a [NavigationBar].
556 ///
557 /// To animate between unselected and selected, build the icon based on
558 /// [_NavigationDestinationInfo.selectedAnimation]. When the animation is
559 /// 0, the destination is unselected, when the animation is 1, the destination
560 /// is selected.
561 ///
562 /// The destination is considered selected as soon as the animation is
563 /// increasing or completed, and it is considered unselected as soon as the
564 /// animation is decreasing or dismissed.
565 final WidgetBuilder buildLabel;
566
567 /// The text value of what is in the label widget, this is required for
568 /// semantics so that screen readers and tooltips can read the proper label.
569 final String label;
570
571 /// The text to display in the tooltip for this [NavigationDestination], when
572 /// the user long presses the destination.
573 ///
574 /// If [tooltip] is an empty string, no tooltip will be used.
575 ///
576 /// Defaults to null, in which case the [label] text will be used.
577 final String? tooltip;
578
579 /// Indicates that this destination is selectable.
580 ///
581 /// Defaults to true.
582 final bool enabled;
583
584 @override
585 State<_NavigationDestinationBuilder> createState() => _NavigationDestinationBuilderState();
586}
587
588class _NavigationDestinationBuilderState extends State<_NavigationDestinationBuilder> {
589 final GlobalKey iconKey = GlobalKey();
590
591 @override
592 Widget build(BuildContext context) {
593 final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
594 final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
595 final NavigationBarThemeData defaults = _defaultsFor(context);
596
597 return _NavigationBarDestinationSemantics(
598 enabled: widget.enabled,
599 child: _NavigationBarDestinationTooltip(
600 message: widget.tooltip ?? widget.label,
601 child: _IndicatorInkWell(
602 iconKey: iconKey,
603 labelBehavior: info.labelBehavior,
604 customBorder:
605 info.indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape,
606 overlayColor: info.overlayColor ?? navigationBarTheme.overlayColor,
607 onTap: widget.enabled ? info.onTap : null,
608 child: Row(
609 children: <Widget>[
610 Expanded(
611 child: _NavigationBarDestinationLayout(
612 icon: widget.buildIcon(context),
613 iconKey: iconKey,
614 label: widget.buildLabel(context),
615 ),
616 ),
617 ],
618 ),
619 ),
620 ),
621 );
622 }
623}
624
625class _IndicatorInkWell extends InkResponse {
626 const _IndicatorInkWell({
627 required this.iconKey,
628 required this.labelBehavior,
629 super.overlayColor,
630 super.customBorder,
631 super.onTap,
632 super.child,
633 }) : super(containedInkWell: true, highlightColor: Colors.transparent);
634
635 final GlobalKey iconKey;
636 final NavigationDestinationLabelBehavior labelBehavior;
637
638 @override
639 RectCallback? getRectCallback(RenderBox referenceBox) {
640 return () {
641 final RenderBox iconBox = iconKey.currentContext!.findRenderObject()! as RenderBox;
642 final Rect iconRect = iconBox.localToGlobal(Offset.zero) & iconBox.size;
643 return referenceBox.globalToLocal(iconRect.topLeft) & iconBox.size;
644 };
645 }
646}
647
648/// Inherited widget for passing data from the [NavigationBar] to the
649/// [NavigationBar.destinations] children widgets.
650///
651/// Useful for building navigation destinations using:
652/// `_NavigationDestinationInfo.of(context)`.
653class _NavigationDestinationInfo extends InheritedWidget {
654 /// Adds the information needed to build a navigation destination to the
655 /// [child] and descendants.
656 const _NavigationDestinationInfo({
657 required this.index,
658 required this.selectedIndex,
659 required this.totalNumberOfDestinations,
660 required this.selectedAnimation,
661 required this.labelBehavior,
662 required this.indicatorColor,
663 required this.indicatorShape,
664 required this.overlayColor,
665 required this.onTap,
666 this.labelTextStyle,
667 this.labelPadding,
668 required super.child,
669 });
670
671 /// Which destination index is this in the navigation bar.
672 ///
673 /// For example:
674 ///
675 /// ```dart
676 /// NavigationBar(
677 /// destinations: const <Widget>[
678 /// NavigationDestination(
679 /// // This is destination index 0.
680 /// icon: Icon(Icons.surfing),
681 /// label: 'Surfing',
682 /// ),
683 /// NavigationDestination(
684 /// // This is destination index 1.
685 /// icon: Icon(Icons.support),
686 /// label: 'Support',
687 /// ),
688 /// NavigationDestination(
689 /// // This is destination index 2.
690 /// icon: Icon(Icons.local_hospital),
691 /// label: 'Hospital',
692 /// ),
693 /// ]
694 /// )
695 /// ```
696 ///
697 /// This is required for semantics, so that each destination can have a label
698 /// "Tab 1 of 3", for example.
699 final int index;
700
701 /// This is the index of the currently selected destination.
702 ///
703 /// This is required for `_IndicatorInkWell` to apply label padding to ripple animations
704 /// when label behavior is [NavigationDestinationLabelBehavior.onlyShowSelected].
705 final int selectedIndex;
706
707 /// How many total destinations are in this navigation bar.
708 ///
709 /// This is required for semantics, so that each destination can have a label
710 /// "Tab 1 of 4", for example.
711 final int totalNumberOfDestinations;
712
713 /// Indicates whether or not this destination is selected, from 0 (unselected)
714 /// to 1 (selected).
715 final Animation<double> selectedAnimation;
716
717 /// Determines the behavior for how the labels will layout.
718 ///
719 /// Can be used to show all labels (the default), show only the selected
720 /// label, or hide all labels.
721 final NavigationDestinationLabelBehavior labelBehavior;
722
723 /// The color of the selection indicator.
724 ///
725 /// This is used by destinations to override the indicator color.
726 final Color? indicatorColor;
727
728 /// The shape of the selection indicator.
729 ///
730 /// This is used by destinations to override the indicator shape.
731 final ShapeBorder? indicatorShape;
732
733 /// The highlight color that's typically used to indicate that
734 /// the [NavigationDestination] is focused, hovered, or pressed.
735 ///
736 /// This is used by destinations to override the overlay color.
737 final MaterialStateProperty<Color?>? overlayColor;
738
739 /// The callback that should be called when this destination is tapped.
740 ///
741 /// This is computed by calling [NavigationBar.onDestinationSelected]
742 /// with [index] passed in.
743 final VoidCallback onTap;
744
745 /// The text style of the label.
746 final MaterialStateProperty<TextStyle?>? labelTextStyle;
747
748 /// The padding around the label.
749 ///
750 /// Defaults to a padding of 4 pixels on the top.
751 final EdgeInsetsGeometry? labelPadding;
752
753 /// Returns a non null [_NavigationDestinationInfo].
754 ///
755 /// This will return an error if called with no [_NavigationDestinationInfo]
756 /// ancestor.
757 ///
758 /// Used by widgets that are implementing a navigation destination info to
759 /// get information like the selected animation and destination number.
760 static _NavigationDestinationInfo of(BuildContext context) {
761 final _NavigationDestinationInfo? result =
762 context.dependOnInheritedWidgetOfExactType<_NavigationDestinationInfo>();
763 assert(
764 result != null,
765 'Navigation destinations need a _NavigationDestinationInfo parent, '
766 'which is usually provided by NavigationBar.',
767 );
768 return result!;
769 }
770
771 @override
772 bool updateShouldNotify(_NavigationDestinationInfo oldWidget) {
773 return index != oldWidget.index ||
774 totalNumberOfDestinations != oldWidget.totalNumberOfDestinations ||
775 selectedAnimation != oldWidget.selectedAnimation ||
776 labelBehavior != oldWidget.labelBehavior ||
777 onTap != oldWidget.onTap;
778 }
779}
780
781/// Selection Indicator for the Material 3 [NavigationBar] and [NavigationRail]
782/// components.
783///
784/// When [animation] is 0, the indicator is not present. As [animation] grows
785/// from 0 to 1, the indicator scales in on the x axis.
786///
787/// Used in a [Stack] widget behind the icons in the Material 3 Navigation Bar
788/// to illuminate the selected destination.
789class NavigationIndicator extends StatelessWidget {
790 /// Builds an indicator, usually used in a stack behind the icon of a
791 /// navigation bar destination.
792 const NavigationIndicator({
793 super.key,
794 required this.animation,
795 this.color,
796 this.width = _kIndicatorWidth,
797 this.height = _kIndicatorHeight,
798 this.borderRadius = const BorderRadius.all(Radius.circular(16)),
799 this.shape,
800 });
801
802 /// Determines the scale of the indicator.
803 ///
804 /// When [animation] is 0, the indicator is not present. The indicator scales
805 /// in as [animation] grows from 0 to 1.
806 final Animation<double> animation;
807
808 /// The fill color of this indicator.
809 ///
810 /// If null, defaults to [ColorScheme.secondary].
811 final Color? color;
812
813 /// The width of this indicator.
814 ///
815 /// Defaults to `64`.
816 final double width;
817
818 /// The height of this indicator.
819 ///
820 /// Defaults to `32`.
821 final double height;
822
823 /// The border radius of the shape of the indicator.
824 ///
825 /// This is used to create a [RoundedRectangleBorder] shape for the indicator.
826 /// This is ignored if [shape] is non-null.
827 ///
828 /// Defaults to `BorderRadius.circular(16)`.
829 final BorderRadius borderRadius;
830
831 /// The shape of the indicator.
832 ///
833 /// If non-null this is used as the shape used to draw the background
834 /// of the indicator. If null then a [RoundedRectangleBorder] with the
835 /// [borderRadius] is used.
836 final ShapeBorder? shape;
837
838 @override
839 Widget build(BuildContext context) {
840 return AnimatedBuilder(
841 animation: animation,
842 builder: (BuildContext context, Widget? child) {
843 // The scale should be 0 when the animation is unselected, as soon as
844 // the animation starts, the scale jumps to 40%, and then animates to
845 // 100% along a curve.
846 final double scale =
847 animation.isDismissed
848 ? 0.0
849 : Tween<double>(begin: .4, end: 1.0).transform(
850 CurveTween(curve: Curves.easeInOutCubicEmphasized).transform(animation.value),
851 );
852
853 return Transform(
854 alignment: Alignment.center,
855 // Scale in the X direction only.
856 transform: Matrix4.diagonal3Values(scale, 1.0, 1.0),
857 child: child,
858 );
859 },
860 // Fade should be a 100ms animation whenever the parent animation changes
861 // direction.
862 child: _StatusTransitionWidgetBuilder(
863 animation: animation,
864 builder: (BuildContext context, Widget? child) {
865 return _SelectableAnimatedBuilder(
866 isSelected: animation.isForwardOrCompleted,
867 duration: const Duration(milliseconds: 100),
868 alwaysDoFullAnimation: true,
869 builder: (BuildContext context, Animation<double> fadeAnimation) {
870 return FadeTransition(
871 opacity: fadeAnimation,
872 child: Ink(
873 width: width,
874 height: height,
875 decoration: ShapeDecoration(
876 shape: shape ?? RoundedRectangleBorder(borderRadius: borderRadius),
877 color: color ?? Theme.of(context).colorScheme.secondary,
878 ),
879 ),
880 );
881 },
882 );
883 },
884 ),
885 );
886 }
887}
888
889/// Widget that handles the layout of the icon + label in a navigation bar
890/// destination, based on [_NavigationDestinationInfo.labelBehavior] and
891/// [_NavigationDestinationInfo.selectedAnimation].
892///
893/// Depending on the [_NavigationDestinationInfo.labelBehavior], the labels
894/// will shift and fade accordingly.
895class _NavigationBarDestinationLayout extends StatelessWidget {
896 /// Builds a widget to layout an icon + label for a destination in a Material
897 /// 3 [NavigationBar].
898 const _NavigationBarDestinationLayout({
899 required this.icon,
900 required this.iconKey,
901 required this.label,
902 });
903
904 /// The icon widget that sits on top of the label.
905 ///
906 /// See [NavigationDestination.icon].
907 final Widget icon;
908
909 /// The global key for the icon of this destination.
910 ///
911 /// This is used to determine the position of the icon.
912 final GlobalKey iconKey;
913
914 /// The label widget that sits below the icon.
915 ///
916 /// This widget will sometimes be faded out, depending on
917 /// [_NavigationDestinationInfo.selectedAnimation].
918 ///
919 /// See [NavigationDestination.label].
920 final Widget label;
921
922 static final Key _labelKey = UniqueKey();
923
924 @override
925 Widget build(BuildContext context) {
926 return _DestinationLayoutAnimationBuilder(
927 builder: (BuildContext context, Animation<double> animation) {
928 return CustomMultiChildLayout(
929 delegate: _NavigationDestinationLayoutDelegate(animation: animation),
930 children: <Widget>[
931 LayoutId(
932 id: _NavigationDestinationLayoutDelegate.iconId,
933 child: RepaintBoundary(key: iconKey, child: icon),
934 ),
935 LayoutId(
936 id: _NavigationDestinationLayoutDelegate.labelId,
937 child: FadeTransition(
938 alwaysIncludeSemantics: true,
939 opacity: animation,
940 child: RepaintBoundary(key: _labelKey, child: label),
941 ),
942 ),
943 ],
944 );
945 },
946 );
947 }
948}
949
950/// Determines the appropriate [Curve] and [Animation] to use for laying out the
951/// [NavigationDestination], based on
952/// [_NavigationDestinationInfo.labelBehavior].
953///
954/// The animation controlling the position and fade of the labels differs
955/// from the selection animation, depending on the
956/// [NavigationDestinationLabelBehavior]. This widget determines what
957/// animation should be used for the position and fade of the labels.
958class _DestinationLayoutAnimationBuilder extends StatelessWidget {
959 /// Builds a child with the appropriate animation [Curve] based on the
960 /// [_NavigationDestinationInfo.labelBehavior].
961 const _DestinationLayoutAnimationBuilder({required this.builder});
962
963 /// Builds the child of this widget.
964 ///
965 /// The [Animation] will be the appropriate [Animation] to use for the layout
966 /// and fade of the [NavigationDestination], either a curve, always
967 /// showing (1), or always hiding (0).
968 final Widget Function(BuildContext, Animation<double>) builder;
969
970 @override
971 Widget build(BuildContext context) {
972 final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
973 switch (info.labelBehavior) {
974 case NavigationDestinationLabelBehavior.alwaysShow:
975 return builder(context, kAlwaysCompleteAnimation);
976 case NavigationDestinationLabelBehavior.alwaysHide:
977 return builder(context, kAlwaysDismissedAnimation);
978 case NavigationDestinationLabelBehavior.onlyShowSelected:
979 return _CurvedAnimationBuilder(
980 animation: info.selectedAnimation,
981 curve: Curves.easeInOutCubicEmphasized,
982 reverseCurve: Curves.easeInOutCubicEmphasized.flipped,
983 builder: builder,
984 );
985 }
986 }
987}
988
989/// Semantics widget for a navigation bar destination.
990///
991/// Requires a [_NavigationDestinationInfo] parent (normally provided by the
992/// [NavigationBar] by default).
993///
994/// Provides localized semantic labels to the destination, for example, it will
995/// read "Home, Tab 1 of 3".
996///
997/// Used by [_NavigationDestinationBuilder].
998class _NavigationBarDestinationSemantics extends StatelessWidget {
999 /// Adds the appropriate semantics for navigation bar destinations to the
1000 /// [child].
1001 const _NavigationBarDestinationSemantics({required this.enabled, required this.child});
1002
1003 /// Whether this widget is enabled.
1004 final bool enabled;
1005
1006 /// The widget that should receive the destination semantics.
1007 final Widget child;
1008
1009 @override
1010 Widget build(BuildContext context) {
1011 final MaterialLocalizations localizations = MaterialLocalizations.of(context);
1012 final _NavigationDestinationInfo destinationInfo = _NavigationDestinationInfo.of(context);
1013 // The AnimationStatusBuilder will make sure that the semantics update to
1014 // "selected" when the animation status changes.
1015 return _StatusTransitionWidgetBuilder(
1016 animation: destinationInfo.selectedAnimation,
1017 builder: (BuildContext context, Widget? child) {
1018 return Semantics(enabled: enabled, button: true, child: child);
1019 },
1020 child:
1021 kIsWeb
1022 ? child
1023 : Stack(
1024 alignment: Alignment.center,
1025 children: <Widget>[
1026 child,
1027 Semantics(
1028 label: localizations.tabLabel(
1029 tabIndex: destinationInfo.index + 1,
1030 tabCount: destinationInfo.totalNumberOfDestinations,
1031 ),
1032 ),
1033 ],
1034 ),
1035 );
1036 }
1037}
1038
1039/// Tooltip widget for use in a [NavigationBar].
1040///
1041/// It appears just above the navigation bar when one of the destinations is
1042/// long pressed.
1043class _NavigationBarDestinationTooltip extends StatelessWidget {
1044 /// Adds a tooltip to the [child] widget.
1045 const _NavigationBarDestinationTooltip({required this.message, required this.child});
1046
1047 /// The text that is rendered in the tooltip when it appears.
1048 final String message;
1049
1050 /// The widget that, when pressed, will show a tooltip.
1051 final Widget child;
1052
1053 @override
1054 Widget build(BuildContext context) {
1055 return Tooltip(
1056 message: message,
1057 // TODO(johnsonmh): Make this value configurable/themable.
1058 verticalOffset: 42,
1059 excludeFromSemantics: true,
1060 preferBelow: false,
1061 child: child,
1062 );
1063 }
1064}
1065
1066/// Custom layout delegate for shifting navigation bar destinations.
1067///
1068/// This will lay out the icon + label according to the [animation].
1069///
1070/// When the [animation] is 0, the icon will be centered, and the label will be
1071/// positioned directly below it.
1072///
1073/// When the [animation] is 1, the label will still be positioned directly below
1074/// the icon, but the icon + label combination will be centered.
1075///
1076/// Used in a [CustomMultiChildLayout] widget in the
1077/// [_NavigationDestinationBuilder].
1078class _NavigationDestinationLayoutDelegate extends MultiChildLayoutDelegate {
1079 _NavigationDestinationLayoutDelegate({required this.animation}) : super(relayout: animation);
1080
1081 /// The selection animation that indicates whether or not this destination is
1082 /// selected.
1083 ///
1084 /// See [_NavigationDestinationInfo.selectedAnimation].
1085 final Animation<double> animation;
1086
1087 /// ID for the icon widget child.
1088 ///
1089 /// This is used by the [LayoutId] when this delegate is used in a
1090 /// [CustomMultiChildLayout].
1091 ///
1092 /// See [_NavigationDestinationBuilder].
1093 static const int iconId = 1;
1094
1095 /// ID for the label widget child.
1096 ///
1097 /// This is used by the [LayoutId] when this delegate is used in a
1098 /// [CustomMultiChildLayout].
1099 ///
1100 /// See [_NavigationDestinationBuilder].
1101 static const int labelId = 2;
1102
1103 @override
1104 void performLayout(Size size) {
1105 double halfWidth(Size size) => size.width / 2;
1106 double halfHeight(Size size) => size.height / 2;
1107
1108 final Size iconSize = layoutChild(iconId, BoxConstraints.loose(size));
1109 final Size labelSize = layoutChild(labelId, BoxConstraints.loose(size));
1110
1111 final double yPositionOffset = Tween<double>(
1112 // When unselected, the icon is centered vertically.
1113 begin: halfHeight(iconSize),
1114 // When selected, the icon and label are centered vertically.
1115 end: halfHeight(iconSize) + halfHeight(labelSize),
1116 ).transform(animation.value);
1117 final double iconYPosition = halfHeight(size) - yPositionOffset;
1118
1119 // Position the icon.
1120 positionChild(
1121 iconId,
1122 Offset(
1123 // Center the icon horizontally.
1124 halfWidth(size) - halfWidth(iconSize),
1125 iconYPosition,
1126 ),
1127 );
1128
1129 // Position the label.
1130 positionChild(
1131 labelId,
1132 Offset(
1133 // Center the label horizontally.
1134 halfWidth(size) - halfWidth(labelSize),
1135 // Label always appears directly below the icon.
1136 iconYPosition + iconSize.height,
1137 ),
1138 );
1139 }
1140
1141 @override
1142 bool shouldRelayout(_NavigationDestinationLayoutDelegate oldDelegate) {
1143 return oldDelegate.animation != animation;
1144 }
1145}
1146
1147/// Widget that listens to an animation, and rebuilds when the animation changes
1148/// [AnimationStatus].
1149///
1150/// This can be more efficient than just using an [AnimatedBuilder] when you
1151/// only need to rebuild when the [Animation.status] changes, since
1152/// [AnimatedBuilder] rebuilds every time the animation ticks.
1153class _StatusTransitionWidgetBuilder extends StatusTransitionWidget {
1154 /// Creates a widget that rebuilds when the given animation changes status.
1155 const _StatusTransitionWidgetBuilder({
1156 required super.animation,
1157 required this.builder,
1158 this.child,
1159 });
1160
1161 /// Called every time the [animation] changes [AnimationStatus].
1162 final TransitionBuilder builder;
1163
1164 /// The child widget to pass to the [builder].
1165 ///
1166 /// If a [builder] callback's return value contains a subtree that does not
1167 /// depend on the animation, it's more efficient to build that subtree once
1168 /// instead of rebuilding it on every animation status change.
1169 ///
1170 /// Using this pre-built child is entirely optional, but can improve
1171 /// performance in some cases and is therefore a good practice.
1172 ///
1173 /// See: [AnimatedBuilder.child]
1174 final Widget? child;
1175
1176 @override
1177 Widget build(BuildContext context) => builder(context, child);
1178}
1179
1180// Builder widget for widgets that need to be animated from 0 (unselected) to
1181// 1.0 (selected).
1182//
1183// This widget creates and manages an [AnimationController] that it passes down
1184// to the child through the [builder] function.
1185//
1186// When [isSelected] is `true`, the animation controller will animate from
1187// 0 to 1 (for [duration] time).
1188//
1189// When [isSelected] is `false`, the animation controller will animate from
1190// 1 to 0 (for [duration] time).
1191//
1192// If [isSelected] is updated while the widget is animating, the animation will
1193// be reversed until it is either 0 or 1 again. If [alwaysDoFullAnimation] is
1194// true, the animation will reset to 0 or 1 before beginning the animation, so
1195// that the full animation is done.
1196//
1197// Usage:
1198// ```dart
1199// _SelectableAnimatedBuilder(
1200// isSelected: _isDrawerOpen,
1201// builder: (context, animation) {
1202// return AnimatedIcon(
1203// icon: AnimatedIcons.menu_arrow,
1204// progress: animation,
1205// semanticLabel: 'Show menu',
1206// );
1207// }
1208// )
1209// ```
1210class _SelectableAnimatedBuilder extends StatefulWidget {
1211 /// Builds and maintains an [AnimationController] that will animate from 0 to
1212 /// 1 and back depending on when [isSelected] is true.
1213 const _SelectableAnimatedBuilder({
1214 required this.isSelected,
1215 this.duration = const Duration(milliseconds: 200),
1216 this.alwaysDoFullAnimation = false,
1217 required this.builder,
1218 });
1219
1220 /// When true, the widget will animate an animation controller from 0 to 1.
1221 ///
1222 /// The animation controller is passed to the child widget through [builder].
1223 final bool isSelected;
1224
1225 /// How long the animation controller should animate for when [isSelected] is
1226 /// updated.
1227 ///
1228 /// If the animation is currently running and [isSelected] is updated, only
1229 /// the [duration] left to finish the animation will be run.
1230 final Duration duration;
1231
1232 /// If true, the animation will always go all the way from 0 to 1 when
1233 /// [isSelected] is true, and from 1 to 0 when [isSelected] is false, even
1234 /// when the status changes mid animation.
1235 ///
1236 /// If this is false and the status changes mid animation, the animation will
1237 /// reverse direction from it's current point.
1238 ///
1239 /// Defaults to false.
1240 final bool alwaysDoFullAnimation;
1241
1242 /// Builds the child widget based on the current animation status.
1243 ///
1244 /// When [isSelected] is updated to true, this builder will be called and the
1245 /// animation will animate up to 1. When [isSelected] is updated to
1246 /// `false`, this will be called and the animation will animate down to 0.
1247 final Widget Function(BuildContext, Animation<double>) builder;
1248
1249 @override
1250 _SelectableAnimatedBuilderState createState() => _SelectableAnimatedBuilderState();
1251}
1252
1253/// State that manages the [AnimationController] that is passed to
1254/// [_SelectableAnimatedBuilder.builder].
1255class _SelectableAnimatedBuilderState extends State<_SelectableAnimatedBuilder>
1256 with SingleTickerProviderStateMixin {
1257 late AnimationController _controller;
1258
1259 @override
1260 void initState() {
1261 super.initState();
1262 _controller = AnimationController(vsync: this);
1263 _controller.duration = widget.duration;
1264 _controller.value = widget.isSelected ? 1.0 : 0.0;
1265 }
1266
1267 @override
1268 void didUpdateWidget(_SelectableAnimatedBuilder oldWidget) {
1269 super.didUpdateWidget(oldWidget);
1270 if (oldWidget.duration != widget.duration) {
1271 _controller.duration = widget.duration;
1272 }
1273 if (oldWidget.isSelected != widget.isSelected) {
1274 if (widget.isSelected) {
1275 _controller.forward(from: widget.alwaysDoFullAnimation ? 0 : null);
1276 } else {
1277 _controller.reverse(from: widget.alwaysDoFullAnimation ? 1 : null);
1278 }
1279 }
1280 }
1281
1282 @override
1283 void dispose() {
1284 _controller.dispose();
1285 super.dispose();
1286 }
1287
1288 @override
1289 Widget build(BuildContext context) {
1290 return widget.builder(context, _controller);
1291 }
1292}
1293
1294/// Watches [animation] and calls [builder] with the appropriate [Curve]
1295/// depending on the direction of the [animation] status.
1296///
1297/// If [Animation.status] is forward or complete, [curve] is used. If
1298/// [Animation.status] is reverse or dismissed, [reverseCurve] is used.
1299///
1300/// If the [animation] changes direction while it is already running, the curve
1301/// used will not change, this will keep the animations smooth until it
1302/// completes.
1303///
1304/// This is similar to [CurvedAnimation] except the animation status listeners
1305/// are removed when this widget is disposed.
1306class _CurvedAnimationBuilder extends StatefulWidget {
1307 const _CurvedAnimationBuilder({
1308 required this.animation,
1309 required this.curve,
1310 required this.reverseCurve,
1311 required this.builder,
1312 });
1313
1314 final Animation<double> animation;
1315 final Curve curve;
1316 final Curve reverseCurve;
1317 final Widget Function(BuildContext, Animation<double>) builder;
1318
1319 @override
1320 _CurvedAnimationBuilderState createState() => _CurvedAnimationBuilderState();
1321}
1322
1323class _CurvedAnimationBuilderState extends State<_CurvedAnimationBuilder> {
1324 late AnimationStatus _animationDirection;
1325 AnimationStatus? _preservedDirection;
1326
1327 @override
1328 void initState() {
1329 super.initState();
1330 _animationDirection = widget.animation.status;
1331 _updateStatus(widget.animation.status);
1332 widget.animation.addStatusListener(_updateStatus);
1333 }
1334
1335 @override
1336 void dispose() {
1337 widget.animation.removeStatusListener(_updateStatus);
1338 super.dispose();
1339 }
1340
1341 // Keeps track of the current animation status, as well as the "preserved
1342 // direction" when the animation changes direction mid animation.
1343 //
1344 // The preserved direction is reset when the animation finishes in either
1345 // direction.
1346 void _updateStatus(AnimationStatus status) {
1347 if (_animationDirection != status) {
1348 setState(() {
1349 _animationDirection = status;
1350 });
1351 }
1352 switch (status) {
1353 case AnimationStatus.forward || AnimationStatus.reverse when _preservedDirection != null:
1354 break;
1355 case AnimationStatus.forward || AnimationStatus.reverse:
1356 setState(() {
1357 _preservedDirection = status;
1358 });
1359 case AnimationStatus.completed || AnimationStatus.dismissed:
1360 setState(() {
1361 _preservedDirection = null;
1362 });
1363 }
1364 }
1365
1366 @override
1367 Widget build(BuildContext context) {
1368 final bool shouldUseForwardCurve =
1369 (_preservedDirection ?? _animationDirection) != AnimationStatus.reverse;
1370
1371 final Animation<double> curvedAnimation = CurveTween(
1372 curve: shouldUseForwardCurve ? widget.curve : widget.reverseCurve,
1373 ).animate(widget.animation);
1374
1375 return widget.builder(context, curvedAnimation);
1376 }
1377}
1378
1379NavigationBarThemeData _defaultsFor(BuildContext context) {
1380 return Theme.of(context).useMaterial3
1381 ? _NavigationBarDefaultsM3(context)
1382 : _NavigationBarDefaultsM2(context);
1383}
1384
1385// Hand coded defaults based on Material Design 2.
1386class _NavigationBarDefaultsM2 extends NavigationBarThemeData {
1387 _NavigationBarDefaultsM2(BuildContext context)
1388 : _theme = Theme.of(context),
1389 _colors = Theme.of(context).colorScheme,
1390 super(
1391 height: 80.0,
1392 elevation: 0.0,
1393 indicatorShape: const RoundedRectangleBorder(
1394 borderRadius: BorderRadius.all(Radius.circular(16)),
1395 ),
1396 labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
1397 );
1398
1399 final ThemeData _theme;
1400 final ColorScheme _colors;
1401
1402 // With Material 2, the NavigationBar uses an overlay blend for the
1403 // default color regardless of light/dark mode.
1404 @override
1405 Color? get backgroundColor =>
1406 ElevationOverlay.colorWithOverlay(_colors.surface, _colors.onSurface, 3.0);
1407
1408 @override
1409 MaterialStateProperty<IconThemeData?>? get iconTheme {
1410 return MaterialStatePropertyAll<IconThemeData>(
1411 IconThemeData(size: 24, color: _colors.onSurface),
1412 );
1413 }
1414
1415 @override
1416 Color? get indicatorColor => _colors.secondary.withOpacity(0.24);
1417
1418 @override
1419 MaterialStateProperty<TextStyle?>? get labelTextStyle => MaterialStatePropertyAll<TextStyle?>(
1420 _theme.textTheme.labelSmall!.copyWith(color: _colors.onSurface),
1421 );
1422
1423 @override
1424 EdgeInsetsGeometry? get labelPadding => const EdgeInsets.only(top: 4);
1425}
1426
1427// BEGIN GENERATED TOKEN PROPERTIES - NavigationBar
1428
1429// Do not edit by hand. The code between the "BEGIN GENERATED" and
1430// "END GENERATED" comments are generated from data in the Material
1431// Design token database by the script:
1432// dev/tools/gen_defaults/bin/gen_defaults.dart.
1433
1434// dart format off
1435class _NavigationBarDefaultsM3 extends NavigationBarThemeData {
1436 _NavigationBarDefaultsM3(this.context)
1437 : super(
1438 height: 80.0,
1439 elevation: 3.0,
1440 labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
1441 );
1442
1443 final BuildContext context;
1444 late final ColorScheme _colors = Theme.of(context).colorScheme;
1445 late final TextTheme _textTheme = Theme.of(context).textTheme;
1446
1447 @override
1448 Color? get backgroundColor => _colors.surfaceContainer;
1449
1450 @override
1451 Color? get shadowColor => Colors.transparent;
1452
1453 @override
1454 Color? get surfaceTintColor => Colors.transparent;
1455
1456 @override
1457 MaterialStateProperty<IconThemeData?>? get iconTheme {
1458 return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
1459 return IconThemeData(
1460 size: 24.0,
1461 color: states.contains(MaterialState.disabled)
1462 ? _colors.onSurfaceVariant.withOpacity(0.38)
1463 : states.contains(MaterialState.selected)
1464 ? _colors.onSecondaryContainer
1465 : _colors.onSurfaceVariant,
1466 );
1467 });
1468 }
1469
1470 @override
1471 Color? get indicatorColor => _colors.secondaryContainer;
1472
1473 @override
1474 ShapeBorder? get indicatorShape => const StadiumBorder();
1475
1476 @override
1477 MaterialStateProperty<TextStyle?>? get labelTextStyle {
1478 return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
1479 final TextStyle style = _textTheme.labelMedium!;
1480 return style.apply(
1481 color: states.contains(MaterialState.disabled)
1482 ? _colors.onSurfaceVariant.withOpacity(0.38)
1483 : states.contains(MaterialState.selected)
1484 ? _colors.onSurface
1485 : _colors.onSurfaceVariant
1486 );
1487 });
1488 }
1489
1490 @override
1491 EdgeInsetsGeometry? get labelPadding => const EdgeInsets.only(top: 4);
1492}
1493// dart format on
1494
1495// END GENERATED TOKEN PROPERTIES - NavigationBar
1496

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com