Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 import 'dart:math' as math; | 5 import 'dart:math' as math; |
| 6 | 6 |
| 7 import 'package:sky/animation/scroll_behavior.dart'; | |
| 8 import 'package:sky/painting/text_style.dart'; | |
| 7 import 'package:sky/rendering/box.dart'; | 9 import 'package:sky/rendering/box.dart'; |
| 8 import 'package:sky/rendering/object.dart'; | 10 import 'package:sky/rendering/object.dart'; |
| 11 import 'package:vector_math/vector_math.dart'; | |
| 9 import 'package:sky/widgets/basic.dart'; | 12 import 'package:sky/widgets/basic.dart'; |
| 10 import 'package:sky/widgets/icon.dart'; | 13 import 'package:sky/widgets/icon.dart'; |
| 11 import 'package:sky/widgets/ink_well.dart'; | 14 import 'package:sky/widgets/ink_well.dart'; |
| 15 import 'package:sky/widgets/scrollable.dart'; | |
| 12 import 'package:sky/widgets/theme.dart'; | 16 import 'package:sky/widgets/theme.dart'; |
| 13 import 'package:sky/widgets/widget.dart'; | 17 import 'package:sky/widgets/widget.dart'; |
| 14 | 18 |
| 15 typedef void SelectedIndexChanged(int selectedIndex); | 19 typedef void SelectedIndexChanged(int selectedIndex); |
| 20 typedef void LayoutChanged(Size size, List<double> widths); | |
| 16 | 21 |
| 22 // See https://www.google.com/design/spec/components/tabs.html#tabs-specs | |
| 17 const double _kTabHeight = 46.0; | 23 const double _kTabHeight = 46.0; |
| 18 const double _kTextAndIconTabHeight = 72.0; | 24 const double _kTextAndIconTabHeight = 72.0; |
| 19 const double _kTabIndicatorHeight = 2.0; | 25 const double _kTabIndicatorHeight = 2.0; |
| 20 const double _kMinTabWidth = 72.0; | 26 const double _kMinTabWidth = 72.0; |
| 27 const double _kMaxTabWidth = 264.0; | |
| 28 const double _kRelativeMaxTabWidth = 56.0; | |
| 29 const EdgeDims _kTabLabelPadding = const EdgeDims.symmetric(horizontal: 12.0); | |
| 30 const TextStyle _kTabTextStyle = const TextStyle(textAlign: TextAlign.center); | |
| 21 const int _kTabIconSize = 24; | 31 const int _kTabIconSize = 24; |
| 22 | 32 |
| 23 class TabBarParentData extends BoxParentData with | 33 class TabBarParentData extends BoxParentData with |
| 24 ContainerParentDataMixin<RenderBox> { } | 34 ContainerParentDataMixin<RenderBox> { } |
| 25 | 35 |
| 26 class RenderTabBar extends RenderBox with | 36 class RenderTabBar extends RenderBox with |
| 27 ContainerRenderObjectMixin<RenderBox, TabBarParentData>, | 37 ContainerRenderObjectMixin<RenderBox, TabBarParentData>, |
| 28 RenderBoxContainerDefaultsMixin<RenderBox, TabBarParentData> { | 38 RenderBoxContainerDefaultsMixin<RenderBox, TabBarParentData> { |
| 29 | 39 |
| 40 RenderTabBar(this.onLayoutChanged); | |
| 41 | |
| 30 int _selectedIndex; | 42 int _selectedIndex; |
| 31 int get selectedIndex => _selectedIndex; | 43 int get selectedIndex => _selectedIndex; |
| 32 void set selectedIndex(int value) { | 44 void set selectedIndex(int value) { |
| 33 if (_selectedIndex != value) { | 45 if (_selectedIndex != value) { |
| 34 _selectedIndex = value; | 46 _selectedIndex = value; |
| 35 markNeedsPaint(); | 47 markNeedsPaint(); |
| 36 } | 48 } |
| 37 } | 49 } |
| 38 | 50 |
| 39 Color _backgroundColor; | 51 Color _backgroundColor; |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 56 | 68 |
| 57 bool _textAndIcons; | 69 bool _textAndIcons; |
| 58 bool get textAndIcons => _textAndIcons; | 70 bool get textAndIcons => _textAndIcons; |
| 59 void set textAndIcons(bool value) { | 71 void set textAndIcons(bool value) { |
| 60 if (_textAndIcons != value) { | 72 if (_textAndIcons != value) { |
| 61 _textAndIcons = value; | 73 _textAndIcons = value; |
| 62 markNeedsLayout(); | 74 markNeedsLayout(); |
| 63 } | 75 } |
| 64 } | 76 } |
| 65 | 77 |
| 78 bool _scrollable; | |
| 79 bool get scrollable => _scrollable; | |
| 80 void set scrollable(bool value) { | |
| 81 if (_scrollable != value) { | |
| 82 _scrollable = value; | |
| 83 markNeedsLayout(); | |
| 84 } | |
| 85 } | |
| 86 | |
| 66 void setupParentData(RenderBox child) { | 87 void setupParentData(RenderBox child) { |
| 67 if (child.parentData is! TabBarParentData) | 88 if (child.parentData is! TabBarParentData) |
| 68 child.parentData = new TabBarParentData(); | 89 child.parentData = new TabBarParentData(); |
| 69 } | 90 } |
| 70 | 91 |
| 71 double getMinIntrinsicWidth(BoxConstraints constraints) { | 92 double getMinIntrinsicWidth(BoxConstraints constraints) { |
| 72 BoxConstraints widthConstraints = | 93 BoxConstraints widthConstraints = |
| 73 new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraint s.maxHeight); | 94 new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraint s.maxHeight); |
| 95 | |
| 74 double maxWidth = 0.0; | 96 double maxWidth = 0.0; |
| 75 RenderBox child = firstChild; | 97 RenderBox child = firstChild; |
| 76 while (child != null) { | 98 while (child != null) { |
| 77 maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(widthConstraints) ); | 99 maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(widthConstraints) ); |
| 78 assert(child.parentData is TabBarParentData); | 100 assert(child.parentData is TabBarParentData); |
| 79 child = child.parentData.nextSibling; | 101 child = child.parentData.nextSibling; |
| 80 } | 102 } |
| 81 return constraints.constrainWidth(maxWidth * childCount); | 103 double width = scrollable ? maxWidth : maxWidth * childCount; |
| 104 return constraints.constrainWidth(width); | |
| 82 } | 105 } |
| 83 | 106 |
| 84 double getMaxIntrinsicWidth(BoxConstraints constraints) { | 107 double getMaxIntrinsicWidth(BoxConstraints constraints) { |
| 85 BoxConstraints widthConstraints = | 108 BoxConstraints widthConstraints = |
| 86 new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraint s.maxHeight); | 109 new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraint s.maxHeight); |
| 110 | |
| 87 double maxWidth = 0.0; | 111 double maxWidth = 0.0; |
| 88 RenderBox child = firstChild; | 112 RenderBox child = firstChild; |
| 89 while (child != null) { | 113 while (child != null) { |
| 90 maxWidth = math.max(maxWidth, child.getMaxIntrinsicWidth(widthConstraints) ); | 114 maxWidth = math.max(maxWidth, child.getMaxIntrinsicWidth(widthConstraints) ); |
| 91 assert(child.parentData is TabBarParentData); | 115 assert(child.parentData is TabBarParentData); |
| 92 child = child.parentData.nextSibling; | 116 child = child.parentData.nextSibling; |
| 93 } | 117 } |
| 94 return constraints.constrainWidth(maxWidth * childCount); | 118 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
| |
| 119 return constraints.constrainWidth(width); | |
| 95 } | 120 } |
| 96 | 121 |
| 97 double get _tabBarHeight { | 122 double get _tabBarHeight { |
| 98 return (textAndIcons ? _kTextAndIconTabHeight : _kTabHeight) + _kTabIndicato rHeight; | 123 return (textAndIcons ? _kTextAndIconTabHeight : _kTabHeight) + _kTabIndicato rHeight; |
| 99 } | 124 } |
| 100 | 125 |
| 101 double _getIntrinsicHeight(BoxConstraints constraints) => constraints.constrai nHeight(_tabBarHeight); | 126 double _getIntrinsicHeight(BoxConstraints constraints) => constraints.constrai nHeight(_tabBarHeight); |
| 102 | 127 |
| 103 double getMinIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeigh t(constraints); | 128 double getMinIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeigh t(constraints); |
| 104 | 129 |
| 105 double getMaxIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeigh t(constraints); | 130 double getMaxIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeigh t(constraints); |
| 106 | 131 |
| 107 void performLayout() { | 132 void layoutFixedWidthTabs() { |
| 108 assert(constraints is BoxConstraints); | |
| 109 | |
| 110 size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight)); | |
| 111 assert(!size.isInfinite); | |
| 112 | |
| 113 if (childCount == 0) | |
| 114 return; | |
| 115 | |
| 116 double tabWidth = size.width / childCount; | 133 double tabWidth = size.width / childCount; |
| 117 BoxConstraints tabConstraints = | 134 BoxConstraints tabConstraints = |
| 118 new BoxConstraints.tightFor(width: tabWidth, height: size.height); | 135 new BoxConstraints.tightFor(width: tabWidth, height: size.height); |
| 119 double x = 0.0; | 136 double x = 0.0; |
| 120 RenderBox child = firstChild; | 137 RenderBox child = firstChild; |
| 121 while (child != null) { | 138 while (child != null) { |
| 122 child.layout(tabConstraints); | 139 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
| |
| 123 assert(child.parentData is TabBarParentData); | 140 assert(child.parentData is TabBarParentData); |
| 124 child.parentData.position = new Point(x, 0.0); | 141 child.parentData.position = new Point(x, 0.0); |
| 125 x += tabWidth; | 142 x += tabWidth; |
| 126 child = child.parentData.nextSibling; | 143 child = child.parentData.nextSibling; |
| 127 } | 144 } |
| 128 } | 145 } |
| 129 | 146 |
| 147 void layoutScrollableTabs() { | |
| 148 BoxConstraints tabConstraints = new BoxConstraints( | |
| 149 minWidth: _kMinTabWidth, | |
| 150 maxWidth: math.min(size.width - _kRelativeMaxTabWidth, _kMaxTabWidth), | |
| 151 minHeight: size.height, | |
| 152 maxHeight: size.height); | |
| 153 double x = 0.0; | |
| 154 RenderBox child = firstChild; | |
| 155 while (child != null) { | |
| 156 child.layout(tabConstraints, parentUsesSize: true); | |
| 157 assert(child.parentData is TabBarParentData); | |
| 158 child.parentData.position = new Point(x, 0.0); | |
| 159 x += child.size.width; | |
| 160 child = child.parentData.nextSibling; | |
| 161 } | |
| 162 } | |
| 163 | |
| 164 Size layoutSize; | |
| 165 List<double> layoutWidths; | |
| 166 LayoutChanged onLayoutChanged; | |
| 167 | |
| 168 void reportLayoutChanged() { | |
| 169 assert(onLayoutChanged != null); | |
| 170 RenderBox child = firstChild; | |
| 171 List<double> widths = new List<double>(childCount); | |
| 172 int childIndex = 0; | |
| 173 while (child != null) { | |
| 174 widths[childIndex++] = child.size.width; | |
| 175 child = child.parentData.nextSibling; | |
| 176 } | |
| 177 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.
| |
| 178 layoutSize = size; | |
| 179 layoutWidths = widths; | |
| 180 onLayoutChanged(layoutSize, layoutWidths); | |
| 181 } | |
| 182 } | |
| 183 | |
| 184 void performLayout() { | |
| 185 assert(constraints is BoxConstraints); | |
| 186 | |
| 187 size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight)); | |
| 188 assert(!size.isInfinite); | |
| 189 | |
| 190 if (childCount == 0) | |
| 191 return; | |
| 192 | |
| 193 if (scrollable) | |
| 194 layoutScrollableTabs(); | |
| 195 else | |
| 196 layoutFixedWidthTabs(); | |
| 197 | |
| 198 if (onLayoutChanged != null) | |
| 199 reportLayoutChanged(); | |
|
abarth-chromium
2015/07/02 15:27:21
reportLayoutChangedIfNeeded()
hansmuller
2015/07/06 17:21:46
Done.
| |
| 200 } | |
| 201 | |
| 130 void hitTestChildren(HitTestResult result, { Point position }) { | 202 void hitTestChildren(HitTestResult result, { Point position }) { |
| 131 defaultHitTestChildren(result, position: position); | 203 defaultHitTestChildren(result, position: position); |
| 132 } | 204 } |
| 133 | 205 |
| 134 void _paintIndicator(PaintingCanvas canvas, RenderBox selectedTab, Offset offs et) { | 206 void _paintIndicator(PaintingCanvas canvas, RenderBox selectedTab, Offset offs et) { |
| 135 if (indicatorColor == null) | 207 if (indicatorColor == null) |
| 136 return; | 208 return; |
| 137 | 209 |
| 138 var size = new Size(selectedTab.size.width, _kTabIndicatorHeight); | 210 var size = new Size(selectedTab.size.width, _kTabIndicatorHeight); |
| 139 var point = new Point( | 211 var point = new Point( |
| 140 selectedTab.parentData.position.x, | 212 selectedTab.parentData.position.x, |
| 141 _tabBarHeight - _kTabIndicatorHeight | 213 _tabBarHeight - _kTabIndicatorHeight |
| 142 ); | 214 ); |
| 143 Rect rect = (point + offset) & size; | 215 Rect rect = (point + offset) & size; |
| 144 canvas.drawRect(rect, new Paint()..color = indicatorColor); | 216 canvas.drawRect(rect, new Paint()..color = indicatorColor); |
| 145 } | 217 } |
| 146 | 218 |
| 147 void paint(PaintingCanvas canvas, Offset offset) { | 219 void paint(PaintingCanvas canvas, Offset offset) { |
| 148 if (backgroundColor != null) { | 220 if (backgroundColor != null) { |
| 149 Rect rect = offset & size; | 221 double width = layoutWidths != null |
| 222 ? layoutWidths.reduce((sum, width) => sum + width) | |
| 223 : size.width; | |
| 224 Rect rect = offset & new Size(width, size.height); | |
| 150 canvas.drawRect(rect, new Paint()..color = backgroundColor); | 225 canvas.drawRect(rect, new Paint()..color = backgroundColor); |
| 151 } | 226 } |
| 152 | 227 |
| 153 int index = 0; | 228 int index = 0; |
| 154 RenderBox child = firstChild; | 229 RenderBox child = firstChild; |
| 155 while (child != null) { | 230 while (child != null) { |
| 156 assert(child.parentData is TabBarParentData); | 231 assert(child.parentData is TabBarParentData); |
| 157 canvas.paintChild(child, child.parentData.position + offset); | 232 canvas.paintChild(child, child.parentData.position + offset); |
| 158 if (index++ == selectedIndex) | 233 if (index++ == selectedIndex) |
| 159 _paintIndicator(canvas, child, offset); | 234 _paintIndicator(canvas, child, offset); |
| 160 child = child.parentData.nextSibling; | 235 child = child.parentData.nextSibling; |
| 161 } | 236 } |
| 162 } | 237 } |
| 163 } | 238 } |
| 164 | 239 |
| 165 class TabBarWrapper extends MultiChildRenderObjectWrapper { | 240 class TabBarWrapper extends MultiChildRenderObjectWrapper { |
| 166 TabBarWrapper({ | 241 TabBarWrapper({ |
| 167 List<Widget> children, | 242 List<Widget> children, |
| 168 this.selectedIndex, | 243 this.selectedIndex, |
| 169 this.backgroundColor, | 244 this.backgroundColor, |
| 170 this.indicatorColor, | 245 this.indicatorColor, |
| 171 this.textAndIcons, | 246 this.textAndIcons, |
| 247 this.scrollable: false, | |
| 248 this.onLayoutChanged, | |
| 172 String key | 249 String key |
| 173 }) : super(key: key, children: children); | 250 }) : super(key: key, children: children); |
| 174 | 251 |
| 175 final int selectedIndex; | 252 final int selectedIndex; |
| 176 final Color backgroundColor; | 253 final Color backgroundColor; |
| 177 final Color indicatorColor; | 254 final Color indicatorColor; |
| 178 final bool textAndIcons; | 255 final bool textAndIcons; |
| 256 final bool scrollable; | |
| 257 final LayoutChanged onLayoutChanged; | |
| 179 | 258 |
| 180 RenderTabBar get root => super.root; | 259 RenderTabBar get root => super.root; |
| 181 RenderTabBar createNode() => new RenderTabBar(); | 260 RenderTabBar createNode() => new RenderTabBar(onLayoutChanged); |
| 182 | 261 |
| 183 void syncRenderObject(Widget old) { | 262 void syncRenderObject(Widget old) { |
| 184 super.syncRenderObject(old); | 263 super.syncRenderObject(old); |
| 185 root.selectedIndex = selectedIndex; | 264 root.selectedIndex = selectedIndex; |
| 186 root.backgroundColor = backgroundColor; | 265 root.backgroundColor = backgroundColor; |
| 187 root.indicatorColor = indicatorColor; | 266 root.indicatorColor = indicatorColor; |
| 188 root.textAndIcons = textAndIcons; | 267 root.textAndIcons = textAndIcons; |
| 268 root.scrollable = scrollable; | |
| 269 root.onLayoutChanged = onLayoutChanged; | |
| 189 } | 270 } |
| 190 } | 271 } |
| 191 | 272 |
| 192 class TabLabel { | 273 class TabLabel { |
| 193 const TabLabel({ this.text, this.icon }); | 274 const TabLabel({ this.text, this.icon }); |
| 194 | 275 |
| 195 final String text; | 276 final String text; |
| 196 final String icon; | 277 final String icon; |
| 197 } | 278 } |
| 198 | 279 |
| 199 class Tab extends Component { | 280 class Tab extends Component { |
| 200 Tab({ | 281 Tab({ |
| 201 String key, | 282 String key, |
| 202 this.label, | 283 this.label, |
| 203 this.selected: false | 284 this.selected: false |
| 204 }) : super(key: key) { | 285 }) : super(key: key) { |
| 205 assert(label.text != null || label.icon != null); | 286 assert(label.text != null || label.icon != null); |
| 206 } | 287 } |
| 207 | 288 |
| 208 final TabLabel label; | 289 final TabLabel label; |
| 209 final bool selected; | 290 final bool selected; |
| 210 | 291 |
| 211 Widget _buildLabelText() { | 292 Widget _buildLabelText() { |
| 212 assert(label.text != null); | 293 assert(label.text != null); |
| 213 return new Text(label.text, style: Theme.of(this).toolbarText.button); | 294 TextStyle textStyle = Theme.of(this).toolbarText.button.merge(_kTabTextStyle ); |
| 295 return new Text(label.text, style: textStyle); | |
| 214 } | 296 } |
| 215 | 297 |
| 216 Widget _buildLabelIcon() { | 298 Widget _buildLabelIcon() { |
| 217 assert(label.icon != null); | 299 assert(label.icon != null); |
| 218 return new Icon(type: label.icon, size: _kTabIconSize); | 300 return new Icon(type: label.icon, size: _kTabIconSize); |
| 219 } | 301 } |
| 220 | 302 |
| 221 Widget build() { | 303 Widget build() { |
| 222 Widget labelContents; | 304 Widget labelContents; |
| 223 if (label.icon == null) { | 305 if (label.icon == null) { |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 239 ); | 321 ); |
| 240 } | 322 } |
| 241 | 323 |
| 242 Widget highlightedLabel = new Opacity( | 324 Widget highlightedLabel = new Opacity( |
| 243 child: labelContents, | 325 child: labelContents, |
| 244 opacity: selected ? 1.0 : 0.7 | 326 opacity: selected ? 1.0 : 0.7 |
| 245 ); | 327 ); |
| 246 | 328 |
| 247 Container centeredLabel = new Container( | 329 Container centeredLabel = new Container( |
| 248 child: new Center(child: highlightedLabel), | 330 child: new Center(child: highlightedLabel), |
| 249 constraints: new BoxConstraints(minWidth: _kMinTabWidth) | 331 constraints: new BoxConstraints(minWidth: _kMinTabWidth), |
| 332 padding: _kTabLabelPadding | |
| 250 ); | 333 ); |
| 251 | 334 |
| 252 return new InkWell(child: centeredLabel); | 335 return new InkWell(child: centeredLabel); |
| 253 } | 336 } |
| 254 } | 337 } |
| 255 | 338 |
| 256 class TabBar extends Component { | 339 class TabBar extends Scrollable { |
| 257 TabBar({ | 340 TabBar({ |
| 258 String key, | 341 String key, |
| 259 this.labels, | 342 this.labels, |
| 260 this.selectedIndex: 0, | 343 this.selectedIndex: 0, |
| 261 this.onChanged | 344 this.onChanged, |
| 262 }) : super(key: key); | 345 this.scrollable: false |
| 346 }) : super(key: key, direction: ScrollDirection.horizontal); | |
| 263 | 347 |
| 264 final Iterable<TabLabel> labels; | 348 Iterable<TabLabel> labels; |
| 265 final int selectedIndex; | 349 int selectedIndex; |
| 266 final SelectedIndexChanged onChanged; | 350 SelectedIndexChanged onChanged; |
| 351 bool scrollable; | |
| 352 | |
| 353 void syncFields(TabBar source) { | |
| 354 super.syncFields(source); | |
| 355 labels = source.labels; | |
| 356 selectedIndex = source.selectedIndex; | |
| 357 onChanged = source.onChanged; | |
| 358 scrollable = source.scrollable; | |
| 359 } | |
| 360 | |
| 361 ScrollBehavior createScrollBehavior() => new BoundedScrollBehavior(); | |
| 362 BoundedScrollBehavior get scrollBehavior => super.scrollBehavior; | |
| 267 | 363 |
| 268 void _handleTap(int tabIndex) { | 364 void _handleTap(int tabIndex) { |
| 269 if (tabIndex != selectedIndex && onChanged != null) | 365 if (tabIndex != selectedIndex && onChanged != null) |
| 270 onChanged(tabIndex); | 366 onChanged(tabIndex); |
| 271 } | 367 } |
| 272 | 368 |
| 273 Widget _toTab(TabLabel label, int tabIndex) { | 369 Widget _toTab(TabLabel label, int tabIndex) { |
| 274 Tab tab = new Tab( | 370 Tab tab = new Tab( |
| 275 label: label, | 371 label: label, |
| 276 selected: tabIndex == selectedIndex, | 372 selected: tabIndex == selectedIndex, |
| 277 key: label.text == null ? label.icon : label.text | 373 key: label.text == null ? label.icon : label.text |
| 278 ); | 374 ); |
| 279 return new Listener( | 375 return new Listener( |
| 280 child: tab, | 376 child: tab, |
| 281 onGestureTap: (_) => _handleTap(tabIndex) | 377 onGestureTap: (_) => _handleTap(tabIndex) |
| 282 ); | 378 ); |
| 283 } | 379 } |
| 284 | 380 |
| 285 Widget build() { | 381 Size _tabBarSize; |
| 382 List<double> _tabWidths; | |
| 383 | |
| 384 void _layoutChanged(Size tabBarSize, List<double> tabWidths) { | |
| 385 setState(() { | |
| 386 _tabBarSize = tabBarSize; | |
| 387 _tabWidths = tabWidths; | |
| 388 scrollBehavior.maxOffset = | |
| 389 _tabWidths.reduce((sum, width) => sum + width) - _tabBarSize.width; | |
| 390 }); | |
| 391 } | |
| 392 | |
| 393 Widget buildContent() { | |
| 286 assert(labels != null && labels.isNotEmpty); | 394 assert(labels != null && labels.isNotEmpty); |
| 287 List<Widget> tabs = <Widget>[]; | 395 List<Widget> tabs = <Widget>[]; |
| 288 bool textAndIcons = false; | 396 bool textAndIcons = false; |
| 289 int tabIndex = 0; | 397 int tabIndex = 0; |
| 290 for (TabLabel label in labels) { | 398 for (TabLabel label in labels) { |
| 291 tabs.add(_toTab(label, tabIndex++)); | 399 tabs.add(_toTab(label, tabIndex++)); |
| 292 if (label.text != null && label.icon != null) | 400 if (label.text != null && label.icon != null) |
| 293 textAndIcons = true; | 401 textAndIcons = true; |
| 294 } | 402 } |
| 295 return new TabBarWrapper( | 403 |
| 404 TabBarWrapper tabBarWrapper = new TabBarWrapper( | |
| 296 children: tabs, | 405 children: tabs, |
| 297 selectedIndex: selectedIndex, | 406 selectedIndex: selectedIndex, |
| 298 backgroundColor: Theme.of(this).primary[500], | 407 backgroundColor: Theme.of(this).primary[500], |
| 299 indicatorColor: Theme.of(this).accent[200], | 408 indicatorColor: Theme.of(this).accent[200], |
| 300 textAndIcons: textAndIcons | 409 textAndIcons: textAndIcons, |
| 410 scrollable: scrollable, | |
| 411 onLayoutChanged: scrollable ? _layoutChanged : null | |
| 301 ); | 412 ); |
| 413 | |
| 414 Matrix4 transform = new Matrix4.identity(); | |
| 415 transform.translate(-scrollOffset, 0.0); | |
| 416 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
| |
| 302 } | 417 } |
| 303 } | 418 } |
| 304 | 419 |
| 305 class TabNavigatorView { | 420 class TabNavigatorView { |
| 306 TabNavigatorView({ this.label, this.builder }); | 421 TabNavigatorView({ this.label, this.builder }); |
| 307 | 422 |
| 308 final TabLabel label; | 423 final TabLabel label; |
| 309 final Builder builder; | 424 final Builder builder; |
| 310 | 425 |
| 311 Widget buildContent() { | 426 Widget buildContent() { |
| 312 assert(builder != null); | 427 assert(builder != null); |
| 313 Widget content = builder(); | 428 Widget content = builder(); |
| 314 assert(content != null); | 429 assert(content != null); |
| 315 return content; | 430 return content; |
| 316 } | 431 } |
| 317 } | 432 } |
| 318 | 433 |
| 319 class TabNavigator extends Component { | 434 class TabNavigator extends Component { |
| 320 TabNavigator({ | 435 TabNavigator({ |
| 321 String key, | 436 String key, |
| 322 this.views, | 437 this.views, |
| 323 this.selectedIndex: 0, | 438 this.selectedIndex: 0, |
| 324 this.onChanged | 439 this.onChanged, |
| 440 this.scrollable: false | |
| 325 }) : super(key: key); | 441 }) : super(key: key); |
| 326 | 442 |
| 327 final List<TabNavigatorView> views; | 443 final List<TabNavigatorView> views; |
| 328 final int selectedIndex; | 444 final int selectedIndex; |
| 329 final SelectedIndexChanged onChanged; | 445 final SelectedIndexChanged onChanged; |
| 446 final bool scrollable; | |
| 330 | 447 |
| 331 void _handleSelectedIndexChanged(int tabIndex) { | 448 void _handleSelectedIndexChanged(int tabIndex) { |
| 332 if (onChanged != null) | 449 if (onChanged != null) |
| 333 onChanged(tabIndex); | 450 onChanged(tabIndex); |
| 334 } | 451 } |
| 335 | 452 |
| 336 Widget build() { | 453 Widget build() { |
| 337 assert(views != null && views.isNotEmpty); | 454 assert(views != null && views.isNotEmpty); |
| 338 assert(selectedIndex >= 0 && selectedIndex < views.length); | 455 assert(selectedIndex >= 0 && selectedIndex < views.length); |
| 339 | 456 |
| 340 TabBar tabBar = new TabBar( | 457 TabBar tabBar = new TabBar( |
| 341 labels: views.map((view) => view.label), | 458 labels: views.map((view) => view.label), |
| 342 onChanged: _handleSelectedIndexChanged, | 459 onChanged: _handleSelectedIndexChanged, |
| 343 selectedIndex: selectedIndex | 460 selectedIndex: selectedIndex, |
| 461 scrollable: scrollable | |
| 344 ); | 462 ); |
| 345 | 463 |
| 346 Widget content = views[selectedIndex].buildContent(); | 464 Widget content = views[selectedIndex].buildContent(); |
| 347 return new Flex([tabBar, new Flexible(child: content)], | 465 return new Flex([tabBar, new Flexible(child: content)], |
| 348 direction: FlexDirection.vertical | 466 direction: FlexDirection.vertical |
| 349 ); | 467 ); |
| 350 } | 468 } |
| 351 } | 469 } |
| OLD | NEW |