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

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

24
typedef void _TabLayoutChanged(Size size, List<double> widths);
25 26 27 28 29 30 31

// 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;
32
const EdgeInsets _kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0);
33
const double _kTabBarScrollDrag = 0.025;
34
const Duration _kTabBarScroll = const Duration(milliseconds: 200);
35

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

40
// The scrollOffset (velocity) provided to fling() is pixels/ms, and the
41 42 43
// 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;
44

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

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

51
  _RenderTabBar(this.onLayoutChanged);
52 53 54

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ian Hickson's avatar
Ian Hickson committed
315 316 317 318 319 320 321 322
  /// 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;
323 324 325 326 327 328 329

  /// 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.
330
  final TabLabelIconBuilder iconBuilder;
Ian Hickson's avatar
Ian Hickson committed
331 332 333 334 335 336

  /// 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;
337 338
}

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

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

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

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

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

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

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

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

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

428
  @override
429 430
  bool isScrollable = true;

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

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

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

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

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

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

463 464 465 466 467 468 469
/// 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.
470
class TabBarSelection<T> extends StatefulWidget {
471 472 473 474 475
  /// 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
476 477
  TabBarSelection({
    Key key,
478
    this.value,
479
    @required this.values,
Hans Muller's avatar
Hans Muller committed
480
    this.onChanged,
481
    @required this.child
482
  }) : super(key: key)  {
483 484 485
    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);
486
    assert(child != null);
487
  }
488

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

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

495 496 497 498 499 500 501
  /// 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.
502
  final ValueChanged<T> onChanged;
503 504

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

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

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

  @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
521 522
}

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

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

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

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

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

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

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

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

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

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

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

579 580 581 582
  /// 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.
583
  bool get valueIsChanging => _valueIsChanging;
584
  bool _valueIsChanging = false;
Hans Muller's avatar
Hans Muller committed
585

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

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

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

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

611
    // If the selected value change was triggered by a drag gesture, the current
612
    // value of _controller.value will reflect where the gesture ended. While
613 614 615 616 617 618 619 620 621
    // 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.
622

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

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

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

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

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

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

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

688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
// 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);
    }
}

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

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

  /// 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.
742
  final bool isScrollable;
743

744 745 746 747 748 749 750 751 752
  /// 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;

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

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

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

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

782
  @override
783
  void didUpdateConfig(TabBar<T> oldConfig) {
784
    super.didUpdateConfig(oldConfig);
785 786 787 788 789
    if (config.isScrollable != oldConfig.isScrollable) {
      scrollBehavior.isScrollable = config.isScrollable;
      if (!config.isScrollable)
        scrollTo(0.0);
    }
790 791
  }

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

798
  @override
799 800 801 802
  void handleSelectionDeactivate() {
    _selection = null;
  }

803 804 805 806 807 808
  // 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);
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823
    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)
      );
    }
824 825 826 827 828 829 830 831 832 833 834 835 836 837
  }

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

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

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

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

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

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

    // 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 {
878 879 880 881 882 883 884 885 886 887 888 889 890
      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
      );
891 892
    }
    if (oldRect != _indicatorRect)
893
      setState(() { /* The indicator rect has changed. */ });
894 895
  }

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

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

920
  @override
921 922 923 924
  ScrollBehavior<double, double> createScrollBehavior() {
    return new _TabsScrollBehavior()
      ..isScrollable = config.isScrollable;
  }
925 926

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

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

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

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

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

971
  void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
972 973 974 975 976 977 978 979
    // This is bad. We should use a LayoutBuilder or CustomMultiChildLayout or some such.
    // As designed today, tabs are always lagging one frame behind, taking two frames
    // to handle a layout change.
    _tabBarSize = tabBarSize;
    _tabWidths = tabWidths;
    _indicatorRect = _selection != null ? _tabIndicatorRect(_selection.index) : Rect.zero;
    _updateScrollBehavior();
    SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
980 981 982 983 984 985
      if (mounted) {
        setState(() {
          // the changes were made at layout time
          // TODO(ianh): remove this setState: https://github.com/flutter/flutter/issues/5749
        });
      }
986 987 988
    });
  }

989 990 991 992 993 994
  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
