tabs.dart 24.5 KB
Newer Older
1 2 3 4
// 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.

Hans Muller's avatar
Hans Muller committed
5
import 'dart:async';
6 7
import 'dart:math' as math;

8
import 'package:newton/newton.dart';
9 10 11
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
12 13 14

import 'colors.dart';
import 'icon.dart';
Adam Barth's avatar
Adam Barth committed
15 16
import 'icon_theme.dart';
import 'icon_theme_data.dart';
17
import 'ink_well.dart';
18
import 'material.dart';
19
import 'theme.dart';
20

21 22
typedef void TabSelectedIndexChanged(int selectedIndex);
typedef void TabLayoutChanged(Size size, List<double> widths);
23 24 25 26 27 28 29 30

// 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);
31
const double _kTabBarScrollDrag = 0.025;
32
const Duration _kTabBarScroll = const Duration(milliseconds: 300);
33

34 35 36 37
// The scrollOffset (velocity) provided to fling() is pixels/ms, and the
// tolerance velocity is pixels/sec.
final double _kMinFlingVelocity = kPixelScrollTolerance.velocity / 2000.0;

Hixie's avatar
Hixie committed
38
class _TabBarParentData extends ContainerBoxParentDataMixin<RenderBox> { }
39

40 41 42
class _RenderTabBar extends RenderBox with
    ContainerRenderObjectMixin<RenderBox, _TabBarParentData>,
    RenderBoxContainerDefaultsMixin<RenderBox, _TabBarParentData> {
43

44
  _RenderTabBar(this.onLayoutChanged);
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

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

64 65 66 67 68 69 70 71 72
  Rect _indicatorRect;
  Rect get indicatorRect => _indicatorRect;
  void set indicatorRect(Rect value) {
    if (_indicatorRect != value) {
      _indicatorRect = value;
      markNeedsPaint();
    }
  }

73 74 75 76 77 78 79 80 81
  bool _textAndIcons;
  bool get textAndIcons => _textAndIcons;
  void set textAndIcons(bool value) {
    if (_textAndIcons != value) {
      _textAndIcons = value;
      markNeedsLayout();
    }
  }

82 83 84 85 86
  bool _isScrollable;
  bool get isScrollable => _isScrollable;
  void set isScrollable(bool value) {
    if (_isScrollable != value) {
      _isScrollable = value;
87 88 89 90 91
      markNeedsLayout();
    }
  }

  void setupParentData(RenderBox child) {
92 93
    if (child.parentData is! _TabBarParentData)
      child.parentData = new _TabBarParentData();
94 95 96 97 98 99 100 101 102 103
  }

  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));
Hixie's avatar
Hixie committed
104 105
      final _TabBarParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
106
    }
107
    double width = isScrollable ? maxWidth : maxWidth * childCount;
108 109 110 111 112 113 114 115 116 117 118
    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));
Hixie's avatar
Hixie committed
119 120
      final _TabBarParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
121
    }
122
    double width = isScrollable ? maxWidth : maxWidth * childCount;
123 124 125
    return constraints.constrainWidth(width);
  }

Hans Muller's avatar
Hans Muller committed
126 127
  double get _tabHeight => textAndIcons ? _kTextAndIconTabHeight : _kTabHeight;
  double get _tabBarHeight => _tabHeight + _kTabIndicatorHeight;
128 129 130 131 132 133 134 135 136 137

  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 =
Hans Muller's avatar
Hans Muller committed
138
      new BoxConstraints.tightFor(width: tabWidth, height: _tabHeight);
139 140 141 142
    double x = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      child.layout(tabConstraints);
Hixie's avatar
Hixie committed
143 144
      final _TabBarParentData childParentData = child.parentData;
      childParentData.position = new Point(x, 0.0);
145
      x += tabWidth;
Hixie's avatar
Hixie committed
146
      child = childParentData.nextSibling;
147 148 149
    }
  }

