Chromium Code Reviews| Index: sky/sdk/lib/widgets/tabs.dart |
| diff --git a/sky/sdk/lib/widgets/tabs.dart b/sky/sdk/lib/widgets/tabs.dart |
| index f66d0ee22253a73bd0ada7af01389a6c1dc92d4e..8cbf9395e1b54752e17f2328ead3fc4f7b37e6fc 100644 |
| --- a/sky/sdk/lib/widgets/tabs.dart |
| +++ b/sky/sdk/lib/widgets/tabs.dart |
| @@ -4,20 +4,30 @@ |
| import 'dart:math' as math; |
| +import 'package:sky/animation/scroll_behavior.dart'; |
| +import 'package:sky/painting/text_style.dart'; |
| import 'package:sky/rendering/box.dart'; |
| import 'package:sky/rendering/object.dart'; |
| +import 'package:vector_math/vector_math.dart'; |
| import 'package:sky/widgets/basic.dart'; |
| import 'package:sky/widgets/icon.dart'; |
| import 'package:sky/widgets/ink_well.dart'; |
| +import 'package:sky/widgets/scrollable.dart'; |
| import 'package:sky/widgets/theme.dart'; |
| import 'package:sky/widgets/widget.dart'; |
| typedef void SelectedIndexChanged(int selectedIndex); |
| +typedef void LayoutChanged(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 double _kRelativeMaxTabWidth = 56.0; |
| +const EdgeDims _kTabLabelPadding = const EdgeDims.symmetric(horizontal: 12.0); |
| +const TextStyle _kTabTextStyle = const TextStyle(textAlign: TextAlign.center); |
| const int _kTabIconSize = 24; |
| class TabBarParentData extends BoxParentData with |
| @@ -27,6 +37,8 @@ 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) { |
| @@ -63,6 +75,15 @@ class RenderTabBar extends RenderBox with |
| } |
| } |
| + bool _scrollable; |
| + bool get scrollable => _scrollable; |
| + void set scrollable(bool value) { |
| + if (_scrollable != value) { |
| + _scrollable = value; |
| + markNeedsLayout(); |
| + } |
| + } |
| + |
| void setupParentData(RenderBox child) { |
| if (child.parentData is! TabBarParentData) |
| child.parentData = new TabBarParentData(); |
| @@ -71,6 +92,7 @@ class RenderTabBar extends RenderBox with |
| double getMinIntrinsicWidth(BoxConstraints constraints) { |
| BoxConstraints widthConstraints = |
| new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight); |
| + |
| double maxWidth = 0.0; |
| RenderBox child = firstChild; |
| while (child != null) { |
| @@ -78,12 +100,14 @@ class RenderTabBar extends RenderBox with |
| assert(child.parentData is TabBarParentData); |
| child = child.parentData.nextSibling; |
| } |
| - return constraints.constrainWidth(maxWidth * childCount); |
| + double width = scrollable ? 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) { |
| @@ -91,7 +115,8 @@ class RenderTabBar extends RenderBox with |
| assert(child.parentData is TabBarParentData); |
| child = child.parentData.nextSibling; |
| } |
| - return constraints.constrainWidth(maxWidth * childCount); |
| + double width = scrollable ? maxWidth : maxWidth * childCount; |
|
abarth-chromium
2015/07/02 15:27:21
Why is this different when we're scrollable?
hansmuller
2015/07/06 17:21:46
When we're scrollable, I'm assuring that the wides
|
| + return constraints.constrainWidth(width); |
| } |
| double get _tabBarHeight { |
| @@ -104,22 +129,14 @@ class RenderTabBar extends RenderBox with |
| double getMaxIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints); |
| - void performLayout() { |
| - assert(constraints is BoxConstraints); |
| - |
| - size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight)); |
| - assert(!size.isInfinite); |
| - |
| - if (childCount == 0) |
| - return; |
| - |
| + void layoutFixedWidthTabs() { |
| double tabWidth = size.width / childCount; |
| BoxConstraints tabConstraints = |
| new BoxConstraints.tightFor(width: tabWidth, height: size.height); |
| double x = 0.0; |
| RenderBox child = firstChild; |
| while (child != null) { |
| - child.layout(tabConstraints); |
| + child.layout(tabConstraints, parentUsesSize: onLayoutChanged != null); |
|
abarth-chromium
2015/07/02 15:27:21
We don't really use the child size, even when onLa
hansmuller
2015/07/06 17:21:46
In this case onLayoutChanged is typically null, bu
|
| assert(child.parentData is TabBarParentData); |
| child.parentData.position = new Point(x, 0.0); |
| x += tabWidth; |
| @@ -127,6 +144,61 @@ class RenderTabBar extends RenderBox with |
| } |
| } |
| + void layoutScrollableTabs() { |
| + BoxConstraints tabConstraints = new BoxConstraints( |
| + minWidth: _kMinTabWidth, |
| + maxWidth: math.min(size.width - _kRelativeMaxTabWidth, _kMaxTabWidth), |
| + minHeight: size.height, |
| + maxHeight: size.height); |
| + double x = 0.0; |
| + RenderBox child = firstChild; |
| + while (child != null) { |
| + child.layout(tabConstraints, parentUsesSize: true); |
| + assert(child.parentData is TabBarParentData); |
| + child.parentData.position = new Point(x, 0.0); |
| + x += child.size.width; |
| + child = child.parentData.nextSibling; |
| + } |
| + } |
| + |
| + Size layoutSize; |
| + List<double> layoutWidths; |
| + LayoutChanged onLayoutChanged; |
| + |
| + void reportLayoutChanged() { |
| + assert(onLayoutChanged != null); |
| + RenderBox child = firstChild; |
| + List<double> widths = new List<double>(childCount); |
| + int childIndex = 0; |
| + while (child != null) { |
| + widths[childIndex++] = child.size.width; |
| + child = child.parentData.nextSibling; |
| + } |
| + if (size != layoutSize || widths != layoutWidths) { |
|
abarth-chromium
2015/07/02 15:27:21
We should add an assert(childIndex == widths.lengt
hansmuller
2015/07/06 17:21:46
Done.
|
| + layoutSize = size; |
| + layoutWidths = widths; |
| + onLayoutChanged(layoutSize, layoutWidths); |
| + } |
| + } |
| + |
| + void performLayout() { |
| + assert(constraints is BoxConstraints); |
| + |
| + size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight)); |
| + assert(!size.isInfinite); |
| + |
| + if (childCount == 0) |
| + return; |
| + |
| + if (scrollable) |
| + layoutScrollableTabs(); |
| + else |
| + layoutFixedWidthTabs(); |
| + |
| + if (onLayoutChanged != null) |
| + reportLayoutChanged(); |
|
abarth-chromium
2015/07/02 15:27:21
reportLayoutChangedIfNeeded()
hansmuller
2015/07/06 17:21:46
Done.
|
| + } |
| + |
| void hitTestChildren(HitTestResult result, { Point position }) { |
| defaultHitTestChildren(result, position: position); |
| } |
| @@ -146,7 +218,10 @@ class RenderTabBar extends RenderBox with |
| void paint(PaintingCanvas canvas, Offset offset) { |
| if (backgroundColor != null) { |
| - Rect rect = offset & size; |
| + double width = layoutWidths != null |
| + ? layoutWidths.reduce((sum, width) => sum + width) |
| + : size.width; |
| + Rect rect = offset & new Size(width, size.height); |
| canvas.drawRect(rect, new Paint()..color = backgroundColor); |
| } |
| @@ -169,6 +244,8 @@ class TabBarWrapper extends MultiChildRenderObjectWrapper { |
| this.backgroundColor, |
| this.indicatorColor, |
| this.textAndIcons, |
| + this.scrollable: false, |
| + this.onLayoutChanged, |
| String key |
| }) : super(key: key, children: children); |
| @@ -176,9 +253,11 @@ class TabBarWrapper extends MultiChildRenderObjectWrapper { |
| final Color backgroundColor; |
| final Color indicatorColor; |
| final bool textAndIcons; |
| + final bool scrollable; |
| + final LayoutChanged onLayoutChanged; |
| RenderTabBar get root => super.root; |
| - RenderTabBar createNode() => new RenderTabBar(); |
| + RenderTabBar createNode() => new RenderTabBar(onLayoutChanged); |
| void syncRenderObject(Widget old) { |
| super.syncRenderObject(old); |
| @@ -186,6 +265,8 @@ class TabBarWrapper extends MultiChildRenderObjectWrapper { |
| root.backgroundColor = backgroundColor; |
| root.indicatorColor = indicatorColor; |
| root.textAndIcons = textAndIcons; |
| + root.scrollable = scrollable; |
| + root.onLayoutChanged = onLayoutChanged; |
| } |
| } |
| @@ -210,7 +291,8 @@ class Tab extends Component { |
| Widget _buildLabelText() { |
| assert(label.text != null); |
| - return new Text(label.text, style: Theme.of(this).toolbarText.button); |
| + TextStyle textStyle = Theme.of(this).toolbarText.button.merge(_kTabTextStyle); |
| + return new Text(label.text, style: textStyle); |
| } |
| Widget _buildLabelIcon() { |
| @@ -246,24 +328,38 @@ class Tab extends Component { |
| Container centeredLabel = new Container( |
| child: new Center(child: highlightedLabel), |
| - constraints: new BoxConstraints(minWidth: _kMinTabWidth) |
| + constraints: new BoxConstraints(minWidth: _kMinTabWidth), |
| + padding: _kTabLabelPadding |
| ); |
| return new InkWell(child: centeredLabel); |
| } |
| } |
| -class TabBar extends Component { |
| +class TabBar extends Scrollable { |
| TabBar({ |
| String key, |
| this.labels, |
| this.selectedIndex: 0, |
| - this.onChanged |
| - }) : super(key: key); |
| + this.onChanged, |
| + this.scrollable: false |
| + }) : super(key: key, direction: ScrollDirection.horizontal); |
| + |
| + Iterable<TabLabel> labels; |
| + int selectedIndex; |
| + SelectedIndexChanged onChanged; |
| + bool scrollable; |
| + |
| + void syncFields(TabBar source) { |
| + super.syncFields(source); |
| + labels = source.labels; |
| + selectedIndex = source.selectedIndex; |
| + onChanged = source.onChanged; |
| + scrollable = source.scrollable; |
| + } |
| - final Iterable<TabLabel> labels; |
| - final int selectedIndex; |
| - final SelectedIndexChanged onChanged; |
| + ScrollBehavior createScrollBehavior() => new BoundedScrollBehavior(); |
| + BoundedScrollBehavior get scrollBehavior => super.scrollBehavior; |
| void _handleTap(int tabIndex) { |
| if (tabIndex != selectedIndex && onChanged != null) |
| @@ -282,7 +378,19 @@ class TabBar extends Component { |
| ); |
| } |
| - Widget build() { |
| + Size _tabBarSize; |
| + List<double> _tabWidths; |
| + |
| + void _layoutChanged(Size tabBarSize, List<double> tabWidths) { |
| + setState(() { |
| + _tabBarSize = tabBarSize; |
| + _tabWidths = tabWidths; |
| + scrollBehavior.maxOffset = |
| + _tabWidths.reduce((sum, width) => sum + width) - _tabBarSize.width; |
| + }); |
| + } |
| + |
| + Widget buildContent() { |
| assert(labels != null && labels.isNotEmpty); |
| List<Widget> tabs = <Widget>[]; |
| bool textAndIcons = false; |
| @@ -292,13 +400,20 @@ class TabBar extends Component { |
| if (label.text != null && label.icon != null) |
| textAndIcons = true; |
| } |
| - return new TabBarWrapper( |
| + |
| + TabBarWrapper tabBarWrapper = new TabBarWrapper( |
| children: tabs, |
| selectedIndex: selectedIndex, |
| backgroundColor: Theme.of(this).primary[500], |
| indicatorColor: Theme.of(this).accent[200], |
| - textAndIcons: textAndIcons |
| + textAndIcons: textAndIcons, |
| + scrollable: scrollable, |
| + onLayoutChanged: scrollable ? _layoutChanged : null |
| ); |
| + |
| + Matrix4 transform = new Matrix4.identity(); |
| + transform.translate(-scrollOffset, 0.0); |
| + return new Transform(child: tabBarWrapper, transform: transform); |
|
abarth-chromium
2015/07/02 15:27:21
Don't we need a clip too?
hansmuller
2015/07/06 17:21:46
I would have thought so. Adding a ClipRect parent
|
| } |
| } |
| @@ -321,12 +436,14 @@ class TabNavigator extends Component { |
| String key, |
| this.views, |
| this.selectedIndex: 0, |
| - this.onChanged |
| + this.onChanged, |
| + this.scrollable: false |
| }) : super(key: key); |
| final List<TabNavigatorView> views; |
| final int selectedIndex; |
| final SelectedIndexChanged onChanged; |
| + final bool scrollable; |
| void _handleSelectedIndexChanged(int tabIndex) { |
| if (onChanged != null) |
| @@ -340,7 +457,8 @@ class TabNavigator extends Component { |
| TabBar tabBar = new TabBar( |
| labels: views.map((view) => view.label), |
| onChanged: _handleSelectedIndexChanged, |
| - selectedIndex: selectedIndex |
| + selectedIndex: selectedIndex, |
| + scrollable: scrollable |
| ); |
| Widget content = views[selectedIndex].buildContent(); |