tabs.dart 42.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:flutter/physics.dart';
9 10
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
11
import 'package:meta/meta.dart';
12

13
import 'app_bar.dart';
14
import 'colors.dart';
15
import 'debug.dart';
16
import 'icon.dart';
Adam Barth's avatar
Adam Barth committed
17 18
import 'icon_theme.dart';
import 'icon_theme_data.dart';
19
import 'ink_well.dart';
20
import 'material.dart';
21
import 'theme.dart';
22

23
typedef void _TabLayoutChanged(Size size, List<double> widths);
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;
31
const EdgeInsets _kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0);
32
const double _kTabBarScrollDrag = 0.025;
33
const Duration _kTabBarScroll = const Duration(milliseconds: 200);
34

35
// Curves for the leading and trailing edge of the selected tab indicator.
36 37
const Curve _kTabIndicatorLeadingCurve = Curves.easeOut;
const Curve _kTabIndicatorTrailingCurve = Curves.easeIn;
38

39
// The scrollOffset (velocity) provided to fling() is pixels/ms, and the
40 41 42
// tolerance velocity is pixels/sec. The additional factor of 5 is to further
// increase sensitivity to swipe gestures and was determined "experimentally".
final double _kMinFlingVelocity = kPixelScrollTolerance.velocity / 5000.0;
43

Hixie's avatar
Hixie committed
44
class _TabBarParentData extends ContainerBoxParentDataMixin<RenderBox> { }
45

46 47 48
class _RenderTabBar extends RenderBox with
    ContainerRenderObjectMixin<RenderBox, _TabBarParentData>,
    RenderBoxContainerDefaultsMixin<RenderBox, _TabBarParentData> {
49

50
  _RenderTabBar(this.onLayoutChanged);
51 52 53

  int _selectedIndex;
  int get selectedIndex => _selectedIndex;
54
  set selectedIndex(int value) {
55 56 57 58 59 60 61 62
    if (_selectedIndex != value) {
      _selectedIndex = value;
      markNeedsPaint();
    }
  }

  Color _indicatorColor;
  Color get indicatorColor => _indicatorColor;
63
  set indicatorColor(Color value) {
64 65 66 67 68 69
    if (_indicatorColor != value) {
      _indicatorColor = value;
      markNeedsPaint();
    }
  }

70 71
  Rect _indicatorRect;
  Rect get indicatorRect => _indicatorRect;
72
  set indicatorRect(Rect value) {
73 74 75 76 77 78
    if (_indicatorRect != value) {
      _indicatorRect = value;
      markNeedsPaint();
    }
  }

79 80
  bool _textAndIcons;
  bool get textAndIcons => _textAndIcons;
81
  set textAndIcons(bool value) {
82 83 84 85 86 87
    if (_textAndIcons != value) {
      _textAndIcons = value;
      markNeedsLayout();
    }
  }

88 89
  bool _isScrollable;
  bool get isScrollable => _isScrollable;
90
  set isScrollable(bool value) {
91 92
    if (_isScrollable != value) {
      _isScrollable = value;
93 94 95 96
      markNeedsLayout();
    }
  }

97
  @override
98
  void setupParentData(RenderBox child) {
99 100
    if (child.parentData is! _TabBarParentData)
      child.parentData = new _TabBarParentData();
101 102
  }

103
  @override
104
  double computeMinIntrinsicWidth(double height) {
105 106 107
    double maxWidth = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
108
      maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(height));
Hixie's avatar
Hixie committed
109 110
      final _TabBarParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
111
    }
112
    return isScrollable ? maxWidth : maxWidth * childCount;
113 114
  }

115
  @override
116
  double computeMaxIntrinsicWidth(double height) {
117
    double maxWidth = 0.0;
118
    double totalWidth = 0.0;
119 120
    RenderBox child = firstChild;
    while (child != null) {
121 122 123
      double childWidth = child.getMaxIntrinsicWidth(height);
      maxWidth = math.max(maxWidth, childWidth);
      totalWidth += childWidth;
Hixie's avatar
Hixie committed
124 125
      final _TabBarParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
126
    }
127
    return isScrollable ? totalWidth : maxWidth * childCount;
128 129
  }

Hans Muller's avatar
Hans Muller committed
130 131
  double get _tabHeight => textAndIcons ? _kTextAndIconTabHeight : _kTabHeight;
  double get _tabBarHeight => _tabHeight + _kTabIndicatorHeight;
132

133
  @override
134
  double computeMinIntrinsicHeight(double width) => _tabBarHeight;
135

136
  @override
137
  double computeMaxIntrinsicHeight(double width) => _tabBarHeight;
138 139 140 141

  void layoutFixedWidthTabs() {
    double tabWidth = size.width / childCount;
    BoxConstraints tabConstraints =
Hans Muller's avatar
Hans Muller committed
142
      new BoxConstraints.tightFor(width: tabWidth, height: _tabHeight);
143 144 145 146
    double x = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      child.layout(tabConstraints);
Hixie's avatar
Hixie committed
147
      final _TabBarParentData childParentData = child.parentData;
148
      childParentData.offset = new Offset(x, 0.0);
149
      x += tabWidth;
Hixie's avatar
Hixie committed
150
      child = childParentData.nextSibling;
151 152 153
    }
  }

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

  Size layoutSize;
  List<double> layoutWidths;
175
  _TabLayoutChanged onLayoutChanged;
176 177 178 179

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

200
  @override
201 202 203 204 205
  void performLayout() {
    assert(constraints is BoxConstraints);
    if (childCount == 0)
      return;

Hans Muller's avatar
Hans Muller committed
206 207 208 209 210
    if (isScrollable) {
      double tabBarWidth = layoutScrollableTabs();
      size = constraints.constrain(new Size(tabBarWidth, _tabBarHeight));
    } else {
      size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight));
211
      layoutFixedWidthTabs();
Hans Muller's avatar
Hans Muller committed
212
    }
213 214 215 216 217

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

218
  @override
Adam Barth's avatar
Adam Barth committed
219 220
  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
221 222
  }