Hans Muller's avatar
Hans Muller committed
150
  double layoutScrollableTabs() {
151 152
    BoxConstraints tabConstraints = new BoxConstraints(
      minWidth: _kMinTabWidth,
Hans Muller's avatar
Hans Muller committed
153 154 155 156
      maxWidth: _kMaxTabWidth,
      minHeight: _tabHeight,
      maxHeight: _tabHeight
    );
157 158 159 160
    double x = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      child.layout(tabConstraints, parentUsesSize: true);
Hixie's avatar
Hixie committed
161 162
      final _TabBarParentData childParentData = child.parentData;
      childParentData.position = new Point(x, 0.0);
163
      x += child.size.width;
Hixie's avatar
Hixie committed
164
      child = childParentData.nextSibling;
165
    }
Hans Muller's avatar
Hans Muller committed
166
    return x;
167 168 169 170
  }

  Size layoutSize;
  List<double> layoutWidths;
171
  TabLayoutChanged onLayoutChanged;
172 173 174 175

  void reportLayoutChangedIfNeeded() {
    assert(onLayoutChanged != null);
    List<double> widths = new List<double>(childCount);
176
    if (!isScrollable && childCount > 0) {
177
      double tabWidth = size.width / childCount;
178
      widths.fillRange(0, widths.length, tabWidth);
179
    } else if (isScrollable) {
180 181 182 183
      RenderBox child = firstChild;
      int childIndex = 0;
      while (child != null) {
        widths[childIndex++] = child.size.width;
Hixie's avatar
Hixie committed
184 185
        final _TabBarParentData childParentData = child.parentData;
        child = childParentData.nextSibling;
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
      }
      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;

Hans Muller's avatar
Hans Muller committed
201 202 203 204 205
    if (isScrollable) {
      double tabBarWidth = layoutScrollableTabs();
      size = constraints.constrain(new Size(tabBarWidth, _tabBarHeight));
    } else {
      size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight));
206
      layoutFixedWidthTabs();
Hans Muller's avatar
Hans Muller committed
207
    }
208 209 210 211 212

    if (onLayoutChanged != null)
      reportLayoutChangedIfNeeded();
  }

Adam Barth's avatar
Adam Barth committed
213 214
  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
215 216
  }

Adam Barth's avatar
Adam Barth committed
217
  void _paintIndicator(Canvas canvas, RenderBox selectedTab, Offset offset) {
218 219 220
    if (indicatorColor == null)
      return;

221
    if (indicatorRect != null) {
Hans Muller's avatar
Hans Muller committed
222
      canvas.drawRect(indicatorRect.shift(offset), new Paint()..color = indicatorColor);
223 224 225
      return;
    }

Hixie's avatar
Hixie committed
226 227 228 229
    final Size size = new Size(selectedTab.size.width, _kTabIndicatorHeight);
    final _TabBarParentData selectedTabParentData = selectedTab.parentData;
    final Point point = new Point(
      selectedTabParentData.position.x,
230 231
      _tabBarHeight - _kTabIndicatorHeight
    );
Hixie's avatar
Hixie committed
232
    canvas.drawRect((point + offset) & size, new Paint()..color = indicatorColor);
233 234
  }

235
  void paint(PaintingContext context, Offset offset) {
236 237 238
    int index = 0;
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
239
      final _TabBarParentData childParentData = child.parentData;
Adam Barth's avatar
Adam Barth committed
240
      context.paintChild(child, childParentData.offset + offset);
241
      if (index++ == selectedIndex)
242
        _paintIndicator(context.canvas, child, offset);
Hixie's avatar
Hixie committed
243
      child = childParentData.nextSibling;
244 245 246 247
    }
  }
}

248 249
class _TabBarWrapper extends MultiChildRenderObjectWidget {
  _TabBarWrapper({
250
    Key key,
251 252 253
    List<Widget> children,
    this.selectedIndex,
    this.indicatorColor,
254
    this.indicatorRect,
255
    this.textAndIcons,
256
    this.isScrollable: false,
257
    this.onLayoutChanged
258 259 260 261
  }) : super(key: key, children: children);