995
    _updateScrollBehavior();
996
    if (config.isScrollable && _selection != null)
Hans Muller's avatar
Hans Muller committed
997
      scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
998
    return scrollOffsetToPixelDelta(scrollOffset);
Hans Muller's avatar
Hans Muller committed
999 1000
  }

1001
  @override
1002
  Widget buildContent(BuildContext context) {
1003
    TabBarSelectionState<T> newSelection = TabBarSelection.of(context);
1004
    _initSelection(newSelection);
Hans Muller's avatar
Hans Muller committed
1005

1006
    assert(config.labels.isNotEmpty);
1007
    assert(Material.of(context) != null);
1008

1009
    ThemeData themeData = Theme.of(context);
1010
    Color backgroundColor = Material.of(context).color;
1011
    Color indicatorColor = config.indicatorColor ?? themeData.indicatorColor;
1012 1013
    if (indicatorColor == backgroundColor) {
      // ThemeData tries to avoid this by having indicatorColor avoid being the
1014
      // primaryColor. However, it's possible that the tab bar is on a
1015 1016 1017 1018 1019
      // 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.
1020
      indicatorColor = Colors.white;
1021
    }
1022

1023
    final TextStyle textStyle = themeData.primaryTextTheme.body2;
1024 1025
    final Color selectedLabelColor = config.labelColor ?? themeData.primaryTextTheme.body2.color;
    final Color labelColor = selectedLabelColor.withAlpha(0xB2); // 70% alpha
1026

Hans Muller's avatar
Hans Muller committed
1027 1028 1029
    List<Widget> tabs = <Widget>[];
    bool textAndIcons = false;
    int tabIndex = 0;
1030
    for (TabLabel label in config.labels.values) {
1031
      tabs.add(_toTab(label, tabIndex++, labelColor, selectedLabelColor));
Ian Hickson's avatar
Ian Hickson committed
1032
      if (label.hasText && label.hasIcon)
Hans Muller's avatar
Hans Muller committed
1033 1034 1035
        textAndIcons = true;
    }

Ian Hickson's avatar
Ian Hickson committed
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045
    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
1046 1047
      )
    );
Hans Muller's avatar
Hans Muller committed
1048

1049
    if (config.isScrollable) {
Hans Muller's avatar
Hans Muller committed
1050
      return new Viewport(
1051
        mainAxis: Axis.horizontal,
1052 1053 1054
        paintOffset: scrollOffsetToPixelDelta(scrollOffset),
        onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
        child: contents
1055 1056 1057
      );
    }

1058
    return contents;
1059 1060 1061
  }
}

1062 1063 1064 1065 1066 1067 1068 1069 1070 1071
/// 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>
1072
class TabBarView<T> extends PageableList {
1073 1074 1075
  /// Creates a widget that displays the contents of a tab.
  ///
  /// The [children] argument must not be null and must not be empty.
1076 1077
  TabBarView({
    Key key,
1078
    @required List<Widget> children
Adam Barth's avatar
Adam Barth committed
1079
  }) : super(
1080
    key: key,
1081
    scrollDirection: Axis.horizontal,
Adam Barth's avatar
Adam Barth committed
1082
    children: children
1083
  ) {
Adam Barth's avatar
Adam Barth committed
1084 1085
    assert(children != null);
    assert(children.length > 1);
1086 1087
  }

1088
  @override
1089
  _TabBarViewState<T> createState() => new _TabBarViewState<T>();
1090 1091
}

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

1094
  TabBarSelectionState<T> _selection;
1095
  List<Widget> _items;
1096

Adam Barth's avatar
Adam Barth committed
1097
  int get _tabCount => config.children.length;
Hans Muller's avatar
Hans Muller committed
1098 1099 1100

  BoundedBehavior _boundedBehavior;

1101
  @override
Hans Muller's avatar
Hans Muller committed
1102
  ExtentScrollBehavior get scrollBehavior {
1103
    _boundedBehavior ??= new BoundedBehavior(platform: platform);
Hans Muller's avatar
Hans Muller committed
1104 1105 1106
    return _boundedBehavior;
  }

1107 1108 1109
  @override
  TargetPlatform get platform => Theme.of(context).platform;