Adam Barth's avatar
Adam Barth committed
223
  void _paintIndicator(Canvas canvas, RenderBox selectedTab, Offset offset) {
224 225 226
    if (indicatorColor == null)
      return;

227
    if (indicatorRect != null) {
Hans Muller's avatar
Hans Muller committed
228
      canvas.drawRect(indicatorRect.shift(offset), new Paint()..color = indicatorColor);
229 230 231
      return;
    }

Hixie's avatar
Hixie committed
232 233 234
    final Size size = new Size(selectedTab.size.width, _kTabIndicatorHeight);
    final _TabBarParentData selectedTabParentData = selectedTab.parentData;
    final Point point = new Point(
235
      selectedTabParentData.offset.dx,
236 237
      _tabBarHeight - _kTabIndicatorHeight
    );
Hixie's avatar
Hixie committed
238
    canvas.drawRect((point + offset) & size, new Paint()..color = indicatorColor);
239 240
  }

241
  @override
242
  void paint(PaintingContext context, Offset offset) {
243 244 245
    int index = 0;
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
246
      final _TabBarParentData childParentData = child.parentData;
Adam Barth's avatar
Adam Barth committed
247
      context.paintChild(child, childParentData.offset + offset);
248
      if (index++ == selectedIndex)
249
        _paintIndicator(context.canvas, child, offset);
Hixie's avatar
Hixie committed
250
      child = childParentData.nextSibling;
251 252 253 254
    }
  }
}

255 256
class _TabBarWrapper extends MultiChildRenderObjectWidget {
  _TabBarWrapper({
257
    Key key,
258 259 260
    List<Widget> children,
    this.selectedIndex,
    this.indicatorColor,
261
    this.indicatorRect,
262
    this.textAndIcons,
263
    this.isScrollable: false,
264
    this.onLayoutChanged
265 266 267 268
  }) : super(key: key, children: children);

  final int selectedIndex;
  final Color indicatorColor;
269
  final Rect indicatorRect;
270
  final bool textAndIcons;
271
  final bool isScrollable;
272
  final _TabLayoutChanged onLayoutChanged;
273

274
  @override
275
  _RenderTabBar createRenderObject(BuildContext context) {
276
    _RenderTabBar result = new _RenderTabBar(onLayoutChanged);
277
    updateRenderObject(context, result);
278 279
    return result;
  }
280

281
  @override
282
  void updateRenderObject(BuildContext context, _RenderTabBar renderObject) {
283 284 285 286 287 288 289
    renderObject
      ..selectedIndex = selectedIndex
      ..indicatorColor = indicatorColor
      ..indicatorRect = indicatorRect
      ..textAndIcons = textAndIcons
      ..isScrollable = isScrollable
      ..onLayoutChanged = onLayoutChanged;
290 291 292
  }
}

293 294 295 296 297
/// Signature for building icons for tabs.
///
/// See also:
///
///  * [TabLabel]
298 299 300
typedef Widget TabLabelIconBuilder(BuildContext context, Color color);

/// Each TabBar tab can display either a title [text], an icon, or both. An icon
301 302 303 304
/// 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.
305
class TabLabel {
306 307 308
  /// Creates a tab label description.
  ///
  /// At least one of [text], [icon], or [iconBuilder] must be non-null.
309
  const TabLabel({ this.text, this.icon, this.iconBuilder });
310

311
  /// The text to display as the label of the tab.
312
  final String text;
313

Ian Hickson's avatar
Ian Hickson committed
314 315 316 317 318 319 320 321
  /// The icon to display as the label of the tab.
  ///
  /// The size and color of the icon is configured automatically using an
  /// [IconTheme] and therefore does not need to be explicitly given in the
  /// icon widget.
  ///
  /// See [Icon], [ImageIcon].
  final Widget icon;
322 323 324 325 326 327 328

  /// Called if [icon] is null to build an icon as a label for this tab.
  ///
  /// The color argument to this builder is the color that an ordinary icon
  /// would have been drawn with. The color reflects that tab's selection state.
  ///
  /// Return value must be non-null.
329
  final TabLabelIconBuilder iconBuilder;
Ian Hickson's avatar
Ian Hickson committed
330 331 332 333 334 335

  /// Whether this label has any text (specified using [text]).
  bool get hasText => text != null;

  /// Whether this label has an icon (specified either using [icon] or [iconBuilder]).
  bool get hasIcon => icon != null || iconBuilder != null;
336 337
}

338
class _Tab extends StatelessWidget {
339
  _Tab({
340
    Key key,
341
    this.onSelected,
342
    this.label,
343
    this.color
344
  }) : super(key: key) {
Ian Hickson's avatar
Ian Hickson committed
345
    assert(label.hasText || label.hasIcon);
346 347
  }

348
  final VoidCallback onSelected;
349
  final TabLabel label;
Hans Muller's avatar
Hans Muller committed
350
  final Color color;
351 352 353

  Widget _buildLabelText() {
    assert(label.text != null);
354
    TextStyle style = new TextStyle(color: color);
355 356 357 358 359 360
    return new Text(
      label.text,
      style: style,
      softWrap: false,
      overflow: TextOverflow.fade
    );
361 362
  }

363
  Widget _buildLabelIcon(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
364
    assert(label.hasIcon);
365
    if (label.icon != null) {
Ian Hickson's avatar
Ian Hickson committed
366 367 368 369 370 371 372 373
      return new IconTheme.merge(
        context: context,
        data: new IconThemeData(
          color: color,
          size: 24.0
        ),
        child: label.icon
      );
374 375 376 377 378 379 380
    } else {
      return new SizedBox(
        width: 24.0,
        height: 24.0,
        child: label.iconBuilder(context, color)
      );
    }
381 382
  }

383
  @override
384
  Widget build(BuildContext context) {
385
    assert(debugCheckHasMaterial(context));
386
    Widget labelContent;
Ian Hickson's avatar
Ian Hickson committed
387
    if (!label.hasIcon) {
388
      labelContent = _buildLabelText();
Ian Hickson's avatar
Ian Hickson committed
389
    } else if (!label.hasText) {
390
      labelContent = _buildLabelIcon(context);
391
    } else {
392
      labelContent = new Column(
393 394
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
395
        children: <Widget>[
396
          new Container(
397
            child: _buildLabelIcon(context),
398
            margin: const EdgeInsets.only(bottom: 10.0)
399 400
          ),
          _buildLabelText()
401
        ]
402 403 404 405
      );
    }

    Container centeredLabel = new Container(
406
      child: new Center(child: labelContent, widthFactor: 1.0, heightFactor: 1.0),
407 408 409 410
      constraints: new BoxConstraints(minWidth: _kMinTabWidth),
      padding: _kTabLabelPadding
    );

411 412 413 414
    return new InkWell(
      onTap: onSelected,
      child: centeredLabel
    );
415
  }
Hixie's avatar
Hixie committed
416

417
  @override
Hixie's avatar
Hixie committed
418 419 420 421
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$label');
  }