  final int selectedIndex;
  final Color indicatorColor;
262
  final Rect indicatorRect;
263
  final bool textAndIcons;
264
  final bool isScrollable;
265
  final TabLayoutChanged onLayoutChanged;
266

267 268
  _RenderTabBar createRenderObject() {
    _RenderTabBar result = new _RenderTabBar(onLayoutChanged);
269 270 271
    updateRenderObject(result, null);
    return result;
  }
272

273
  void updateRenderObject(_RenderTabBar renderObject, _TabBarWrapper oldWidget) {
274 275 276 277 278 279
    renderObject.selectedIndex = selectedIndex;
    renderObject.indicatorColor = indicatorColor;
    renderObject.indicatorRect = indicatorRect;
    renderObject.textAndIcons = textAndIcons;
    renderObject.isScrollable = isScrollable;
    renderObject.onLayoutChanged = onLayoutChanged;
280 281 282 283 284 285 286 287
  }
}

class TabLabel {
  const TabLabel({ this.text, this.icon });

  final String text;
  final String icon;
Hixie's avatar
Hixie committed
288 289 290 291 292 293 294 295 296 297

  String toString() {
    if (text != null && icon != null)
      return '"$text" ($icon)';
    if (text != null)
      return '"$text"';
    if (icon != null)
      return '$icon';
    return 'EMPTY TAB LABEL';
  }
298 299
}

300 301
class _Tab extends StatelessComponent {
  _Tab({
302
    Key key,
303
    this.onSelected,
304
    this.label,
305
    this.color
306 307 308 309
  }) : super(key: key) {
    assert(label.text != null || label.icon != null);
  }

310
  final VoidCallback onSelected;
311
  final TabLabel label;
Hans Muller's avatar
Hans Muller committed
312
  final Color color;
313 314 315

  Widget _buildLabelText() {
    assert(label.text != null);
316
    TextStyle style = new TextStyle(color: color);
Hans Muller's avatar
Hans Muller committed
317
    return new Text(label.text, style: style);
318 319 320 321
  }

  Widget _buildLabelIcon() {
    assert(label.icon != null);
322
    ColorFilter filter = new ColorFilter.mode(color, TransferMode.srcATop);
323
    return new Icon(icon: label.icon, colorFilter: filter);
324 325
  }

326
  Widget build(BuildContext context) {
327
    Widget labelContent;
328
    if (label.icon == null) {
329
      labelContent = _buildLabelText();
330
    } else if (label.text == null) {
331
      labelContent = _buildLabelIcon();
332
    } else {
333
      labelContent = new Column(
334 335 336 337 338 339 340 341
        <Widget>[
          new Container(
            child: _buildLabelIcon(),
            margin: const EdgeDims.only(bottom: 10.0)
          ),
          _buildLabelText()
        ],
        justifyContent: FlexJustifyContent.center,
342
        alignItems: FlexAlignItems.center
343 344 345 346
      );
    }

    Container centeredLabel = new Container(
347
      child: new Center(child: labelContent, widthFactor: 1.0, heightFactor: 1.0),
348 349 350 351
      constraints: new BoxConstraints(minWidth: _kMinTabWidth),
      padding: _kTabLabelPadding
    );

352 353 354 355
    return new InkWell(
      onTap: onSelected,
      child: centeredLabel
    );
356
  }
Hixie's avatar
Hixie committed
357 358 359 360 361

  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$label');
  }
362 363
}

364
class _TabsScrollBehavior extends BoundedBehavior {
365
  _TabsScrollBehavior();
366 367 368

  bool isScrollable = true;

369
  Simulation createFlingScrollSimulation(double position, double velocity) {
370 371 372 373 374 375 376 377 378 379 380 381 382 383
    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;
  }
}

384
class TabBarSelection {
385 386 387 388 389
  TabBarSelection({ int index: 0, this.maxIndex, this.onChanged }) : _index = index {
    assert(maxIndex != null);
    assert(index != null);
    assert(_index >= 0 && _index <= maxIndex);
  }
390 391

  final VoidCallback onChanged;
392
  final int maxIndex;
393 394 395 396

