Line data Source code
1 : import 'package:flutter/gestures.dart';
2 : import 'package:flutter/material.dart';
3 : import 'package:flutter/services.dart';
4 :
5 : import '../destination.dart';
6 : import '../navigation_controller.dart';
7 : import 'index.dart';
8 :
9 : /// A [NavigatorBuilder] that allows to switch between destinations using
10 : /// [TabBar].
11 : ///
12 : /// It builds a wrapper widget, which is a [Scaffold] with a [Scaffold.appbar] set
13 : /// to the [TabBar] with provided [tabs], and a [Scaffold.body] set to the [TabBarView],
14 : /// which display a content of corresponding destination.
15 : ///
16 : /// The [tabs] must correspond to the navigator's destinations.
17 : ///
18 : /// The tab bar can be customized using [parameters], which includes all parameters
19 : /// supported by the [TabBar] widget.
20 : ///
21 : /// See also:
22 : /// - [NavigatorBuilder]
23 : /// - [AppBarParameters]
24 : /// - [TabBarParameters]
25 : /// - [NavigationController]
26 : /// - [TabBar]
27 : ///
28 : class TabsNavigationBuilder extends NavigatorBuilder {
29 : /// Creates a [TabsNavigationBuilder] instance.
30 : ///
31 0 : const TabsNavigationBuilder({
32 : required this.tabs,
33 : this.parameters = const TabBarParameters(),
34 : this.appBarParametersBuilder,
35 : this.wrapInScaffold = true,
36 0 : }) : super();
37 :
38 : /// Typically a list of [Tab] widgets, that corresponds to the navigator's
39 : /// destination list.
40 : ///
41 : /// The list must contain the same number of widgets, following with the same order
42 : /// as a destination list specified for the navigator.
43 : ///
44 : final List<Widget> tabs;
45 :
46 : /// A set of [TabBar] parameters.
47 : ///
48 : /// Contains all supported parameters to customize [TabBar] widget.
49 : /// Doesn't include 'tabs', 'onTap' and 'controller', which are managed by
50 : /// [TabsNavigationBuilder].
51 : ///
52 : final TabBarParameters parameters;
53 :
54 : /// Return an instance of [AppBarParameters] for provided destination.
55 : ///
56 : /// Once this builder is specified, the navigation [TabBar] will appear as part of
57 : /// [AppBar] widget.
58 : /// When this function is called, the [destination] parameter is set to current
59 : /// destination (selected tab).
60 : ///
61 : /// The function should return an instance of [AppBarParameters],
62 : /// which is a set of all parameters available in the [AppBar] widget.
63 : /// So the app bar widget con be made to match the current destination.
64 : /// For example, you can set a title and actions, depending on the current destination.
65 : ///
66 : final AppBarParameters Function(
67 : BuildContext context, Destination destination)? appBarParametersBuilder;
68 :
69 : /// Controls if the [Scaffold] widget should be used around the tab bar and tab's content.
70 : ///
71 : /// This might be needed if you are using tabs as a top level navigation in your app.
72 : /// Defaults to 'true'.
73 : ///
74 : final bool wrapInScaffold;
75 :
76 0 : @override
77 : Widget build(BuildContext context, NavigationController navigator) {
78 0 : final currentDestination = navigator.currentDestination;
79 0 : return _TabsNavigationWrapper(
80 0 : tabs: tabs,
81 : // TODO: This implementation doesn't respect the possible parameters of destinations (excluding current destination).
82 : // How this could be resolved?
83 0 : tabDestination: (tabIndex) => navigator.destinations[tabIndex],
84 0 : onTabSelected: (index) => navigator.goTo(navigator.destinations[index]),
85 0 : selectedIndex: navigator.destinations.indexOf(currentDestination),
86 0 : parameters: parameters,
87 : appBarParameters:
88 0 : appBarParametersBuilder?.call(context, currentDestination),
89 0 : wrapInScaffold: wrapInScaffold,
90 : );
91 : }
92 : }
93 :
94 : class _TabsNavigationWrapper extends StatefulWidget {
95 0 : const _TabsNavigationWrapper({
96 : Key? key,
97 : required this.tabs,
98 : required this.tabDestination,
99 : required this.onTabSelected,
100 : required this.selectedIndex,
101 : required this.parameters,
102 : this.appBarParameters,
103 : this.wrapInScaffold = false,
104 0 : }) : super(key: key);
105 :
106 : final List<Widget> tabs;
107 :
108 : final Destination Function(int tabIndex) tabDestination;
109 :
110 : final void Function(int index) onTabSelected;
111 :
112 : final int selectedIndex;
113 :
114 : final TabBarParameters parameters;
115 :
116 : final AppBarParameters? appBarParameters;
117 :
118 : final bool wrapInScaffold;
119 :
120 0 : @override
121 0 : _TabsNavigationWrapperState createState() => _TabsNavigationWrapperState();
122 : }
123 :
124 : class _TabsNavigationWrapperState extends State<_TabsNavigationWrapper>
125 : with TickerProviderStateMixin {
126 : final _content = <Widget>[];
127 :
128 : late final TabController _controller;
129 :
130 0 : @override
131 : void initState() {
132 0 : super.initState();
133 0 : _content.addAll(List<Widget>.generate(
134 0 : widget.tabs.length,
135 0 : (index) => _TabDestinationContentWrapper(
136 0 : keepState: widget.tabDestination(index).isFinalDestination,
137 0 : child: widget.tabDestination(index).build(context),
138 : )));
139 0 : _controller = TabController(length: widget.tabs.length, vsync: this);
140 0 : _controller.addListener(_onTabChanged);
141 0 : _controller.animateTo(widget.selectedIndex);
142 : }
143 :
144 0 : @override
145 : void didUpdateWidget(_TabsNavigationWrapper oldWidget) {
146 0 : super.didUpdateWidget(oldWidget);
147 0 : if (oldWidget.selectedIndex != widget.selectedIndex) {
148 0 : _controller.animateTo(widget.selectedIndex);
149 : }
150 : }
151 :
152 0 : @override
153 : void dispose() {
154 0 : _controller.dispose();
155 0 : super.dispose();
156 : }
157 :
158 0 : @override
159 : Widget build(BuildContext context) {
160 0 : final tabBar = TabBar(
161 0 : controller: _controller,
162 0 : tabs: widget.tabs,
163 0 : onTap: (value) => widget.onTabSelected(value),
164 0 : isScrollable: widget.parameters.isScrollable,
165 0 : padding: widget.parameters.padding,
166 0 : indicatorColor: widget.parameters.indicatorColor,
167 : automaticIndicatorColorAdjustment:
168 0 : widget.parameters.automaticIndicatorColorAdjustment,
169 0 : indicatorWeight: widget.parameters.indicatorWeight,
170 0 : indicatorPadding: widget.parameters.indicatorPadding,
171 0 : indicator: widget.parameters.indicator,
172 0 : indicatorSize: widget.parameters.indicatorSize,
173 0 : labelColor: widget.parameters.labelColor,
174 0 : labelStyle: widget.parameters.labelStyle,
175 0 : labelPadding: widget.parameters.labelPadding,
176 0 : unselectedLabelColor: widget.parameters.unselectedLabelColor,
177 0 : unselectedLabelStyle: widget.parameters.unselectedLabelStyle,
178 0 : dragStartBehavior: widget.parameters.dragStartBehavior,
179 0 : overlayColor: widget.parameters.overlayColor,
180 0 : mouseCursor: widget.parameters.mouseCursor,
181 0 : enableFeedback: widget.parameters.enableFeedback,
182 0 : physics: widget.parameters.physics,
183 0 : splashFactory: widget.parameters.splashFactory,
184 0 : splashBorderRadius: widget.parameters.splashBorderRadius,
185 : );
186 0 : final tabBarView = TabBarView(
187 0 : controller: _controller,
188 0 : children: _content,
189 : );
190 : Widget result;
191 0 : if (widget.appBarParameters != null) {
192 0 : final appBar = AppBar(
193 : bottom: tabBar,
194 0 : leading: widget.appBarParameters?.leading,
195 : automaticallyImplyLeading:
196 0 : widget.appBarParameters?.automaticallyImplyLeading ?? true,
197 0 : title: widget.appBarParameters?.title,
198 0 : actions: widget.appBarParameters?.actions,
199 0 : flexibleSpace: widget.appBarParameters?.flexibleSpace,
200 0 : elevation: widget.appBarParameters?.elevation,
201 0 : scrolledUnderElevation: widget.appBarParameters?.scrolledUnderElevation,
202 0 : shadowColor: widget.appBarParameters?.shadowColor,
203 0 : surfaceTintColor: widget.appBarParameters?.surfaceTintColor,
204 0 : shape: widget.appBarParameters?.shape,
205 0 : backgroundColor: widget.appBarParameters?.backgroundColor,
206 0 : foregroundColor: widget.appBarParameters?.foregroundColor,
207 0 : iconTheme: widget.appBarParameters?.iconTheme,
208 0 : actionsIconTheme: widget.appBarParameters?.actionsIconTheme,
209 0 : primary: widget.appBarParameters?.primary ?? true,
210 0 : centerTitle: widget.appBarParameters?.centerTitle,
211 : excludeHeaderSemantics:
212 0 : widget.appBarParameters?.excludeHeaderSemantics ?? false,
213 0 : titleSpacing: widget.appBarParameters?.titleSpacing,
214 0 : toolbarOpacity: widget.appBarParameters?.toolbarOpacity ?? 1.0,
215 0 : bottomOpacity: widget.appBarParameters?.bottomOpacity ?? 1.0,
216 0 : toolbarHeight: widget.appBarParameters?.toolbarHeight,
217 0 : leadingWidth: widget.appBarParameters?.leadingWidth,
218 0 : toolbarTextStyle: widget.appBarParameters?.toolbarTextStyle,
219 0 : titleTextStyle: widget.appBarParameters?.titleTextStyle,
220 0 : systemOverlayStyle: widget.appBarParameters?.systemOverlayStyle,
221 : );
222 0 : if (widget.wrapInScaffold) {
223 0 : result = Scaffold(
224 : appBar: appBar,
225 : body: tabBarView,
226 : );
227 : } else {
228 0 : result = Column(
229 0 : children: [
230 : appBar,
231 0 : Expanded(
232 : child: tabBarView,
233 : ),
234 : ],
235 : );
236 : }
237 : } else {
238 0 : result = Column(
239 0 : children: [
240 : tabBar,
241 0 : Expanded(
242 : child: tabBarView,
243 : ),
244 : ],
245 : );
246 0 : if (widget.wrapInScaffold) {
247 0 : result = Scaffold(
248 : body: result,
249 : );
250 : }
251 : }
252 : return result;
253 : }
254 :
255 0 : void _onTabChanged() {
256 0 : widget.onTabSelected(_controller.index);
257 : }
258 : }
259 :
260 : class _TabDestinationContentWrapper extends StatefulWidget {
261 0 : const _TabDestinationContentWrapper({
262 : Key? key,
263 : required this.keepState,
264 : required this.child,
265 0 : }) : super(key: key);
266 :
267 : final bool keepState;
268 :
269 : final Widget child;
270 :
271 0 : @override
272 : State<_TabDestinationContentWrapper> createState() =>
273 0 : _TabDestinationContentWrapperState();
274 : }
275 :
276 : class _TabDestinationContentWrapperState
277 : extends State<_TabDestinationContentWrapper>
278 : with AutomaticKeepAliveClientMixin<_TabDestinationContentWrapper> {
279 0 : @override
280 0 : bool get wantKeepAlive => widget.keepState;
281 :
282 0 : @override
283 : Widget build(BuildContext context) {
284 0 : super.build(context);
285 0 : return widget.child;
286 : }
287 : }
288 :
289 : /// Contains parameters to customize the [TabBar].
290 : ///
291 : /// It includes all the same arguments as the [TabBar()], excepting
292 : /// the 'tabs', 'onTap' and 'controller', which are managed by the [TabsNavigationBuilder].
293 : ///
294 : /// See also:
295 : /// - [TabsNavigationBuilder]
296 : /// - [TabBar]
297 : ///
298 : class TabBarParameters {
299 : /// Create a [TabBarParameters] instance.
300 : ///
301 11 : const TabBarParameters({
302 : this.isScrollable = false,
303 : this.padding,
304 : this.indicatorColor,
305 : this.automaticIndicatorColorAdjustment = true,
306 : this.indicatorWeight = 2.0,
307 : this.indicatorPadding = EdgeInsets.zero,
308 : this.indicator,
309 : this.indicatorSize,
310 : this.labelColor,
311 : this.labelStyle,
312 : this.labelPadding,
313 : this.unselectedLabelColor,
314 : this.unselectedLabelStyle,
315 : this.dragStartBehavior = DragStartBehavior.start,
316 : this.overlayColor,
317 : this.mouseCursor,
318 : this.enableFeedback,
319 : this.physics,
320 : this.splashFactory,
321 : this.splashBorderRadius,
322 : });
323 :
324 : /// [TabBar.isScrollable]
325 : ///
326 : final bool isScrollable;
327 :
328 : /// [TabBar.padding]
329 : ///
330 : final EdgeInsetsGeometry? padding;
331 :
332 : /// [TabBar.indicatorColor]
333 : ///
334 : final Color? indicatorColor;
335 :
336 : /// [TabBar.automaticIndicatorColorAdjustment]
337 : ///
338 : final bool automaticIndicatorColorAdjustment;
339 :
340 : /// [TabBar.indicatorWeight]
341 : ///
342 : final double indicatorWeight;
343 :
344 : /// [TabBar.indicatorPadding]
345 : ///
346 : final EdgeInsetsGeometry indicatorPadding;
347 :
348 : /// [TabBar.indicator]
349 : ///
350 : final Decoration? indicator;
351 :
352 : /// [TabBar.indicatorSize]
353 : ///
354 : final TabBarIndicatorSize? indicatorSize;
355 :
356 : /// [TabBar.labelColor]
357 : ///
358 : final Color? labelColor;
359 :
360 : /// [TabBar.labelStyle]
361 : ///
362 : final TextStyle? labelStyle;
363 :
364 : /// [TabBar.labelPadding]
365 : ///
366 : final EdgeInsetsGeometry? labelPadding;
367 :
368 : /// [TabBar.unselectedLabelColor]
369 : ///
370 : final Color? unselectedLabelColor;
371 :
372 : /// [TabBar.unselectedLabelStyle]
373 : ///
374 : final TextStyle? unselectedLabelStyle;
375 :
376 : /// [TabBar.dragStartBehavior]
377 : ///
378 : final DragStartBehavior dragStartBehavior;
379 :
380 : /// [TabBar.overlayColor]
381 : ///
382 : final MaterialStateProperty<Color?>? overlayColor;
383 :
384 : /// [TabBar.mouseCursor]
385 : ///
386 : final MouseCursor? mouseCursor;
387 :
388 : /// [TabBar.enableFeedback]
389 : ///
390 : final bool? enableFeedback;
391 :
392 : /// [TabBar.physics]
393 : ///
394 : final ScrollPhysics? physics;
395 :
396 : /// [TabBar.splashFactory]
397 : ///
398 : final InteractiveInkFeatureFactory? splashFactory;
399 :
400 : /// [TabBar.splashBorderRadius]
401 : ///
402 : final BorderRadius? splashBorderRadius;
403 : }
404 :
405 : /// Contains parameters to customize the [AppBar].
406 : ///
407 : /// It includes all the same arguments as the [AppBar()], excepting
408 : /// the 'bottom' which is managed by the [TabsNavigationBuilder].
409 : ///
410 : /// See also:
411 : /// - [TabsNavigationBuilder]
412 : /// - [AppBar]
413 : ///
414 : class AppBarParameters {
415 : /// Create a [AppBarParameters] instance.
416 : ///
417 0 : const AppBarParameters({
418 : this.leading,
419 : this.automaticallyImplyLeading = true,
420 : this.title,
421 : this.actions,
422 : this.flexibleSpace,
423 : this.elevation,
424 : this.scrolledUnderElevation,
425 : this.shadowColor,
426 : this.surfaceTintColor,
427 : this.shape,
428 : this.backgroundColor,
429 : this.foregroundColor,
430 : this.iconTheme,
431 : this.actionsIconTheme,
432 : this.primary = true,
433 : this.centerTitle,
434 : this.excludeHeaderSemantics = false,
435 : this.titleSpacing,
436 : this.toolbarOpacity = 1.0,
437 : this.bottomOpacity = 1.0,
438 : this.toolbarHeight,
439 : this.leadingWidth,
440 : this.toolbarTextStyle,
441 : this.titleTextStyle,
442 : this.systemOverlayStyle,
443 : });
444 :
445 : /// [AppBar.leading]
446 : ///
447 : final Widget? leading;
448 :
449 : /// [AppBar.leading]
450 : ///
451 : final bool automaticallyImplyLeading;
452 :
453 : /// [AppBar.title]
454 : ///
455 : final Widget? title;
456 :
457 : /// [AppBar.actions]
458 : ///
459 : final List<Widget>? actions;
460 :
461 : /// [AppBar.flexibleSpace]
462 : ///
463 : final Widget? flexibleSpace;
464 :
465 : /// [AppBar.elevation]
466 : ///
467 : final double? elevation;
468 :
469 : /// [AppBar.scrolledUnderElevation]
470 : ///
471 : final double? scrolledUnderElevation;
472 :
473 : /// [AppBar.shadowColor]
474 : ///
475 : final Color? shadowColor;
476 :
477 : /// [AppBar.surfaceTintColor]
478 : ///
479 : final Color? surfaceTintColor;
480 :
481 : /// [AppBar.shape]
482 : ///
483 : final ShapeBorder? shape;
484 :
485 : /// [AppBar.backgroundColor]
486 : ///
487 : final Color? backgroundColor;
488 :
489 : /// [AppBar.foregroundColor]
490 : ///
491 : final Color? foregroundColor;
492 :
493 : /// [AppBar.iconTheme]
494 : ///
495 : final IconThemeData? iconTheme;
496 :
497 : /// [AppBar.actionsIconTheme]
498 : ///
499 : final IconThemeData? actionsIconTheme;
500 :
501 : /// [AppBar.primary]
502 : ///
503 : final bool primary;
504 :
505 : /// [AppBar.centerTitle]
506 : ///
507 : final bool? centerTitle;
508 :
509 : /// [AppBar.excludeHeaderSemantics]
510 : ///
511 : final bool excludeHeaderSemantics;
512 :
513 : /// [AppBar.titleSpacing]
514 : ///
515 : final double? titleSpacing;
516 :
517 : /// [AppBar.toolbarOpacity]
518 : ///
519 : final double toolbarOpacity;
520 :
521 : /// [AppBar.bottomOpacity]
522 : ///
523 : final double bottomOpacity;
524 :
525 : /// [AppBar.toolbarHeight]
526 : ///
527 : final double? toolbarHeight;
528 :
529 : /// [AppBar.leadingWidth]
530 : ///
531 : final double? leadingWidth;
532 :
533 : /// [AppBar.toolbarTextStyle]
534 : ///
535 : final TextStyle? toolbarTextStyle;
536 :
537 : /// [AppBar.titleTextStyle]
538 : ///
539 : final TextStyle? titleTextStyle;
540 :
541 : /// [AppBar.systemOverlayStyle]
542 : ///
543 : final SystemUiOverlayStyle? systemOverlayStyle;
544 : }
|