422 423
}

424
class _TabsScrollBehavior extends BoundedBehavior {
425
  _TabsScrollBehavior();
426

427
  @override
428 429
  bool isScrollable = true;

430
  @override
431
  Simulation createScrollSimulation(double position, double velocity) {
432 433 434 435 436 437 438 439 440
    if (!isScrollable)
      return null;

    double velocityPerSecond = velocity * 1000.0;
    return new BoundedFrictionSimulation(
      _kTabBarScrollDrag, position, velocityPerSecond, minScrollOffset, maxScrollOffset
    );
  }

441
  @override
442 443 444 445 446
  double applyCurve(double scrollOffset, double scrollDelta) {
    return (isScrollable) ? super.applyCurve(scrollOffset, scrollDelta) : 0.0;
  }
}

447
/// An abstract interface through which [TabBarSelection] reports changes.
448
abstract class TabBarSelectionAnimationListener {
449
  /// Called when the status of the [TabBarSelection] animation changes.
450
  void handleStatusChange(AnimationStatus status);
451 452

  /// Called on each animation frame when the [TabBarSelection] animation ticks.
453
  void handleProgressChange();
454 455 456 457 458

  /// Called when the [TabBarSelection] is deactivated.
  ///
  /// Implementations typically drop their reference to the [TabBarSelection]
  /// during this callback.
459
  void handleSelectionDeactivate();
Hans Muller's avatar
Hans Muller committed
460 461
}

462 463 464 465 466 467 468
/// Coordinates the tab selection between a [TabBar] and a [TabBarView].
///
/// Place a [TabBarSelection] widget in the tree such that it is a common
/// ancestor of both the [TabBar] and the [TabBarView]. Both the [TabBar] and
/// the [TabBarView] can alter which tab is selected. They coodinate by
/// listening to the selection value stored in a common ancestor
/// [TabBarSelection] selection widget.
469
class TabBarSelection<T> extends StatefulWidget {
470 471 472 473 474
  /// Creates a tab bar selection.
  ///
  /// The values argument must be non-null, non-empty, and each value must be
  /// unique. The value argument must either be null or contained in the values
  /// argument. The child argument must be non-null.
Hans Muller's avatar
Hans Muller committed
475 476
  TabBarSelection({
    Key key,
477
    this.value,
478
    @required this.values,
Hans Muller's avatar
Hans Muller committed
479
    this.onChanged,
480
    @required this.child
481
  }) : super(key: key)  {
482 483 484
    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);
485
    assert(child != null);
486
  }
487

488
  /// The current value of the selection.
489
  final T value;
490

491
  /// The list of possible values that the selection can obtain.
492
  List<T> values;
493

494 495 496 497 498 499 500
  /// Called when the value of the selection should change.
  ///
  /// The tab bar selection passes the new value to the callback but does not
  /// actually change state until the parent widget rebuilds the tab bar
  /// selection with the new value.
  ///
  /// If null, the tab bar selection cannot change value.
501
  final ValueChanged<T> onChanged;
502 503

  /// The widget below this widget in the tree.
Hans Muller's avatar
Hans Muller committed
504 505
  final Widget child;

506
  @override
Hixie's avatar
Hixie committed
507
  TabBarSelectionState<T> createState() => new TabBarSelectionState<T>();
Hans Muller's avatar
Hans Muller committed
508

509
  /// The state from the closest instance of this class that encloses the given context.
510 511
  static TabBarSelectionState<dynamic/*=T*/> of/*<T>*/(BuildContext context) {
    return context.ancestorStateOfType(new TypeMatcher<TabBarSelectionState<dynamic/*=T*/>>());
Hans Muller's avatar
Hans Muller committed
512
  }
513 514 515 516 517 518 519

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('current tab: $value');
    description.add('available tabs: $values');
  }
Hans Muller's avatar
Hans Muller committed
520 521
}

522 523 524 525
/// State for a [TabBarSelection] widget.
///
/// Subclasses of [TabBarSelection] typically use [State] objects that extend
/// this class.
526
class TabBarSelectionState<T> extends State<TabBarSelection<T>> {
527

528
  /// An animation that updates as the selected tab changes.
529
  Animation<double> get animation => _controller.view;
530

531
  // Both the TabBar and TabBarView classes access _controller because they
Hans Muller's avatar
Hans Muller committed
532
  // alternately drive selection progress between tabs.
533
  final AnimationController _controller = new AnimationController(duration: _kTabBarScroll, value: 1.0);
534 535 536 537 538 539 540 541
  final Map<T, int> _valueToIndex = new Map<T, int>();