1110 1111 1112 1113 1114 1115 1116
  void _initSelection(TabBarSelectionState<T> newSelection) {
    if (_selection == newSelection)
      return;
    _selection?.unregisterAnimationListener(this);
    _selection = newSelection;
    _selection?.registerAnimationListener(this);
    if (_selection != null)
1117
      _updateItemsAndScrollBehavior();
1118 1119
  }

1120
  @override
1121
  void didUpdateConfig(TabBarView<T> oldConfig) {
1122 1123 1124 1125 1126
    super.didUpdateConfig(oldConfig);
    if (_selection != null && config.children != oldConfig.children)
      _updateItemsForSelectedIndex(_selection.index);
  }

1127
  @override
1128
  void dispose() {
1129
    _selection?.unregisterAnimationListener(this);
1130 1131
    super.dispose();
  }
1132

1133
  @override
1134 1135 1136 1137
  void handleSelectionDeactivate() {
    _selection = null;
  }

1138
  void _updateItemsFromChildren(int first, int second, [int third]) {
1139
    List<Widget> widgets = config.children;
Adam Barth's avatar
Adam Barth committed
1140 1141 1142 1143
    _items = <Widget>[
      new KeyedSubtree.wrap(widgets[first], first),
      new KeyedSubtree.wrap(widgets[second], second),
    ];
1144
    if (third != null)
Adam Barth's avatar
Adam Barth committed
1145
      _items.add(new KeyedSubtree.wrap(widgets[third], third));
1146 1147
  }

1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
  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
1159
    if (selectedIndex == 0) {
1160
      didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 0.0));
Hans Muller's avatar
Hans Muller committed
1161
    } else if (selectedIndex == _tabCount - 1) {
1162
      didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 1.0));
Hans Muller's avatar
Hans Muller committed
1163
    } else {
1164
      didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 3.0, containerExtent: 1.0, scrollOffset: 1.0));
Hans Muller's avatar
Hans Muller committed
1165 1166 1167
    }
  }

1168 1169 1170
  void _updateItemsAndScrollBehavior() {
    assert(_selection != null);
    final int selectedIndex = _selection.index;
1171
    assert(selectedIndex != null);
1172 1173 1174 1175
    _updateItemsForSelectedIndex(selectedIndex);
    _updateScrollBehaviorForSelectedIndex(selectedIndex);
  }

1176
  @override
1177
  void handleStatusChange(AnimationStatus status) {
1178 1179
  }

1180
  @override
1181
  void handleProgressChange() {
1182
    if (_selection == null || !_selection.valueIsChanging)
Hans Muller's avatar
Hans Muller committed
1183
      return;
1184
    // The TabBar is driving the TabBarSelection animation.
Hans Muller's avatar
Hans Muller committed
1185

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

1188
    if (animation.status == AnimationStatus.completed) {
1189
      _updateItemsAndScrollBehavior();
1190
      return;
1191 1192
    }

1193
    if (animation.status != AnimationStatus.forward)
Hans Muller's avatar
Hans Muller committed
1194
      return;
1195

Hans Muller's avatar
Hans Muller committed
1196 1197
    final int selectedIndex = _selection.index;
    final int previousSelectedIndex = _selection.previousIndex;
1198 1199

    if (selectedIndex < previousSelectedIndex) {
1200
      _updateItemsFromChildren(selectedIndex, previousSelectedIndex);
1201
      scrollTo(new CurveTween(curve: Curves.fastOutSlowIn.flipped).evaluate(new ReverseAnimation(animation)));
1202
    } else {
1203
      _updateItemsFromChildren(previousSelectedIndex, selectedIndex);
1204
      scrollTo(new CurveTween(curve: Curves.fastOutSlowIn).evaluate(animation));
Adam Barth's avatar
Adam Barth committed
1205
    }
1206 1207
  }

1208
  @override
