| // 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 'icon.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() { |
| _RenderTabBar result = new _RenderTabBar(onLayoutChanged); |
| updateRenderObject(result, null); |
| return result; |
| } |
| |
| void updateRenderObject(_RenderTabBar renderObject, _TabBarWrapper oldWidget) { |
| 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 String 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) { |
| 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) |
| )); |
| } |
| |
| 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) { |
| child: 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).primaryColor; |
| 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 |
| ) |
| ); |
| } |
| ); |
| } |
| } |