  void _initValueToIndex() {
    _valueToIndex.clear();
    int index = 0;
    for(T value in values)
      _valueToIndex[value] = index++;
  }
542

543
  @override
544 545
  void initState() {
    super.initState();
546
    _value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first;
547 548 549 550 551 552

    // 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;

553 554 555 556
    _previousValue = _value;
    _initValueToIndex();
  }

557
  @override
558
  void didUpdateConfig(TabBarSelection<T> oldConfig) {
559 560 561
    super.didUpdateConfig(oldConfig);
    if (values != oldConfig.values)
      _initValueToIndex();
562 563
  }

Hans Muller's avatar
Hans Muller committed
564
  void _writeValue() {
565
    PageStorage.of(context)?.writeState(context, _value);
Hans Muller's avatar
Hans Muller committed
566 567
  }

568
  /// The list of possible values that the selection can obtain.
569 570
  List<T> get values => config.values;

571 572 573 574
  /// The previously selected value.
  ///
  /// When the tab selection changes, the tab selection animates from the
  /// previously selected value to the new value.
575 576 577
  T get previousValue => _previousValue;
  T _previousValue;

578 579 580 581
  /// Whether the tab selection is in the process of animating from one value to
  /// another.
  // TODO(abarth): Try computing this value from _controller.state so we don't
  // need to keep a separate bool in sync.
582
  bool get valueIsChanging => _valueIsChanging;
583
  bool _valueIsChanging = false;
Hans Muller's avatar
Hans Muller committed
584

585 586 587
  /// The index of a given value in [values].
  ///
  /// Runs in constant time.
588
  int indexOf(T tabValue) => _valueToIndex[tabValue];
589 590

  /// The index of the currently selected value.
591
  int get index => _valueToIndex[value];
592 593

  /// The index of the previoulsy selected value.
594 595
  int get previousIndex => indexOf(_previousValue);

596 597 598 599
  /// The currently selected value.
  ///
  /// Writing to this field will cause the tab selection to animate from the
  /// previous value to the new value.
600 601
  T get value => _value;
  T _value;
602
  set value(T newValue) {
603
    if (newValue == _value)
604
      return;
Hans Muller's avatar
Hans Muller committed
605
    _previousValue = _value;
606
    _value = newValue;
Hans Muller's avatar
Hans Muller committed
607
    _writeValue();
608
    _valueIsChanging = true;
609

610
    // If the selected value change was triggered by a drag gesture, the current
611
    // value of _controller.value will reflect where the gesture ended. While
612 613 614 615 616 617 618 619 620
    // the drag was underway the controller's value indicates where the indicator
    // and TabBarView scrollPositions 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 the controller's value just moves between
    // the selected tab and the adjacent one. So: convert the controller's value
    // here to reflect the fact that we're now moving between (just) the previous
    // and current selection index.
621

622
    double value;
623
    if (_controller.status == AnimationStatus.completed)
624
      value = 0.0;
625
    else if (_previousValue == values.first)
626
      value = _controller.value;
627
    else if (_previousValue == values.last)
628
      value = 1.0 - _controller.value;
629
    else if (previousIndex < index)
630
      value = (_controller.value - 0.5) * 2.0;
631
    else
632
      value = 1.0 - _controller.value * 2.0;
633

634
    _controller
635
      ..value = value
636
      ..forward().then((_) {
637 638
        // TODO(abarth): Consider using a status listener and checking for
        // AnimationStatus.completed.
639
        if (_controller.value == 1.0) {
640
          if (config.onChanged != null)
641 642
            config.onChanged(_value);
          _valueIsChanging = false;
643
        }
644 645 646
      });
  }

647
  final List<TabBarSelectionAnimationListener> _animationListeners = <TabBarSelectionAnimationListener>[];
648

649 650 651
  /// Calls listener methods every time the value or status of the selection animation changes.
  ///
  /// Listeners can be removed with [unregisterAnimationListener].
652 653
  void registerAnimationListener(TabBarSelectionAnimationListener listener) {
    _animationListeners.add(listener);
654
    _controller
655 656 657 658
      ..addStatusListener(listener.handleStatusChange)
      ..addListener(listener.handleProgressChange);
  }

659 660 661
  /// Stop calling listener methods every time the value or status of the animation changes.
  ///
  /// Listeners can be added with [registerAnimationListener].
662 663
  void unregisterAnimationListener(TabBarSelectionAnimationListener listener) {
    _animationListeners.remove(listener);
664
    _controller
665 666 667 668
      ..removeStatusListener(listener.handleStatusChange)
      ..removeListener(listener.handleProgressChange);
  }

669
  @override
670
  void deactivate() {
Hans Muller's avatar
Hans Muller committed
671
    _controller.stop();
672
    for (TabBarSelectionAnimationListener listener in _animationListeners.toList()) {
673
      listener.handleSelectionDeactivate();
674
      unregisterAnimationListener(listener);
675
    }
676
    assert(_animationListeners.isEmpty);
Hans Muller's avatar
Hans Muller committed
677
    _writeValue();
678
    super.deactivate();
679 680
  }

681
  @override
Hans Muller's avatar
Hans Muller committed
682
  Widget build(BuildContext context) {
683
    return config.child;
Hans Muller's avatar
Hans Muller committed
684
  }
685 686
}

687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
// Used when the user is dragging the TabBar or the TabBarView left or right.
// Dragging from the selected tab to the left varies t between 0.5 and 0.0.
// Dragging towards the tab on the right varies t between 0.5 and 1.0.
class _TabIndicatorTween extends Tween<Rect> {
  _TabIndicatorTween({ Rect begin, this.middle, Rect end }) : super(begin: begin, end: end);

  final Rect middle;

  @override
  Rect lerp(double t) {
    return t <= 0.5
      ? Rect.lerp(begin, middle, t * 2.0)
      : Rect.lerp(middle, end, (t - 0.5) * 2.0);
    }
}

703
/// A widget that displays a horizontal row of tabs, one per label.
704
///
705 706 707 708 709 710
/// Requires one of its ancestors to be a [TabBarSelection] widget to enable
/// saving and monitoring the selected tab.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
711
///
712 713 714 715
///  * [TabBarSelection]
///  * [TabBarView]
///  * [AppBar.tabBar]
///  * <https://www.google.com/design/spec/components/tabs.html>
716
class TabBar<T> extends Scrollable implements AppBarBottomWidget {
717 718 719
  /// Creates a widget that displays a horizontal row of tabs, one per label.
  ///
  /// The [labels] argument must not be null.
720
  TabBar({
721
    Key key,
722
    @required this.labels,
723 724 725
    this.isScrollable: false,
    this.indicatorColor,
    this.labelColor
726 727 728
  }) : super(key: key, scrollDirection: Axis.horizontal) {
    assert(labels != null);
  }
729

730
  /// The labels to display in the tabs.
731 732 733
  ///
  /// The [TabBarSelection.values] are used as keys for this map to determine
  /// which tab label is selected.
734
  final Map<T, TabLabel> labels;
735 736 737 738 739 740

