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'; |
9 | library; |
10 | |
11 | import 'dart:ui'; |
12 | |
13 | import 'package:flutter/foundation.dart'; |
14 | import 'package:flutter/widgets.dart'; |
15 | |
16 | import 'color_scheme.dart'; |
17 | import 'colors.dart'; |
18 | import 'elevation_overlay.dart'; |
19 | import 'ink_decoration.dart'; |
20 | import 'ink_well.dart'; |
21 | import 'material.dart'; |
22 | import 'material_localizations.dart'; |
23 | import 'material_state.dart'; |
24 | import 'navigation_bar_theme.dart'; |
25 | import 'text_theme.dart'; |
26 | import 'theme.dart'; |
27 | import 'tooltip.dart'; |
28 | |
29 | const double _kIndicatorHeight = 32; |
30 | const double _kIndicatorWidth = 64; |
31 | const 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> |
97 | class 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. |
342 | enum 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. |
368 | class 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. |
533 | class _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 | |
588 | class _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 | |
625 | class _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)`. |
653 | class _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. |
789 | class 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. |
895 | class _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. |
958 | class _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]. |
998 | class _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. |
1043 | class _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]. |
1078 | class _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. |
1153 | class _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 | // ``` |
1210 | class _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]. |
1255 | class _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. |
1306 | class _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 | |
1323 | class _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 | |
1379 | NavigationBarThemeData _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. |
1386 | class _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 |
1435 | class _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 |
Definitions
- _kIndicatorHeight
- _kIndicatorWidth
- _kMaxLabelTextScaleFactor
- NavigationBar
- NavigationBar
- _handleTap
- build
- NavigationDestinationLabelBehavior
- NavigationDestination
- NavigationDestination
- build
- _NavigationDestinationBuilder
- _NavigationDestinationBuilder
- createState
- _NavigationDestinationBuilderState
- build
- _IndicatorInkWell
- _IndicatorInkWell
- getRectCallback
- _NavigationDestinationInfo
- _NavigationDestinationInfo
- of
- updateShouldNotify
- NavigationIndicator
- NavigationIndicator
- build
- _NavigationBarDestinationLayout
- _NavigationBarDestinationLayout
- build
- _DestinationLayoutAnimationBuilder
- _DestinationLayoutAnimationBuilder
- build
- _NavigationBarDestinationSemantics
- _NavigationBarDestinationSemantics
- build
- _NavigationBarDestinationTooltip
- _NavigationBarDestinationTooltip
- build
- _NavigationDestinationLayoutDelegate
- _NavigationDestinationLayoutDelegate
- performLayout
- halfWidth
- halfHeight
- shouldRelayout
- _StatusTransitionWidgetBuilder
- _StatusTransitionWidgetBuilder
- build
- _SelectableAnimatedBuilder
- _SelectableAnimatedBuilder
- createState
- _SelectableAnimatedBuilderState
- initState
- didUpdateWidget
- dispose
- build
- _CurvedAnimationBuilder
- _CurvedAnimationBuilder
- createState
- _CurvedAnimationBuilderState
- initState
- dispose
- _updateStatus
- build
- _defaultsFor
- _NavigationBarDefaultsM2
- _NavigationBarDefaultsM2
- backgroundColor
- iconTheme
- indicatorColor
- labelTextStyle
- labelPadding
- _NavigationBarDefaultsM3
- _NavigationBarDefaultsM3
- backgroundColor
- shadowColor
- surfaceTintColor
- iconTheme
- indicatorColor
- indicatorShape
- labelTextStyle
Learn more about Flutter for embedded and desktop on industrialflutter.com