  PerformanceView get performance => _performance.view;
  final _performance = new Performance(duration: _kTabBarScroll, progress: 1.0);

Hans Muller's avatar
Hans Muller committed
397 398 399
  bool _indexIsChanging = false;
  bool get indexIsChanging => _indexIsChanging;

400 401 402 403 404 405 406
  int get index => _index;
  int _index;
  void set index(int value) {
    if (value == _index)
      return;
    _previousIndex = _index;
    _index = value;
Hans Muller's avatar
Hans Muller committed
407
    _indexIsChanging = true;
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431

    // If the selected index change was triggered by a drag gesture, the current
    // value of _performance.progress 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 selected index was 0 or maxIndex. 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 progress;
    if (_performance.status == PerformanceStatus.completed)
      progress = 0.0;
    else if (_previousIndex == 0)
      progress = _performance.progress;
    else if (_previousIndex == maxIndex)
      progress = 1.0 - _performance.progress;
    else if (_previousIndex < _index)
      progress = (_performance.progress - 0.5) * 2.0;
    else
      progress = 1.0 - _performance.progress * 2.0;

432
    _performance
433 434
      ..progress = progress
      ..forward().then((_) {
Hans Muller's avatar
Hans Muller committed
435 436 437
        if (onChanged != null)
          onChanged();
        _indexIsChanging = false;
438 439 440 441 442 443 444
      });
  }

  int get previousIndex => _previousIndex;
  int _previousIndex = 0;
}

445 446 447 448
/// A tab strip, consisting of several TabLabels and a TabBarSelection.
/// The TabBarSelection can be used to link this to a TabBarView.
///
/// Tabs must always have an ancestor Material object.
449 450
class TabBar extends Scrollable {
  TabBar({
451
    Key key,
452
    this.labels,
453
    this.selection,
454
    this.isScrollable: false
455 456
  }) : super(key: key, scrollDirection: ScrollDirection.horizontal) {
    assert(labels != null);
457
    assert(labels.length > 1);
458
    assert(selection != null);
459
    assert(selection.maxIndex == labels.length - 1);
460
  }
461

462
  final Iterable<TabLabel> labels;
463
  final TabBarSelection selection;
464
  final bool isScrollable;
465

466
  _TabBarState createState() => new _TabBarState();
467
}
468

469
class _TabBarState extends ScrollableState<TabBar> {
470 471
  void initState() {
    super.initState();
472
    scrollBehavior.isScrollable = config.isScrollable;
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
    config.selection._performance
      ..addStatusListener(_handleStatusChange)
      ..addListener(_handleProgressChange);
  }

  void dispose() {
    config.selection._performance
      ..removeStatusListener(_handleStatusChange)
      ..removeListener(_handleProgressChange)
      ..stop();
    super.dispose();
  }

  Performance get _performance => config.selection._performance;

Hans Muller's avatar
Hans Muller committed
488 489 490
  int get _tabCount => config.labels.length;

  bool _indexIsChanging = false;
491 492