  /// Whether this tab bar can be scrolled horizontally.
  ///
  /// 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.
741
  final bool isScrollable;
742

743 744 745 746 747 748 749 750 751
  /// The color of the line that appears below the selected tab. If this parameter
  /// is null then the value of the Theme's indicatorColor property is used.
  final Color indicatorColor;

  /// The color of selected tab labels. Unselected tab labels are rendered
  /// with the same color rendered at 70% opacity. If this parameter is null then
  /// the color of the theme's body2 text color is used.
  final Color labelColor;

752 753 754
  /// The height of the tab labels and indicator.
  @override
  double get bottomHeight {
755
    for (TabLabel label in labels.values) {
Ian Hickson's avatar
Ian Hickson committed
756
      if (label.hasText && label.hasIcon)
757 758 759 760 761
        return _kTextAndIconTabHeight + _kTabIndicatorHeight;
    }
    return _kTabHeight + _kTabIndicatorHeight;
  }

762
  @override
763
  _TabBarState<T> createState() => new _TabBarState<T>();
764
}
765

766
class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelectionAnimationListener {
767
  TabBarSelectionState<T> _selection;
768
  bool _valueIsChanging = false;
769
  int _lastSelectedIndex = -1;
Hans Muller's avatar
Hans Muller committed
770

771 772 773
  void _initSelection(TabBarSelectionState<T> newSelection) {
    if (_selection == newSelection)
      return;
774
    _selection?.unregisterAnimationListener(this);
775
    _selection = newSelection;
776
    _selection?.registerAnimationListener(this);
777 778
    if (_selection != null)
      _lastSelectedIndex = _selection.index;
779
  }
Hans Muller's avatar
Hans Muller committed
780

781
  @override
782 783
  void initState() {
    super.initState();
784
    scrollBehavior.isScrollable = config.isScrollable;
785 786
  }

787
  @override
788
  void didUpdateConfig(TabBar<T> oldConfig) {
789 790 791
    super.didUpdateConfig(oldConfig);
    if (!config.isScrollable)
      scrollTo(0.0);
792 793
  }

794
  @override
795
  void dispose() {
796
    _selection?.unregisterAnimationListener(this);
797 798 799
    super.dispose();
  }

800
  @override
801 802 803 804
  void handleSelectionDeactivate() {
    _selection = null;
  }

805 806 807 808 809 810
  // Initialize _indicatorTween for interactive dragging between the tab on the left
  // and the tab on the right. In this case _selection.animation.value is 0.5 when
  // the indicator is below the selected tab, 0.0 when it's under the left tab, and 1.0
  // when it's under the tab on the right.
  void _initIndicatorTweenForDrag() {
    assert(!_valueIsChanging);
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825
    int index = _selection.index;
    int beginIndex = math.max(0, index - 1);
    int endIndex = math.min(config.labels.length - 1, index + 1);
    if (beginIndex == index || endIndex == index) {
      _indicatorTween = new RectTween(
        begin: _tabIndicatorRect(beginIndex),
        end: _tabIndicatorRect(endIndex)
      );
    } else {
      _indicatorTween = new _TabIndicatorTween(
        begin: _tabIndicatorRect(beginIndex),
        middle: _tabIndicatorRect(index),
        end: _tabIndicatorRect(endIndex)
      );
    }
826 827 828 829 830 831 832 833 834 835 836 837 838 839
  }

  // Initialize _indicatorTween for animating the selected tab indicator from the
  // previously selected tab to the newly selected one. In this case
  // _selection.animation.value is 0.0 when the indicator is below the previously
  // selected tab, and 1.0 when it's under the newly selected one.
  void _initIndicatorTweenForAnimation() {
    assert(_valueIsChanging);
    _indicatorTween = new RectTween(
      begin: _indicatorRect ?? _tabIndicatorRect(_selection.previousIndex),
      end: _tabIndicatorRect(_selection.index)
    );
  }

840
  @override
841
  void handleStatusChange(AnimationStatus status) {
842
    if (config.labels.length == 0)
Hans Muller's avatar
Hans Muller committed
843 844
      return;

845
    if (_valueIsChanging && status == AnimationStatus.completed) {
846
      _valueIsChanging = false;
847
      setState(() {
848
        _initIndicatorTweenForDrag();
849
        _indicatorRect = _tabIndicatorRect(_selection.index);
850 851
      });
    }
852
  }
853

854
  @override
855
  void handleProgressChange() {
856
    if (config.labels.length == 0 || _selection == null)
Hans Muller's avatar
Hans Muller committed
857 858
      return;

859
    if (_lastSelectedIndex != _selection.index) {
860
      _valueIsChanging = true;
Hans Muller's avatar
Hans Muller committed
861
      if (config.isScrollable)
Hans Muller's avatar
Hans Muller committed
862
        scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
863
      _initIndicatorTweenForAnimation();
864
      _lastSelectedIndex = _selection.index;
865 866
    } else if (_indicatorTween == null) {
      _initIndicatorTweenForDrag();
867
    }
868

869
    Rect oldRect = _indicatorRect;
870
    double t = _selection.animation.value;
871 872 873 874 875 876 877 878 879

    // When _valueIsChanging is false, we're animating based on drag gesture and
    // want linear selected tab indicator motion. When _valueIsChanging is true,
    // a ticker is driving the selection change and we want to curve the animation.
    // In this case the leading and trailing edges of the move at different rates.
    // The easiest way to do this is to lerp 2 rects, and piece them together into 1.
    if (!_valueIsChanging) {
      _indicatorRect = _indicatorTween.lerp(t);
    } else {
880 881 882 883 884 885 886 887 888 889 890 891 892
      Rect leftRect, rightRect;
      if (_selection.index > _selection.previousIndex) {
        // Moving to the right - right edge is leading.
        rightRect = _indicatorTween.lerp(_kTabIndicatorLeadingCurve.transform(t));
        leftRect = _indicatorTween.lerp(_kTabIndicatorTrailingCurve.transform(t));
      } else {
        // Moving to the left - left edge is leading.
        leftRect = _indicatorTween.lerp(_kTabIndicatorLeadingCurve.transform(t));
        rightRect = _indicatorTween.lerp(_kTabIndicatorTrailingCurve.transform(t));
      }
      _indicatorRect = new Rect.fromLTRB(
        leftRect.left, leftRect.top, rightRect.right, rightRect.bottom
      );
893 894
    }
    if (oldRect != _indicatorRect)
895
      setState(() { /* The indicator rect has changed. */ });
896 897
  }

898 899 900
  Size _viewportSize = Size.zero;
  Size _tabBarSize;
  List<double> _tabWidths;
901
  Rect _indicatorRect;
902
  Tween<Rect> _indicatorTween;
903

904 905 906 907 908 909
  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
910
      tabLeft = _tabWidths.take(tabIndex).reduce((double sum, double width) => sum + width);
Hans Muller's avatar
Hans Muller committed
911 912 913
    final double tabTop = 0.0;
    final double tabBottom = _tabBarSize.height - _kTabIndicatorHeight;
    final double tabRight = tabLeft + _tabWidths[tabIndex];
914 915 916 917 918 919 920 921
    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);
  }