Hans Muller's avatar
Hans Muller committed
1209
  void dispatchOnScroll() {
1210
    if (_selection == null || _selection.valueIsChanging)
Hans Muller's avatar
Hans Muller committed
1211
      return;
1212
    // This class is driving the TabBarSelection's animation.
Hans Muller's avatar
Hans Muller committed
1213

1214
    final AnimationController controller = _selection._controller;
Hans Muller's avatar
Hans Muller committed
1215 1216

    if (_selection.index == 0 || _selection.index == _tabCount - 1)
1217
      controller.value = scrollOffset;
Hans Muller's avatar
Hans Muller committed
1218
    else
1219
      controller.value = scrollOffset / 2.0;
Hans Muller's avatar
Hans Muller committed
1220 1221
  }

1222
  @override
1223
  Future<Null> fling(double scrollVelocity) {
1224
    if (_selection == null || _selection.valueIsChanging)
1225
      return new Future<Null>.value();
Hans Muller's avatar
Hans Muller committed
1226

1227 1228
    if (scrollVelocity.abs() > _kMinFlingVelocity) {
      final int selectionDelta = scrollVelocity.sign.truncate();
1229
      final int targetIndex = (_selection.index + selectionDelta).clamp(0, _tabCount - 1);
1230 1231 1232 1233
      if (_selection.index != targetIndex) {
        _selection.value = _selection.values[targetIndex];
        return new Future<Null>.value();
      }
Hans Muller's avatar
Hans Muller committed
1234 1235
    }

Hans Muller's avatar
Hans Muller committed
1236
    final int selectionIndex = _selection.index;
Hans Muller's avatar
Hans Muller committed
1237 1238
    final int settleIndex = snapScrollOffset(scrollOffset).toInt();
    if (selectionIndex > 0 && settleIndex != 1) {
1239 1240
      final int targetIndex = (selectionIndex + (settleIndex == 2 ? 1 : -1)).clamp(0, _tabCount - 1);
      _selection.value = _selection.values[targetIndex];
1241
      return new Future<Null>.value();
Hans Muller's avatar
Hans Muller committed
1242
    } else if (selectionIndex == 0 && settleIndex == 1) {
1243
      _selection.value = _selection.values[1];
1244
      return new Future<Null>.value();
Hans Muller's avatar
Hans Muller committed
1245 1246 1247 1248
    }
    return settleScrollOffset();
  }

1249
  @override
1250
  Widget buildContent(BuildContext context) {
1251
    TabBarSelectionState<T> newSelection = TabBarSelection.of(context);
1252
    _initSelection(newSelection);
1253 1254
    return new PageViewport(
      itemsWrap: config.itemsWrap,
1255
      mainAxis: config.scrollDirection,
1256 1257 1258
      startOffset: scrollOffset,
      children: _items
    );
Hans Muller's avatar
Hans Muller committed
1259
  }
1260
}
Hixie's avatar
Hixie committed
1261

1262 1263 1264 1265 1266 1267 1268 1269 1270
/// 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]
1271
class TabPageSelector<T> extends StatelessWidget {
1272 1273 1274 1275
  /// 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
1276 1277
  const TabPageSelector({ Key key }) : super(key: key);

1278
  Widget _buildTabIndicator(TabBarSelectionState<T> selection, T tab, Animation<double> animation, ColorTween selectedColor, ColorTween previousColor) {
Hixie's avatar
Hixie committed
1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293
    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,
1294
      margin: new EdgeInsets.all(4.0),
Hixie's avatar
Hixie committed
1295 1296 1297 1298 1299 1300 1301 1302
      decoration: new BoxDecoration(
        backgroundColor: background,
        border: new Border.all(color: selectedColor.end),
        shape: BoxShape.circle
      )
    );
  }

1303
  @override
Hixie's avatar
Hixie committed
1304
  Widget build(BuildContext context) {
1305
    final TabBarSelectionState<T> selection = TabBarSelection.of(context);
1306
    final Color color = Theme.of(context).accentColor;
Hixie's avatar
Hixie committed
1307 1308
    final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color);
    final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent);
1309
    Animation<double> animation = new CurvedAnimation(parent: selection.animation, curve: Curves.fastOutSlowIn);
Hixie's avatar
Hixie committed
1310 1311 1312 1313 1314 1315 1316
    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(),
1317
            mainAxisSize: MainAxisSize.min
Hixie's avatar
Hixie committed
1318 1319 1320 1321 1322 1323
          )
        );
      }
    );
  }
}