  void _handleStatusChange(PerformanceStatus status) {
Hans Muller's avatar
Hans Muller committed
493 494 495 496 497 498 499 500 501 502
    if (_tabCount == 0)
      return;

    if (_indexIsChanging && status == PerformanceStatus.completed) {
      _indexIsChanging = false;
      double progress = 0.5;
      if (config.selection.index == 0)
        progress = 0.0;
      else if (config.selection.index == _tabCount - 1)
        progress = 1.0;
503 504
      setState(() {
        _indicatorRect
Hans Muller's avatar
Hans Muller committed
505 506 507 508
          ..begin = _tabIndicatorRect(math.max(0, config.selection.index - 1))
          ..end = _tabIndicatorRect(math.min(_tabCount - 1, config.selection.index + 1))
          ..curve = null
          ..setProgress(progress, AnimationDirection.forward);
509 510
      });
    }
511
  }
512

513
  void _handleProgressChange() {
Hans Muller's avatar
Hans Muller committed
514 515 516 517 518 519 520 521 522 523 524
    if (_tabCount == 0)
      return;

    if (!_indexIsChanging && config.selection.indexIsChanging) {
      if (config.isScrollable)
        scrollTo(_centeredTabScrollOffset(config.selection.index), duration: _kTabBarScroll);
      _indicatorRect
        ..begin = _indicatorRect.value ?? _tabIndicatorRect(config.selection.previousIndex)
        ..end = _tabIndicatorRect(config.selection.index)
        ..curve = Curves.ease;
      _indexIsChanging = true;
525
    }
Hans Muller's avatar
Hans Muller committed
526 527 528
    setState(() {
      _indicatorRect.setProgress(_performance.progress, AnimationDirection.forward);
    });
529 530
  }

531 532 533
  Size _viewportSize = Size.zero;
  Size _tabBarSize;
  List<double> _tabWidths;
Hans Muller's avatar
Hans Muller committed
534
  AnimatedRectValue _indicatorRect = new AnimatedRectValue(null);
535

536 537 538 539 540 541
  Rect _tabRect(int tabIndex) {
    assert(_tabBarSize != null);
    assert(_tabWidths != null);
    assert(tabIndex >= 0 && tabIndex < _tabWidths.length);
    double tabLeft = 0.0;
    if (tabIndex > 0)
Hixie's avatar
Hixie committed
542
      tabLeft = _tabWidths.take(tabIndex).reduce((double sum, double width) => sum + width);
Hans Muller's avatar
Hans Muller committed
543 544 545
    final double tabTop = 0.0;
    final double tabBottom = _tabBarSize.height - _kTabIndicatorHeight;
    final double tabRight = tabLeft + _tabWidths[tabIndex];
546 547 548 549 550 551 552 553
    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);
  }

554 555 556 557 558 559 560 561 562
  void didUpdateConfig(TabBar oldConfig) {
    super.didUpdateConfig(oldConfig);
    if (!config.isScrollable)
      scrollTo(0.0);
  }

  ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior();
  _TabsScrollBehavior get scrollBehavior => super.scrollBehavior;

563
  double _centeredTabScrollOffset(int tabIndex) {
564
    double viewportWidth = scrollBehavior.containerExtent;
565 566
    Rect tabRect = _tabRect(tabIndex);
    return (tabRect.left + tabRect.width / 2.0 - viewportWidth / 2.0)
567 568 569
      .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
  }

570
  void _handleTabSelected(int tabIndex) {
571 572 573 574
    if (tabIndex != config.selection.index)
      setState(() {
        config.selection.index = tabIndex;
      });
575 576
  }

Hans Muller's avatar
Hans Muller committed
577
  Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) {
Hans Muller's avatar
Hans Muller committed
578 579 580 581 582 583 584 585 586
    final bool isSelectedTab = tabIndex == config.selection.index;
    final bool isPreviouslySelectedTab = tabIndex == config.selection.previousIndex;
    Color labelColor = isSelectedTab ? selectedColor : color;
    if (config.selection.indexIsChanging) {
      if (isSelectedTab)
        labelColor = Color.lerp(color, selectedColor, _performance.progress);
      else if (isPreviouslySelectedTab)
        labelColor = Color.lerp(selectedColor, color, _performance.progress);
    }
587 588
    return new _Tab(
      onSelected: () { _handleTabSelected(tabIndex); },
589
      label: label,
590
      color: labelColor
591 592 593
    );
  }

Hans Muller's avatar
Hans Muller committed
594 595
  void _updateScrollBehavior() {
    scrollBehavior.updateExtents(
596
      containerExtent: config.scrollDirection == ScrollDirection.vertical ? _viewportSize.height : _viewportSize.width,
Hixie's avatar
Hixie committed
597
      contentExtent: _tabWidths.reduce((double sum, double width) => sum + width)
Hans Muller's avatar
Hans Muller committed
598 599 600
    );
  }

601 602 603 604
  void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
    setState(() {
      _tabBarSize = tabBarSize;
      _tabWidths = tabWidths;
Hans Muller's avatar
Hans Muller committed
605
      _updateScrollBehavior();
606 607 608
    });
  }