922
  @override
923
  ScrollBehavior<double, double> createScrollBehavior() => new _TabsScrollBehavior();
924 925

  @override
926 927
  _TabsScrollBehavior get scrollBehavior => super.scrollBehavior;

928
  double _centeredTabScrollOffset(int tabIndex) {
929
    double viewportWidth = scrollBehavior.containerExtent;
930 931
    Rect tabRect = _tabRect(tabIndex);
    return (tabRect.left + tabRect.width / 2.0 - viewportWidth / 2.0)
932 933 934
      .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
  }

935
  void _handleTabSelected(int tabIndex) {
Hans Muller's avatar
Hans Muller committed
936
    if (_selection != null && tabIndex != _selection.index)
937
      setState(() {
938
        _selection.value = _selection.values[tabIndex];
939
      });
940 941
  }

Hans Muller's avatar
Hans Muller committed
942
  Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) {
Hans Muller's avatar
Hans Muller committed
943 944 945 946 947
    Color labelColor = color;
    if (_selection != null) {
      final bool isSelectedTab = tabIndex == _selection.index;
      final bool isPreviouslySelectedTab = tabIndex == _selection.previousIndex;
      labelColor = isSelectedTab ? selectedColor : color;
948
      if (_selection.valueIsChanging) {
Hans Muller's avatar
Hans Muller committed
949
        if (isSelectedTab)
950
          labelColor = Color.lerp(color, selectedColor, _selection.animation.value);
Hans Muller's avatar
Hans Muller committed
951
        else if (isPreviouslySelectedTab)
952
          labelColor = Color.lerp(selectedColor, color, _selection.animation.value);
Hans Muller's avatar
Hans Muller committed
953
      }
Hans Muller's avatar
Hans Muller committed
954
    }
955 956
    return new _Tab(
      onSelected: () { _handleTabSelected(tabIndex); },
957
      label: label,
958
      color: labelColor
959 960 961
    );
  }

Hans Muller's avatar
Hans Muller committed
962
  void _updateScrollBehavior() {
963
    didUpdateScrollBehavior(scrollBehavior.updateExtents(
964
      containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width,
Hans Muller's avatar
Hans Muller committed
965 966
      contentExtent: _tabWidths.reduce((double sum, double width) => sum + width),
      scrollOffset: scrollOffset
967
    ));
Hans Muller's avatar
Hans Muller committed
968 969
  }

970 971 972 973
  void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
    setState(() {
      _tabBarSize = tabBarSize;
      _tabWidths = tabWidths;
974
      _indicatorRect = _selection != null ? _tabIndicatorRect(_selection.index) : Rect.zero;
Hans Muller's avatar
Hans Muller committed
975
      _updateScrollBehavior();
976 977 978
    });
  }

979 980 981 982 983 984
  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;
Hans Muller's avatar
Hans Muller committed
985
    _updateScrollBehavior();
986
    if (config.isScrollable && _selection != null)
Hans Muller's avatar
Hans Muller committed
987
      scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
988
    return scrollOffsetToPixelDelta(scrollOffset);
Hans Muller's avatar
Hans Muller committed
989 990
  }

991
  @override
