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(); |