Hans Muller's avatar
Hans Muller committed
609 610 611
  void _handleViewportSizeChanged(Size newSize) {
    _viewportSize = newSize;
    _updateScrollBehavior();
612 613
    if (config.isScrollable)
      scrollTo(_centeredTabScrollOffset(config.selection.index), duration: _kTabBarScroll);
Hans Muller's avatar
Hans Muller committed
614 615
  }

616 617
  Widget buildContent(BuildContext context) {
    assert(config.labels != null && config.labels.isNotEmpty);
618
    assert(Material.of(context) != null);
619

620
    ThemeData themeData = Theme.of(context);
621
    Color backgroundColor = Material.of(context).color;
622
    Color indicatorColor = themeData.accentColor;
623
    if (indicatorColor == backgroundColor)
624
      indicatorColor = Colors.white;
625

Adam Barth's avatar
Adam Barth committed
626 627
    TextStyle textStyle = themeData.primaryTextTheme.body1;
    IconThemeData iconTheme = themeData.primaryIconTheme;
628

Hans Muller's avatar
Hans Muller committed
629 630 631
    List<Widget> tabs = <Widget>[];
    bool textAndIcons = false;
    int tabIndex = 0;
632
    for (TabLabel label in config.labels) {
Hans Muller's avatar
Hans Muller committed
633 634 635 636 637
      tabs.add(_toTab(label, tabIndex++, textStyle.color, indicatorColor));
      if (label.text != null && label.icon != null)
        textAndIcons = true;
    }

638
    Widget contents = new IconTheme(
Adam Barth's avatar
Adam Barth committed
639
      data: iconTheme,
Hans Muller's avatar
Hans Muller committed
640 641
      child: new DefaultTextStyle(
        style: textStyle,
Hans Muller's avatar
Hans Muller committed
642 643 644 645 646 647 648 649
        child: new _TabBarWrapper(
          children: tabs,
          selectedIndex: config.selection.index,
          indicatorColor: indicatorColor,
          indicatorRect: _indicatorRect.value,
          textAndIcons: textAndIcons,
          isScrollable: config.isScrollable,
          onLayoutChanged: _layoutChanged
650 651 652
        )
      )
    );
Hans Muller's avatar
Hans Muller committed
653

654
    if (config.isScrollable) {
655
      contents = new SizeObserver(
656
        onSizeChanged: _handleViewportSizeChanged,
657 658 659
        child: new Viewport(
          scrollDirection: ScrollDirection.horizontal,
          scrollOffset: new Offset(scrollOffset, 0.0),
660
          child: contents
661 662 663 664
        )
      );
    }

665
    return contents;
666 667 668
  }
}

669
class TabBarView<T> extends PageableList<T> {
670 671 672 673
  TabBarView({
    Key key,
    this.selection,
    List<T> items,
674
    ItemBuilder<T> itemBuilder
675 676 677 678 679 680 681
  }) : super(
    key: key,
    scrollDirection: ScrollDirection.horizontal,
    items: items,
    itemBuilder: itemBuilder,
    itemsWrap: false
  ) {
682 683
    assert(items != null);
    assert(items.length > 1);
684
    assert(selection != null);
685
    assert(selection.maxIndex == items.length - 1);
686 687 688 689 690 691 692
  }

  final TabBarSelection selection;

  _TabBarViewState createState() => new _TabBarViewState<T>();
}