992
  Widget buildContent(BuildContext context) {
993
    TabBarSelectionState<T> newSelection = TabBarSelection.of(context);
994
    _initSelection(newSelection);
Hans Muller's avatar
Hans Muller committed
995

996
    assert(config.labels.isNotEmpty);
997
    assert(Material.of(context) != null);
998

999
    ThemeData themeData = Theme.of(context);
1000
    Color backgroundColor = Material.of(context).color;
1001
    Color indicatorColor = config.indicatorColor ?? themeData.indicatorColor;
1002 1003
    if (indicatorColor == backgroundColor) {
      // ThemeData tries to avoid this by having indicatorColor avoid being the
1004
      // primaryColor. However, it's possible that the tab bar is on a
1005 1006 1007 1008 1009
      // 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.
1010
      indicatorColor = Colors.white;
1011
    }
1012

1013
    final TextStyle textStyle = themeData.primaryTextTheme.body2;
1014 1015
    final Color selectedLabelColor = config.labelColor ?? themeData.primaryTextTheme.body2.color;
    final Color labelColor = selectedLabelColor.withAlpha(0xB2); // 70% alpha
1016

Hans Muller's avatar
Hans Muller committed
1017 1018 1019
    List<Widget> tabs = <Widget>[];
    bool textAndIcons = false;
    int tabIndex = 0;
1020
    for (TabLabel label in config.labels.values) {
1021
      tabs.add(_toTab(label, tabIndex++, labelColor, selectedLabelColor));
Ian Hickson's avatar
Ian Hickson committed
1022
      if (label.hasText && label.hasIcon)
Hans Muller's avatar
Hans Muller committed
1023 1024 1025
        textAndIcons = true;
    }

Ian Hickson's avatar
Ian Hickson committed
1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
    Widget contents = new DefaultTextStyle(
      style: textStyle,
      child: new _TabBarWrapper(
        children: tabs,
        selectedIndex: _selection?.index,
        indicatorColor: indicatorColor,
        indicatorRect: _indicatorRect,
        textAndIcons: textAndIcons,
        isScrollable: config.isScrollable,
        onLayoutChanged: _layoutChanged
1036 1037
      )
    );
Hans Muller's avatar
Hans Muller committed
1038

1039
    if (config.isScrollable) {
Hans Muller's avatar
Hans Muller committed
1040
      return new Viewport(
1041
        mainAxis: Axis.horizontal,
1042 1043 1044
        paintOffset: scrollOffsetToPixelDelta(scrollOffset),
        onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
        child: contents
1045 1046 1047
      );
    }

1048
    return contents;
1049 1050 1051
  }
}

1052 1053 1054 1055 1056 1057 1058 1059 1060 1061
/// A widget that displays the contents of a tab.
///
/// Requires one of its ancestors to be a [TabBarSelection] widget to enable
/// saving and monitoring the selected tab.
///
/// See also:
///
///  * [TabBarSelection]
///  * [TabBar]
///  * <https://www.google.com/design/spec/components/tabs.html>
1062
class TabBarView<T> extends PageableList {
1063 1064 1065
  /// Creates a widget that displays the contents of a tab.
  ///
  /// The [children] argument must not be null and must not be empty.
1066 1067
  TabBarView({
    Key key,
1068
    @required List<Widget> children
Adam Barth's avatar
Adam Barth committed
1069
  }) : super(
1070
    key: key,
1071
    scrollDirection: Axis.horizontal,
Adam Barth's avatar
Adam Barth committed
1072
    children: children
1073
  ) {
Adam Barth's avatar
Adam Barth committed
1074 1075
    assert(children != null);
    assert(children.length > 1);
1076 1077
  }

1078
  @override
1079
  _TabBarViewState<T> createState() => new _TabBarViewState<T>();
1080 1081
}

1082
class _TabBarViewState<T> extends PageableListState<TabBarView<T>> implements TabBarSelectionAnimationListener {
Hans Muller's avatar
Hans Muller committed
1083

1084
  TabBarSelectionState<T> _selection;
1085
  List<Widget> _items;
1086

Adam Barth's avatar
Adam Barth committed
1087
  int get _tabCount => config.children.length;
Hans Muller's avatar
Hans Muller committed
1088 1089 1090

  BoundedBehavior _boundedBehavior;

1091
  @override
Hans Muller's avatar
Hans Muller committed
1092
  ExtentScrollBehavior get scrollBehavior {
1093
    _boundedBehavior ??= new BoundedBehavior(platform: platform);
Hans Muller's avatar
Hans Muller committed
1094 1095 1096
    return _boundedBehavior;
  }

1097 1098 1099
  @override
  TargetPlatform get platform => Theme.of(context).platform;

1100 1101 1102 1103 1104 1105 1106
  void _initSelection(TabBarSelectionState<T> newSelection) {
    if (_selection == newSelection)
      return;
    _selection?.unregisterAnimationListener(this);
    _selection = newSelection;
    _selection?.registerAnimationListener(this);
    if (_selection != null)
1107
      _updateItemsAndScrollBehavior();
1108 1109
  }

1110
  @override
1111
  void didUpdateConfig(TabBarView<T> oldConfig) {
1112 1113 1114 1115 1116
    super.didUpdateConfig(oldConfig);
    if (_selection != null && config.children != oldConfig.children)
      _updateItemsForSelectedIndex(_selection.index);
  }

1117
  @override
1118
  void dispose() {
1119
    _selection?.unregisterAnimationListener(this);
1120 1121
    super.dispose();
  }
1122

1123
  @override
1124 1125 1126 1127
  void handleSelectionDeactivate() {
    _selection = null;
  }

1128
  void _updateItemsFromChildren(int first, int second, [int third]) {
1129
    List<Widget> widgets = config.children;
Adam Barth's avatar
Adam Barth committed
1130 1131 1132 1133
    _items = <Widget>[
      new KeyedSubtree.wrap(widgets[first], first),
      new KeyedSubtree.wrap(widgets[second], second),
    ];
1134
    if (third != null)
Adam Barth's avatar
Adam Barth committed
1135
      _items.add(new KeyedSubtree.wrap(widgets[third], third));
1136 1137
  }

1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148
  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) {
Hans Muller's avatar
Hans Muller committed
1149
    if (selectedIndex == 0) {
1150
      didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 0.0));
Hans Muller's avatar
Hans Muller committed
1151
    } else if (selectedIndex == _tabCount - 1) {
1152
      didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 1.0));
Hans Muller's avatar
Hans Muller committed
1153
    } else {
1154
      didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 3.0, containerExtent: 1.0, scrollOffset: 1.0));
Hans Muller's avatar
Hans Muller committed
1155 1156 1157
    }
  }

1158 1159 1160
  void _updateItemsAndScrollBehavior() {
    assert(_selection != null);
    final int selectedIndex = _selection.index;
1161
    assert(selectedIndex != null);
1162 1163 1164 1165
    _updateItemsForSelectedIndex(selectedIndex);
    _updateScrollBehaviorForSelectedIndex(selectedIndex);
  }

1166
  @override
1167
  void handleStatusChange(AnimationStatus status) {
1168 1169
  }

1170
  @override
