// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/widgets.dart'; import 'bottom_tab_bar.dart'; /// Implements a tabbed iOS application's root layout and behavior structure. /// /// The scaffold lays out the tab bar at the bottom and the content between or /// behind the tab bar. /// /// A [tabBar] and a [tabBuilder] are required. The [CupertinoTabScaffold] /// will automatically listen to the provided [CupertinoTabBar]'s tap callbacks /// to change the active tab. /// /// Tabs' contents are built with the provided [tabBuilder] at the active /// tab index. The [tabBuilder] must be able to build the same number of /// pages as there are [tabBar.items]. Inactive tabs will be moved [Offstage] /// and their animations disabled. /// /// Use [CupertinoTabView] as the content of each tab to support tabs with parallel /// navigation state and history. /// /// ## Sample code /// /// A sample code implementing a typical iOS information architecture with tabs. /// /// ```dart /// new CupertinoTabScaffold( /// tabBar: new CupertinoTabBar( /// items: <BottomNavigationBarItem> [ /// // ... /// ], /// ), /// tabBuilder: (BuildContext context, int index) { /// return new CupertinoTabView( /// builder: (BuildContext context) { /// return new CupertinoPageScaffold( /// navigationBar: new CupertinoNavigationBar( /// middle: new Text('Page 1 of tab $index'), /// ), /// child: new Center( /// child: new CupertinoButton( /// child: const Text('Next page'), /// onPressed: () { /// Navigator.of(context).push( /// new CupertinoPageRoute<Null>( /// builder: (BuildContext context) { /// return new CupertinoPageScaffold( /// navigationBar: new CupertinoNavigationBar( /// middle: new Text('Page 2 of tab $index'), /// ), /// child: new Center( /// child: new CupertinoButton( /// child: const Text('Back'), /// onPressed: () { Navigator.of(context).pop(); }, /// ), /// ), /// ); /// }, /// ), /// ); /// }, /// ), /// ), /// ); /// }, /// ); /// }, /// ) /// ``` /// /// To push a route above all tabs instead of inside the currently selected one /// (such as when showing a dialog on top of this scaffold), use /// `Navigator.of(rootNavigator: true)` from inside the [BuildContext] of a /// [CupertinoTabView]. /// /// See also: /// /// * [CupertinoTabBar], the bottom tab bar inserted in the scaffold. /// * [CupertinoTabView], the typical root content of each tab that holds its own /// [Navigator] stack. /// * [CupertinoPageRoute], a route hosting modal pages with iOS style transitions. /// * [CupertinoPageScaffold], typical contents of an iOS modal page implementing /// layout with a navigation bar on top. class CupertinoTabScaffold extends StatefulWidget { /// Creates a layout for applications with a tab bar at the bottom. /// /// The [tabBar], [tabBuilder] and [currentTabIndex] arguments must not be null. /// /// The [currentTabIndex] argument can be used to programmatically change the /// currently selected tab. const CupertinoTabScaffold({ Key key, @required this.tabBar, @required this.tabBuilder, }) : assert(tabBar != null), assert(tabBuilder != null), super(key: key); /// The [tabBar] is a [CupertinoTabBar] drawn at the bottom of the screen /// that lets the user switch between different tabs in the main content area /// when present. /// /// Setting and changing [CupertinoTabBar.currentIndex] programmatically will /// change the currently selected tab item in the [tabBar] as well as change /// the currently focused tab from the [tabBuilder]. /// If [CupertinoTabBar.onTap] is provided, it will still be called. /// [CupertinoTabScaffold] automatically also listen to the /// [CupertinoTabBar]'s `onTap` to change the [CupertinoTabBar]'s `currentIndex` /// and change the actively displayed tab in [CupertinoTabScaffold]'s own /// main content area. /// /// If translucent, the main content may slide behind it. /// Otherwise, the main content's bottom margin will be offset by its height. /// /// Must not be null. final CupertinoTabBar tabBar; /// An [IndexedWidgetBuilder] that's called when tabs become active. /// /// The widgets built by [IndexedWidgetBuilder] is typically a [CupertinoTabView] /// in order to achieve the parallel hierarchies information architecture seen /// on iOS apps with tab bars. /// /// When the tab becomes inactive, its content is still cached in the widget /// tree [Offstage] and its animations disabled. /// /// Content can slide under the [tabBar] when they're translucent. /// In that case, the child's [BuildContext]'s [MediaQuery] will have a /// bottom padding indicating the area of obstructing overlap from the /// [tabBar]. /// /// Must not be null. final IndexedWidgetBuilder tabBuilder; @override _CupertinoTabScaffoldState createState() => new _CupertinoTabScaffoldState(); } class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> { int _currentPage; @override void initState() { super.initState(); _currentPage = widget.tabBar.currentIndex; } @override void didUpdateWidget(CupertinoTabScaffold oldWidget) { super.didUpdateWidget(oldWidget); if (widget.tabBar.currentIndex != oldWidget.tabBar.currentIndex) { _currentPage = widget.tabBar.currentIndex; } } @override Widget build(BuildContext context) { final List<Widget> stacked = <Widget>[]; Widget content = new _TabSwitchingView( currentTabIndex: _currentPage, tabNumber: widget.tabBar.items.length, tabBuilder: widget.tabBuilder, ); if (widget.tabBar != null) { final MediaQueryData existingMediaQuery = MediaQuery.of(context); // TODO(xster): Use real size after partial layout instead of preferred size. // https://github.com/flutter/flutter/issues/12912 final double bottomPadding = widget.tabBar.preferredSize.height + existingMediaQuery.padding.bottom; // If tab bar opaque, directly stop the main content higher. If // translucent, let main content draw behind the tab bar but hint the // obstructed area. if (widget.tabBar.opaque) { content = new Padding( padding: new EdgeInsets.only(bottom: bottomPadding), child: content, ); } else { content = new MediaQuery( data: existingMediaQuery.copyWith( padding: existingMediaQuery.padding.copyWith( bottom: bottomPadding, ), ), child: content, ); } } // The main content being at the bottom is added to the stack first. stacked.add(content); if (widget.tabBar != null) { stacked.add(new Align( alignment: Alignment.bottomCenter, // Override the tab bar's currentIndex to the current tab and hook in // our own listener to update the _currentPage on top of a possibly user // provided callback. child: widget.tabBar.copyWith( currentIndex: _currentPage, onTap: (int newIndex) { setState(() { _currentPage = newIndex; }); // Chain the user's original callback. if (widget.tabBar.onTap != null) widget.tabBar.onTap(newIndex); } ), )); } return new Stack( children: stacked, ); } } /// A widget laying out multiple tabs with only one active tab being built /// at a time and on stage. Off stage tabs' animations are stopped. class _TabSwitchingView extends StatefulWidget { const _TabSwitchingView({ @required this.currentTabIndex, @required this.tabNumber, @required this.tabBuilder, }) : assert(currentTabIndex != null), assert(tabNumber != null && tabNumber > 0), assert(tabBuilder != null); final int currentTabIndex; final int tabNumber; final IndexedWidgetBuilder tabBuilder; @override _TabSwitchingViewState createState() => new _TabSwitchingViewState(); } class _TabSwitchingViewState extends State<_TabSwitchingView> { List<Widget> tabs; List<FocusScopeNode> tabFocusNodes; @override void initState() { super.initState(); tabs = new List<Widget>(widget.tabNumber); tabFocusNodes = new List<FocusScopeNode>.generate( widget.tabNumber, (int index) => new FocusScopeNode(), ); } @override void didChangeDependencies() { super.didChangeDependencies(); _focusActiveTab(); } @override void didUpdateWidget(_TabSwitchingView oldWidget) { super.didUpdateWidget(oldWidget); _focusActiveTab(); } void _focusActiveTab() { FocusScope.of(context).setFirstFocus(tabFocusNodes[widget.currentTabIndex]); } @override void dispose() { for (FocusScopeNode focusScopeNode in tabFocusNodes) { focusScopeNode.detach(); } super.dispose(); } @override Widget build(BuildContext context) { return new Stack( fit: StackFit.expand, children: new List<Widget>.generate(widget.tabNumber, (int index) { final bool active = index == widget.currentTabIndex; if (active || tabs[index] != null) { tabs[index] = widget.tabBuilder(context, index); } return new Offstage( offstage: !active, child: new TickerMode( enabled: active, child: new FocusScope( node: tabFocusNodes[index], child: tabs[index] ?? new Container(), ), ), ); }), ); } }