693
class _TabBarViewState<T> extends PageableListState<T, TabBarView<T>> {
694 695 696
  List<int> _itemIndices = [0, 1];
  AnimationDirection _scrollDirection = AnimationDirection.forward;

Hans Muller's avatar
Hans Muller committed
697 698
  int get _tabCount => config.items.length;

699 700 701 702 703 704
  void _initItemIndicesAndScrollPosition() {
    final int selectedIndex = config.selection.index;

    if (selectedIndex == 0) {
      _itemIndices = <int>[0, 1];
      scrollTo(0.0);
Hans Muller's avatar
Hans Muller committed
705
    } else if (selectedIndex == _tabCount - 1) {
706
      _itemIndices = <int>[selectedIndex - 1, selectedIndex];
707
      scrollTo(1.0);
708 709
    } else {
      _itemIndices = <int>[selectedIndex - 1, selectedIndex, selectedIndex + 1];
710
      scrollTo(1.0);
711
    }
712
  }
713

Hans Muller's avatar
Hans Muller committed
714 715 716 717 718 719 720
  BoundedBehavior _boundedBehavior;

  ExtentScrollBehavior get scrollBehavior {
    _boundedBehavior ??= new BoundedBehavior();
    return _boundedBehavior;
  }

721
  Performance get _performance => config.selection._performance;
722

723 724 725 726 727 728
  void initState() {
    super.initState();
    _initItemIndicesAndScrollPosition();
    _performance
      ..addListener(_handleProgressChange);
  }
729

730 731 732 733 734 735
  void dispose() {
    _performance
      ..removeListener(_handleProgressChange)
      ..stop();
    super.dispose();
  }
736

737
  void _handleProgressChange() {
Hans Muller's avatar
Hans Muller committed
738 739 740 741
    if (!config.selection.indexIsChanging)
      return;
    // The TabBar is driving the TabBarSelection performance.

742
    if (_performance.status == PerformanceStatus.completed) {
743
      _initItemIndicesAndScrollPosition();
744
      return;
745 746
    }

747
    if (_performance.status != PerformanceStatus.forward)
Hans Muller's avatar
Hans Muller committed
748
      return;
749 750 751 752 753 754 755 756 757 758 759

    final int selectedIndex = config.selection.index;
    final int previousSelectedIndex = config.selection.previousIndex;

    if (selectedIndex < previousSelectedIndex) {
      _itemIndices = <int>[selectedIndex, previousSelectedIndex];
      _scrollDirection = AnimationDirection.reverse;
    } else {
      _itemIndices = <int>[previousSelectedIndex, selectedIndex];
      _scrollDirection = AnimationDirection.forward;
    }
Hans Muller's avatar
Hans Muller committed
760

761
    if (_scrollDirection == AnimationDirection.forward)
762
      scrollTo(_performance.progress);
763
    else
764
      scrollTo(1.0 - _performance.progress);
765 766 767 768 769 770 771 772 773 774
  }

  int get itemCount => _itemIndices.length;

  List<Widget> buildItems(BuildContext context, int start, int count) {
    return _itemIndices
      .skip(start)
      .take(count)
      .map((int i) => config.itemBuilder(context, config.items[i], i))
      .toList();
775
  }
Hans Muller's avatar
Hans Muller committed
776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792

  void dispatchOnScroll() {
    if (config.selection.indexIsChanging)
      return;
    // This class is driving the TabBarSelection's performance.

    if (config.selection.index == 0 || config.selection.index == _tabCount - 1)
      _performance.progress = scrollOffset;
    else
      _performance.progress = scrollOffset / 2.0;
  }

  Future fling(Offset scrollVelocity) {
    // TODO(hansmuller): should not short-circuit in this case.
    if (config.selection.indexIsChanging)
      return new Future.value();

793
    if (scrollVelocity.dx.abs() > _kMinFlingVelocity) {
Hans Muller's avatar
Hans Muller committed
794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
      final int selectionDelta = scrollVelocity.dx > 0 ? -1 : 1;
      config.selection.index = (config.selection.index + selectionDelta).clamp(0, _tabCount - 1);
      return new Future.value();
    }

    final int selectionIndex = config.selection.index;
    final int settleIndex = snapScrollOffset(scrollOffset).toInt();
    if (selectionIndex > 0 && settleIndex != 1) {
        config.selection.index += settleIndex == 2 ? 1 : -1;
        return new Future.value();
    } else if (selectionIndex == 0 && settleIndex == 1) {
      config.selection.index = 1;
      return new Future.value();
    }
    return settleScrollOffset();
  }

811
}