1171
  void handleProgressChange() {
1172
    if (_selection == null || !_selection.valueIsChanging)
Hans Muller's avatar
Hans Muller committed
1173
      return;
1174
    // The TabBar is driving the TabBarSelection animation.
Hans Muller's avatar
Hans Muller committed
1175

1176
    final Animation<double> animation = _selection.animation;
Hans Muller's avatar
Hans Muller committed
1177

1178
    if (animation.status == AnimationStatus.completed) {
1179
      _updateItemsAndScrollBehavior();
1180
      return;
1181 1182
    }

1183
    if (animation.status != AnimationStatus.forward)
Hans Muller's avatar
Hans Muller committed
1184
      return;
1185

Hans Muller's avatar
Hans Muller committed
1186 1187
    final int selectedIndex = _selection.index;
    final int previousSelectedIndex = _selection.previousIndex;
1188 1189

    if (selectedIndex < previousSelectedIndex) {
1190
      _updateItemsFromChildren(selectedIndex, previousSelectedIndex);
1191
      scrollTo(new CurveTween(curve: Curves.ease.flipped).evaluate(new ReverseAnimation(animation)));
1192
    } else {
1193
      _updateItemsFromChildren(previousSelectedIndex, selectedIndex);
1194
      scrollTo(new CurveTween(curve: Curves.ease).evaluate(animation));
Adam Barth's avatar
Adam Barth committed
1195
    }
1196 1197
  }

1198
  @override
Hans Muller's avatar
Hans Muller committed
1199
  void dispatchOnScroll() {
1200
    if (_selection == null || _selection.valueIsChanging)
Hans Muller's avatar
Hans Muller committed
1201
      return;
1202
    // This class is driving the TabBarSelection's animation.
Hans Muller's avatar
Hans Muller committed
1203

1204
    final AnimationController controller = _selection._controller;
Hans Muller's avatar
Hans Muller committed
1205 1206

    if (_selection.index == 0 || _selection.index == _tabCount - 1)
1207
      controller.value = scrollOffset;
Hans Muller's avatar
Hans Muller committed
1208
    else
1209
      controller.value = scrollOffset / 2.0;
Hans Muller's avatar
Hans Muller committed
1210 1211
  }

1212
  @override
1213
  Future<Null> fling(double scrollVelocity) {
1214
    if (_selection == null || _selection.valueIsChanging)
1215
      return new Future<Null>.value();
Hans Muller's avatar
Hans Muller committed
1216

1217 1218
    if (scrollVelocity.abs() > _kMinFlingVelocity) {
      final int selectionDelta = scrollVelocity.sign.truncate();
1219
      final int targetIndex = (_selection.index + selectionDelta).clamp(0, _tabCount - 1);
1220 1221 1222 1223
      if (_selection.index != targetIndex) {
        _selection.value = _selection.values[targetIndex];
        return new Future<Null>.value();
      }
Hans Muller's avatar
Hans Muller committed
1224 1225
    }

Hans Muller's avatar
Hans Muller committed
1226
    final int selectionIndex = _selection.index;
Hans Muller's avatar
Hans Muller committed
1227 1228
    final int settleIndex = snapScrollOffset(scrollOffset).toInt();
    if (selectionIndex > 0 && settleIndex != 1) {
1229 1230
      final int targetIndex = (selectionIndex + (settleIndex == 2 ? 1 : -1)).clamp(0, _tabCount - 1);
      _selection.value = _selection.values[targetIndex];
1231
      return new Future<Null>.value();
Hans Muller's avatar
Hans Muller committed
1232
    } else if (selectionIndex == 0 && settleIndex == 1) {
1233
      _selection.value = _selection.values[1];
1234
      return new Future<Null>.value();
Hans Muller's avatar
Hans Muller committed
1235 1236 1237 1238
    }
    return settleScrollOffset();
  }

1239
  @override
1240
  Widget buildContent(BuildContext context) {
1241
    TabBarSelectionState<T> newSelection = TabBarSelection.of(context);
1242
    _initSelection(newSelection);
1243 1244
    return new PageViewport(
      itemsWrap: config.itemsWrap,
1245
      mainAxis: config.scrollDirection,
1246 1247 1248
      startOffset: scrollOffset,
      children: _items
    );
Hans Muller's avatar
Hans Muller committed
1249
  }
1250
}
Hixie's avatar
Hixie committed
1251

1252 1253 1254 1255 1256 1257 1258 1259 1260
/// A widget that displays a visual indicator of which tab is selected.
///
/// Requires one of its ancestors to be a [TabBarSelection] widget to enable
/// saving and monitoring the selected tab.
///
/// See also:
///
///  * [TabBarSelection]
///  * [TabBarView]
1261
class TabPageSelector<T> extends StatelessWidget {
1262 1263 1264 1265
  /// Creates a widget that displays a visual indicator of which tab is selected.
  ///
  /// Requires one of its ancestors to be a [TabBarSelection] widget to enable
  /// saving and monitoring the selected tab.
Hixie's avatar
Hixie committed
1266 1267
  const TabPageSelector({ Key key }) : super(key: key);

1268
  Widget _buildTabIndicator(TabBarSelectionState<T> selection, T tab, Animation<double> animation, ColorTween selectedColor, ColorTween previousColor) {
Hixie's avatar
Hixie committed
1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283
    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,
1284
      margin: new EdgeInsets.all(4.0),
Hixie's avatar
Hixie committed
1285 1286 1287 1288 1289 1290 1291 1292
      decoration: new BoxDecoration(
        backgroundColor: background,
        border: new Border.all(color: selectedColor.end),
        shape: BoxShape.circle
      )
    );
  }

1293
  @override
Hixie's avatar
Hixie committed
1294
  Widget build(BuildContext context) {
1295
    final TabBarSelectionState<T> selection = TabBarSelection.of(context);
1296
    final Color color = Theme.of(context).accentColor;
Hixie's avatar
Hixie committed
1297 1298 1299 1300 1301 1302 1303 1304 1305 1306
    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(),
1307
            mainAxisSize: MainAxisSize.min
Hixie's avatar
Hixie committed
1308 1309 1310 1311 1312 1313
          )
        );
      }
    );
  }
}