// Copyright 2015 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 'dart:async'; import 'dart:math' as math; import 'package:newton/newton.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'debug.dart'; import 'icon.dart'; import 'icons.dart'; import 'icon_theme.dart'; import 'icon_theme_data.dart'; import 'ink_well.dart'; import 'material.dart'; import 'theme.dart'; typedef void TabSelectedIndexChanged(int selectedIndex); typedef void TabLayoutChanged(Size size, List<double> widths); // See https://www.google.com/design/spec/components/tabs.html#tabs-specs const double _kTabHeight = 46.0; const double _kTextAndIconTabHeight = 72.0; const double _kTabIndicatorHeight = 2.0; const double _kMinTabWidth = 72.0; const double _kMaxTabWidth = 264.0; const EdgeDims _kTabLabelPadding = const EdgeDims.symmetric(horizontal: 12.0); const double _kTabBarScrollDrag = 0.025; const Duration _kTabBarScroll = const Duration(milliseconds: 300); // The scrollOffset (velocity) provided to fling() is pixels/ms, and the // tolerance velocity is pixels/sec. final double _kMinFlingVelocity = kPixelScrollTolerance.velocity / 2000.0; class _TabBarParentData extends ContainerBoxParentDataMixin<RenderBox> { } class _RenderTabBar extends RenderBox with ContainerRenderObjectMixin<RenderBox, _TabBarParentData>, RenderBoxContainerDefaultsMixin<RenderBox, _TabBarParentData> { _RenderTabBar(this.onLayoutChanged); int _selectedIndex; int get selectedIndex => _selectedIndex; void set selectedIndex(int value) { if (_selectedIndex != value) { _selectedIndex = value; markNeedsPaint(); } } Color _indicatorColor; Color get indicatorColor => _indicatorColor; void set indicatorColor(Color value) { if (_indicatorColor != value) { _indicatorColor = value; markNeedsPaint(); } } Rect _indicatorRect; Rect get indicatorRect => _indicatorRect; void set indicatorRect(Rect value) { if (_indicatorRect != value) { _indicatorRect = value; markNeedsPaint(); } } bool _textAndIcons; bool get textAndIcons => _textAndIcons; void set textAndIcons(bool value) { if (_textAndIcons != value) { _textAndIcons = value; markNeedsLayout(); } } bool _isScrollable; bool get isScrollable => _isScrollable; void set isScrollable(bool value) { if (_isScrollable != value) { _isScrollable = value; markNeedsLayout(); } } void setupParentData(RenderBox child) { if (child.parentData is! _TabBarParentData) child.parentData = new _TabBarParentData(); } double getMinIntrinsicWidth(BoxConstraints constraints) { BoxConstraints widthConstraints = new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight); double maxWidth = 0.0; RenderBox child = firstChild; while (child != null) { maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(widthConstraints)); final _TabBarParentData childParentData = child.parentData; child = childParentData.nextSibling; } double width = isScrollable ? maxWidth : maxWidth * childCount; return constraints.constrainWidth(width); } double getMaxIntrinsicWidth(BoxConstraints constraints) { BoxConstraints widthConstraints = new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight); double maxWidth = 0.0; RenderBox child = firstChild; while (child != null) { maxWidth = math.max(maxWidth, child.getMaxIntrinsicWidth(widthConstraints)); final _TabBarParentData childParentData = child.parentData; child = childParentData.nextSibling; } double width = isScrollable ? maxWidth : maxWidth * childCount; return constraints.constrainWidth(width); } double get _tabHeight => textAndIcons ? _kTextAndIconTabHeight : _kTabHeight; double get _tabBarHeight => _tabHeight + _kTabIndicatorHeight; double _getIntrinsicHeight(BoxConstraints constraints) => constraints.constrainHeight(_tabBarHeight); double getMinIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints); double getMaxIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints); void layoutFixedWidthTabs() { double tabWidth = size.width / childCount; BoxConstraints tabConstraints = new BoxConstraints.tightFor(width: tabWidth, height: _tabHeight); double x = 0.0; RenderBox child = firstChild; while (child != null) { child.layout(tabConstraints); final _TabBarParentData childParentData = child.parentData; childParentData.offset = new Offset(x, 0.0); x += tabWidth; child = childParentData.nextSibling; } } double layoutScrollableTabs() { BoxConstraints tabConstraints = new BoxConstraints( minWidth: _kMinTabWidth, maxWidth: _kMaxTabWidth, minHeight: _tabHeight, maxHeight: _tabHeight ); double x = 0.0; RenderBox child = firstChild; while (child != null) { child.layout(tabConstraints, parentUsesSize: true); final _TabBarParentData childParentData = child.parentData; childParentData.offset = new Offset(x, 0.0); x += child.size.width; child = childParentData.nextSibling; } return x; } Size layoutSize; List<double> layoutWidths; TabLayoutChanged onLayoutChanged; void reportLayoutChangedIfNeeded() { assert(onLayoutChanged != null); List<double> widths = new List<double>(childCount); if (!isScrollable && childCount > 0) { double tabWidth = size.width / childCount; widths.fillRange(0, widths.length, tabWidth); } else if (isScrollable) { RenderBox child = firstChild; int childIndex = 0; while (child != null) { widths[childIndex++] = child.size.width; final _TabBarParentData childParentData = child.parentData; child = childParentData.nextSibling; } assert(childIndex == widths.length); } if (size != layoutSize || widths != layoutWidths) { layoutSize = size; layoutWidths = widths; onLayoutChanged(layoutSize, layoutWidths); } } void performLayout() { assert(constraints is BoxConstraints); if (childCount == 0) return; if (isScrollable) { double tabBarWidth = layoutScrollableTabs(); size = constraints.constrain(new Size(tabBarWidth, _tabBarHeight)); } else { size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight)); layoutFixedWidthTabs(); } if (onLayoutChanged != null) reportLayoutChangedIfNeeded(); } bool hitTestChildren(HitTestResult result, { Point position }) { return defaultHitTestChildren(result, position: position); } void _paintIndicator(Canvas canvas, RenderBox selectedTab, Offset offset) { if (indicatorColor == null) return; if (indicatorRect != null) { canvas.drawRect(indicatorRect.shift(offset), new Paint()..color = indicatorColor); return; } final Size size = new Size(selectedTab.size.width, _kTabIndicatorHeight); final _TabBarParentData selectedTabParentData = selectedTab.parentData; final Point point = new Point( selectedTabParentData.offset.dx, _tabBarHeight - _kTabIndicatorHeight ); canvas.drawRect((point + offset) & size, new Paint()..color = indicatorColor); } void paint(PaintingContext context, Offset offset) { int index = 0; RenderBox child = firstChild; while (child != null) { final _TabBarParentData childParentData = child.parentData; context.paintChild(child, childParentData.offset + offset); if (index++ == selectedIndex) _paintIndicator(context.canvas, child, offset); child = childParentData.nextSibling; } } } class _TabBarWrapper extends MultiChildRenderObjectWidget { _TabBarWrapper({ Key key, List<Widget> children, this.selectedIndex, this.indicatorColor, this.indicatorRect, this.textAndIcons, this.isScrollable: false, this.onLayoutChanged }) : super(key: key, children: children); final int selectedIndex; final Color indicatorColor; final Rect indicatorRect; final bool textAndIcons; final bool isScrollable; final TabLayoutChanged onLayoutChanged; _RenderTabBar createRenderObject(BuildContext context) { _RenderTabBar result = new _RenderTabBar(onLayoutChanged); updateRenderObject(context, result); return result; } void updateRenderObject(BuildContext context, _RenderTabBar renderObject) { renderObject ..selectedIndex = selectedIndex ..indicatorColor = indicatorColor ..indicatorRect = indicatorRect ..textAndIcons = textAndIcons ..isScrollable = isScrollable ..onLayoutChanged = onLayoutChanged; } } typedef Widget TabLabelIconBuilder(BuildContext context, Color color); /// Each TabBar tab can display either a title [text], an icon, or both. An icon /// can be specified by either the icon or iconBuilder parameters. In either case /// the icon will occupy a 24x24 box above the title text. If iconBuilder is /// specified its color parameter is the color that an ordinary icon would have /// been drawn with. The color reflects that tab's selection state. class TabLabel { const TabLabel({ this.text, this.icon, this.iconBuilder }); final String text; final IconData icon; final TabLabelIconBuilder iconBuilder; } class _Tab extends StatelessComponent { _Tab({ Key key, this.onSelected, this.label, this.color }) : super(key: key) { assert(label.text != null || label.icon != null || label.iconBuilder != null); } final VoidCallback onSelected; final TabLabel label; final Color color; Widget _buildLabelText() { assert(label.text != null); TextStyle style = new TextStyle(color: color); return new Text(label.text, style: style); } Widget _buildLabelIcon(BuildContext context) { assert(label.icon != null || label.iconBuilder != null); if (label.icon != null) { return new Icon(icon: label.icon, color: color); } else { return new SizedBox( width: 24.0, height: 24.0, child: label.iconBuilder(context, color) ); } } Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); Widget labelContent; if (label.icon == null && label.iconBuilder == null) { labelContent = _buildLabelText(); } else if (label.text == null) { labelContent = _buildLabelIcon(context); } else { labelContent = new Column( children: <Widget>[ new Container( child: _buildLabelIcon(context), margin: const EdgeDims.only(bottom: 10.0) ), _buildLabelText() ], justifyContent: FlexJustifyContent.center, alignItems: FlexAlignItems.center ); } Container centeredLabel = new Container( child: new Center(child: labelContent, widthFactor: 1.0, heightFactor: 1.0), constraints: new BoxConstraints(minWidth: _kMinTabWidth), padding: _kTabLabelPadding ); return new InkWell( onTap: onSelected, child: centeredLabel ); } void debugFillDescription(List<String> description) { super.debugFillDescription(description); description.add('$label'); } } class _TabsScrollBehavior extends BoundedBehavior { _TabsScrollBehavior(); bool isScrollable = true; Simulation createFlingScrollSimulation(double position, double velocity) { if (!isScrollable) return null; double velocityPerSecond = velocity * 1000.0; return new BoundedFrictionSimulation( _kTabBarScrollDrag, position, velocityPerSecond, minScrollOffset, maxScrollOffset ); } double applyCurve(double scrollOffset, double scrollDelta) { return (isScrollable) ? super.applyCurve(scrollOffset, scrollDelta) : 0.0; } } abstract class TabBarSelectionAnimationListener { void handleStatusChange(AnimationStatus status); void handleProgressChange(); void handleSelectionDeactivate(); } class TabBarSelection<T> extends StatefulComponent { TabBarSelection({ Key key, this.value, this.values, this.onChanged, this.child }) : super(key: key) { assert(values != null && values.length > 0); assert(new Set<T>.from(values).length == values.length); assert(value == null ? true : values.where((T e) => e == value).length == 1); assert(child != null); } final T value; List<T> values; final ValueChanged<T> onChanged; final Widget child; TabBarSelectionState<T> createState() => new TabBarSelectionState<T>(); static TabBarSelectionState of(BuildContext context) { return context.ancestorStateOfType(const TypeMatcher<TabBarSelectionState>()); } } class TabBarSelectionState<T> extends State<TabBarSelection<T>> { Animation<double> get animation => _controller.view; // Both the TabBar and TabBarView classes access _controller because they // alternately drive selection progress between tabs. final AnimationController _controller = new AnimationController(duration: _kTabBarScroll, value: 1.0); final Map<T, int> _valueToIndex = new Map<T, int>(); void _initValueToIndex() { _valueToIndex.clear(); int index = 0; for(T value in values) _valueToIndex[value] = index++; } void initState() { super.initState(); _value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first; // If the selection's values have changed since the selected value was saved with // PageStorage.writeState() then use the default. if (!values.contains(_value)) _value = values.first; _previousValue = _value; _initValueToIndex(); } void didUpdateConfig(TabBarSelection oldConfig) { super.didUpdateConfig(oldConfig); if (values != oldConfig.values) _initValueToIndex(); } void _writeValue() { PageStorage.of(context)?.writeState(context, _value); } List<T> get values => config.values; T get previousValue => _previousValue; T _previousValue; bool _valueIsChanging = false; bool get valueIsChanging => _valueIsChanging; int indexOf(T tabValue) => _valueToIndex[tabValue]; int get index => _valueToIndex[value]; int get previousIndex => indexOf(_previousValue); T get value => _value; T _value; void set value(T newValue) { if (newValue == _value) return; if (!_valueIsChanging) _previousValue = _value; _value = newValue; _writeValue(); _valueIsChanging = true; // If the selected value change was triggered by a drag gesture, the current // value of _controller.value will reflect where the gesture ended. While // the drag was underway progress indicates where the indicator and TabBarView // scrollPosition are vis the indices of the two tabs adjacent to the selected // one. So 0.5 means the drag didn't move at all, 0.0 means the drag extended // to the beginning of the tab on the left and 1.0 likewise for the tab on the // right. That is unless the index of the selected value was 0 or values.length - 1. // In those cases progress just moves between the selected tab and the adjacent // one. Convert progress to reflect the fact that we're now moving between (just) // the previous and current selection index. double value; if (_controller.status == AnimationStatus.completed) value = 0.0; else if (_previousValue == values.first) value = _controller.value; else if (_previousValue == values.last) value = 1.0 - _controller.value; else if (previousIndex < index) value = (_controller.value - 0.5) * 2.0; else value = 1.0 - _controller.value * 2.0; _controller ..value = value ..forward().then((_) { if (_controller.value == 1.0) { if (config.onChanged != null) config.onChanged(_value); _valueIsChanging = false; } }); } final List<TabBarSelectionAnimationListener> _animationListeners = <TabBarSelectionAnimationListener>[]; void registerAnimationListener(TabBarSelectionAnimationListener listener) { _animationListeners.add(listener); _controller ..addStatusListener(listener.handleStatusChange) ..addListener(listener.handleProgressChange); } void unregisterAnimationListener(TabBarSelectionAnimationListener listener) { _animationListeners.remove(listener); _controller ..removeStatusListener(listener.handleStatusChange) ..removeListener(listener.handleProgressChange); } void deactivate() { _controller.stop(); for (TabBarSelectionAnimationListener listener in _animationListeners.toList()) { listener.handleSelectionDeactivate(); unregisterAnimationListener(listener); } assert(_animationListeners.isEmpty); _writeValue(); } Widget build(BuildContext context) { return config.child; } } /// Displays a horizontal row of tabs, one per label. If isScrollable is /// true then each tab is as wide as needed for its label and the entire /// [TabBar] is scrollable. Otherwise each tab gets an equal share of the /// available space. A [TabBarSelection] widget ancestor must have been /// built to enable saving and monitoring the selected tab. /// /// Tabs must always have an ancestor Material object. class TabBar<T> extends Scrollable { TabBar({ Key key, this.labels, this.isScrollable: false }) : super(key: key, scrollDirection: Axis.horizontal); final Map<T, TabLabel> labels; final bool isScrollable; _TabBarState createState() => new _TabBarState(); } class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelectionAnimationListener { TabBarSelectionState _selection; bool _valueIsChanging = false; void _initSelection(TabBarSelectionState<T> selection) { _selection?.unregisterAnimationListener(this); _selection = selection; _selection?.registerAnimationListener(this); } void initState() { super.initState(); scrollBehavior.isScrollable = config.isScrollable; _initSelection(TabBarSelection.of(context)); } void didUpdateConfig(TabBar oldConfig) { super.didUpdateConfig(oldConfig); if (!config.isScrollable) scrollTo(0.0); } void dispose() { _selection?.unregisterAnimationListener(this); super.dispose(); } void handleSelectionDeactivate() { _selection = null; } void handleStatusChange(AnimationStatus status) { if (config.labels.length == 0) return; if (_valueIsChanging && status == AnimationStatus.completed) { _valueIsChanging = false; _indicatorTween ..begin = _tabIndicatorRect(math.max(0, _selection.index - 1)) ..end = _tabIndicatorRect(math.min(config.labels.length - 1, _selection.index + 1)); setState(() { _indicatorRect = _tabIndicatorRect(_selection.index); }); } } void handleProgressChange() { if (config.labels.length == 0 || _selection == null) return; if (!_valueIsChanging && _selection.valueIsChanging) { if (config.isScrollable) scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll); _indicatorTween ..begin = _indicatorRect ?? _tabIndicatorRect(_selection.previousIndex) ..end = _tabIndicatorRect(_selection.index); _valueIsChanging = true; } Rect oldRect = _indicatorRect; double t = _selection.animation.value; if (_valueIsChanging) { // When _valueIsChanging is true, we're animating based on a ticker and // want to curve the animation. When _valueIsChanging is false, we're // animating based on a pointer event and want linear feedback. It's // possible we should move this curve into the selection animation. t = Curves.ease.transform(t); } // TODO(abarth): If we've never gone through handleStatusChange before, we // might not have set up our _indicatorTween yet. _indicatorRect = _indicatorTween.lerp(t); if (oldRect != _indicatorRect) setState(() { }); } Size _viewportSize = Size.zero; Size _tabBarSize; List<double> _tabWidths; Rect _indicatorRect; RectTween _indicatorTween = new RectTween(); Rect _tabRect(int tabIndex) { assert(_tabBarSize != null); assert(_tabWidths != null); assert(tabIndex >= 0 && tabIndex < _tabWidths.length); double tabLeft = 0.0; if (tabIndex > 0) tabLeft = _tabWidths.take(tabIndex).reduce((double sum, double width) => sum + width); final double tabTop = 0.0; final double tabBottom = _tabBarSize.height - _kTabIndicatorHeight; final double tabRight = tabLeft + _tabWidths[tabIndex]; return new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); } Rect _tabIndicatorRect(int tabIndex) { Rect r = _tabRect(tabIndex); return new Rect.fromLTRB(r.left, r.bottom, r.right, r.bottom + _kTabIndicatorHeight); } ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior(); _TabsScrollBehavior get scrollBehavior => super.scrollBehavior; double _centeredTabScrollOffset(int tabIndex) { double viewportWidth = scrollBehavior.containerExtent; Rect tabRect = _tabRect(tabIndex); return (tabRect.left + tabRect.width / 2.0 - viewportWidth / 2.0) .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset); } void _handleTabSelected(int tabIndex) { if (_selection != null && tabIndex != _selection.index) setState(() { _selection.value = _selection.values[tabIndex]; }); } Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) { Color labelColor = color; if (_selection != null) { final bool isSelectedTab = tabIndex == _selection.index; final bool isPreviouslySelectedTab = tabIndex == _selection.previousIndex; labelColor = isSelectedTab ? selectedColor : color; if (_selection.valueIsChanging) { if (isSelectedTab) labelColor = Color.lerp(color, selectedColor, _selection.animation.value); else if (isPreviouslySelectedTab) labelColor = Color.lerp(selectedColor, color, _selection.animation.value); } } return new _Tab( onSelected: () { _handleTabSelected(tabIndex); }, label: label, color: labelColor ); } void _updateScrollBehavior() { scrollTo(scrollBehavior.updateExtents( containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width, contentExtent: _tabWidths.reduce((double sum, double width) => sum + width), scrollOffset: scrollOffset )); } void _layoutChanged(Size tabBarSize, List<double> tabWidths) { setState(() { _tabBarSize = tabBarSize; _tabWidths = tabWidths; _updateScrollBehavior(); }); } Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) { // We make various state changes here but don't have to do so in a // setState() callback because we are called during layout and all // we're updating is the new offset, which we are providing to the // render object via our return value. _viewportSize = dimensions.containerSize; _updateScrollBehavior(); if (config.isScrollable) scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll); return scrollOffsetToPixelDelta(scrollOffset); } Widget buildContent(BuildContext context) { TabBarSelectionState<T> newSelection = TabBarSelection.of(context); if (_selection != newSelection) _initSelection(newSelection); assert(config.labels.isNotEmpty); assert(Material.of(context) != null); ThemeData themeData = Theme.of(context); Color backgroundColor = Material.of(context).color; Color indicatorColor = themeData.indicatorColor; if (indicatorColor == backgroundColor) { // ThemeData tries to avoid this by having indicatorColor avoid being the // primaryColor. However, it's possible that the tab strip is on a // Material that isn't the primaryColor. In that case, if the indicator // color ends up clashing, then this overrides it. When that happens, // automatic transitions of the theme will likely look ugly as the // indicator color suddenly snaps to white at one end, but it's not clear // how to avoid that any further. indicatorColor = Colors.white; } TextStyle textStyle = themeData.primaryTextTheme.body1; IconThemeData iconTheme = themeData.primaryIconTheme; Color textColor = themeData.primaryTextTheme.body1.color.withAlpha(0xB2); // 70% alpha List<Widget> tabs = <Widget>[]; bool textAndIcons = false; int tabIndex = 0; for (TabLabel label in config.labels.values) { tabs.add(_toTab(label, tabIndex++, textColor, indicatorColor)); if (label.text != null && (label.icon != null || label.iconBuilder != null)) textAndIcons = true; } Widget contents = new IconTheme( data: iconTheme, child: new DefaultTextStyle( style: textStyle, child: new _TabBarWrapper( children: tabs, selectedIndex: _selection?.index, indicatorColor: indicatorColor, indicatorRect: _indicatorRect, textAndIcons: textAndIcons, isScrollable: config.isScrollable, onLayoutChanged: _layoutChanged ) ) ); if (config.isScrollable) { return new Viewport( scrollDirection: Axis.horizontal, paintOffset: scrollOffsetToPixelDelta(scrollOffset), onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded, child: contents ); } return contents; } } class TabBarView extends PageableList { TabBarView({ Key key, List<Widget> children }) : super( key: key, scrollDirection: Axis.horizontal, children: children ) { assert(children != null); assert(children.length > 1); } _TabBarViewState createState() => new _TabBarViewState(); } class _TabBarViewState extends PageableListState<TabBarView> implements TabBarSelectionAnimationListener { TabBarSelectionState _selection; List<Widget> _items; int get _tabCount => config.children.length; BoundedBehavior _boundedBehavior; ExtentScrollBehavior get scrollBehavior { _boundedBehavior ??= new BoundedBehavior(); return _boundedBehavior; } void _initSelection(TabBarSelectionState selection) { _selection = selection; if (_selection != null) { _selection.registerAnimationListener(this); _updateItemsAndScrollBehavior(); } } void initState() { super.initState(); _initSelection(TabBarSelection.of(context)); } void didUpdateConfig(TabBarView oldConfig) { super.didUpdateConfig(oldConfig); if (_selection != null && config.children != oldConfig.children) _updateItemsForSelectedIndex(_selection.index); } void dispose() { _selection?.unregisterAnimationListener(this); super.dispose(); } void handleSelectionDeactivate() { _selection = null; } void _updateItemsFromChildren(int first, int second, [int third]) { List<Widget> widgets = config.children; _items = <Widget>[widgets[first], widgets[second]]; if (third != null) _items.add(widgets[third]); } void _updateItemsForSelectedIndex(int selectedIndex) { if (selectedIndex == 0) { _updateItemsFromChildren(0, 1); } else if (selectedIndex == _tabCount - 1) { _updateItemsFromChildren(selectedIndex - 1, selectedIndex); } else { _updateItemsFromChildren(selectedIndex - 1, selectedIndex, selectedIndex + 1); } } void _updateScrollBehaviorForSelectedIndex(int selectedIndex) { if (selectedIndex == 0) { scrollTo(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 0.0)); } else if (selectedIndex == _tabCount - 1) { scrollTo(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 1.0)); } else { scrollTo(scrollBehavior.updateExtents(contentExtent: 3.0, containerExtent: 1.0, scrollOffset: 1.0)); } } void _updateItemsAndScrollBehavior() { assert(_selection != null); final int selectedIndex = _selection.index; assert(selectedIndex != null); _updateItemsForSelectedIndex(selectedIndex); _updateScrollBehaviorForSelectedIndex(selectedIndex); } void handleStatusChange(AnimationStatus status) { } void handleProgressChange() { if (_selection == null || !_selection.valueIsChanging) return; // The TabBar is driving the TabBarSelection animation. final Animation<double> animation = _selection.animation; if (animation.status == AnimationStatus.completed) { _updateItemsAndScrollBehavior(); return; } if (animation.status != AnimationStatus.forward) return; final int selectedIndex = _selection.index; final int previousSelectedIndex = _selection.previousIndex; if (selectedIndex < previousSelectedIndex) { _updateItemsFromChildren(selectedIndex, previousSelectedIndex); scrollTo(1.0 - animation.value); } else { _updateItemsFromChildren(previousSelectedIndex, selectedIndex); scrollTo(animation.value); } } void dispatchOnScroll() { if (_selection == null || _selection.valueIsChanging) return; // This class is driving the TabBarSelection's animation. final AnimationController controller = _selection._controller; if (_selection.index == 0 || _selection.index == _tabCount - 1) controller.value = scrollOffset; else controller.value = scrollOffset / 2.0; } Future fling(double scrollVelocity) { if (_selection == null || _selection.valueIsChanging) return new Future.value(); if (scrollVelocity.abs() > _kMinFlingVelocity) { final int selectionDelta = scrollVelocity.sign.truncate(); final int targetIndex = (_selection.index + selectionDelta).clamp(0, _tabCount - 1); _selection.value = _selection.values[targetIndex]; return new Future.value(); } final int selectionIndex = _selection.index; final int settleIndex = snapScrollOffset(scrollOffset).toInt(); if (selectionIndex > 0 && settleIndex != 1) { final int targetIndex = (selectionIndex + (settleIndex == 2 ? 1 : -1)).clamp(0, _tabCount - 1); _selection.value = _selection.values[targetIndex]; return new Future.value(); } else if (selectionIndex == 0 && settleIndex == 1) { _selection.value = _selection.values[1]; return new Future.value(); } return settleScrollOffset(); } Widget buildContent(BuildContext context) { TabBarSelectionState newSelection = TabBarSelection.of(context); if (_selection != newSelection) _initSelection(newSelection); return new PageViewport( itemsWrap: config.itemsWrap, scrollDirection: config.scrollDirection, startOffset: scrollOffset, overlayPainter: config.scrollableListPainter, children: _items ); } } class TabPageSelector<T> extends StatelessComponent { const TabPageSelector({ Key key }) : super(key: key); Widget _buildTabIndicator(TabBarSelectionState<T> selection, T tab, Animation animation, ColorTween selectedColor, ColorTween previousColor) { Color background; if (selection.valueIsChanging) { // The selection's animation is animating from previousValue to value. if (selection.value == tab) background = selectedColor.evaluate(animation); else if (selection.previousValue == tab) background = previousColor.evaluate(animation); else background = selectedColor.begin; } else { background = selection.value == tab ? selectedColor.end : selectedColor.begin; } return new Container( width: 12.0, height: 12.0, margin: new EdgeDims.all(4.0), decoration: new BoxDecoration( backgroundColor: background, border: new Border.all(color: selectedColor.end), shape: BoxShape.circle ) ); } Widget build(BuildContext context) { final TabBarSelectionState selection = TabBarSelection.of(context); final Color color = Theme.of(context).accentColor; final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color); final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent); Animation<double> animation = new CurvedAnimation(parent: selection.animation, curve: Curves.ease); return new AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget child) { return new Semantics( label: 'Page ${selection.index + 1} of ${selection.values.length}', child: new Row( children: selection.values.map((T tab) => _buildTabIndicator(selection, tab, animation, selectedColor, previousColor)).toList(), justifyContent: FlexJustifyContent.collapse ) ); } ); } }