tabs.dart 38.8 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
import 'dart:math' as math;
Hans Muller's avatar
Hans Muller committed
7
import 'dart:ui' show lerpDouble;
8

9
import 'package:flutter/foundation.dart';
10 11
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
12

13
import 'app_bar.dart';
14
import 'colors.dart';
Hans Muller's avatar
Hans Muller committed
15
import 'constants.dart';
16
import 'debug.dart';
17
import 'ink_well.dart';
18
import 'material.dart';
Hans Muller's avatar
Hans Muller committed
19
import 'tab_controller.dart';
20
import 'theme.dart';
21 22 23 24 25 26

const double _kTabHeight = 46.0;
const double _kTextAndIconTabHeight = 72.0;
const double _kMinTabWidth = 72.0;
const double _kMaxTabWidth = 264.0;

Hans Muller's avatar
Hans Muller committed
27 28 29 30 31 32 33 34 35 36 37 38
/// A material design [TabBar] tab. If both [icon] and [text] are
/// provided, the text is displayed below the icon.
///
/// See also:
///
///  * [TabBar], which displays a row of tabs.
///  * [TabBarView], which displays a widget for the currently selected tab.
///  * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
///  * <https://material.google.com/components/tabs.html>
class Tab extends StatelessWidget {
  /// Creates a material design [TabBar] tab. At least one of [text] and [icon]
  /// must be non-null.
39
  const Tab({
40
    Key key,
Hans Muller's avatar
Hans Muller committed
41 42
    this.text,
    this.icon,
43 44
  }) : assert(text != null || icon != null),
       super(key: key);
45

Hans Muller's avatar
Hans Muller committed
46
  /// The text to display as the tab's label.
47
  final String text;
48

Hans Muller's avatar
Hans Muller committed
49
  /// An icon to display as the tab's label.
Ian Hickson's avatar
Ian Hickson committed
50
  final Widget icon;
51

52
  Widget _buildLabelText() {
Hans Muller's avatar
Hans Muller committed
53
    return new Text(text, softWrap: false, overflow: TextOverflow.fade);
54 55
  }

56
  @override
57
  Widget build(BuildContext context) {
58
    assert(debugCheckHasMaterial(context));
Hans Muller's avatar
Hans Muller committed
59 60 61 62 63 64 65 66 67

    double height;
    Widget label;
    if (icon == null) {
      height = _kTabHeight;
      label = _buildLabelText();
    } else if (text == null) {
      height = _kTabHeight;
      label = icon;
68
    } else {
Hans Muller's avatar
Hans Muller committed
69 70
      height = _kTextAndIconTabHeight;
      label = new Column(
71 72
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
73
        children: <Widget>[
74
          new Container(
Hans Muller's avatar
Hans Muller committed
75
            child: icon,
76
            margin: const EdgeInsets.only(bottom: 10.0)
77 78
          ),
          _buildLabelText()
79
        ]
80 81 82
      );
    }

Hans Muller's avatar
Hans Muller committed
83
    return new Container(
84
      padding: kTabLabelPadding,
Hans Muller's avatar
Hans Muller committed
85
      height: height,
86
      constraints: const BoxConstraints(minWidth: _kMinTabWidth),
Hans Muller's avatar
Hans Muller committed
87
      child: new Center(child: label),
88
    );
89
  }
Hixie's avatar
Hixie committed
90

91
  @override
92
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
93 94 95
    super.debugFillProperties(description);
    description.add(new StringProperty('text', text, defaultValue: null));
    description.add(new DiagnosticsProperty<Widget>('icon', icon, defaultValue: null));
Hixie's avatar
Hixie committed
96
  }
97 98
}

Hans Muller's avatar
Hans Muller committed
99
class _TabStyle extends AnimatedWidget {
100
  const _TabStyle({
Hans Muller's avatar
Hans Muller committed
101 102 103 104
    Key key,
    Animation<double> animation,
    this.selected,
    this.labelColor,
105
    this.unselectedLabelColor,
106 107
    this.labelStyle,
    this.unselectedLabelStyle,
108
    @required this.child,
109
  }) : super(key: key, listenable: animation);
110

111 112
  final TextStyle labelStyle;
  final TextStyle unselectedLabelStyle;
Hans Muller's avatar
Hans Muller committed
113 114
  final bool selected;
  final Color labelColor;
115
  final Color unselectedLabelColor;
Hans Muller's avatar
Hans Muller committed
116
  final Widget child;
117

118
  @override
Hans Muller's avatar
Hans Muller committed
119 120
  Widget build(BuildContext context) {
    final ThemeData themeData = Theme.of(context);
121 122 123 124 125
    final TextStyle defaultStyle = labelStyle ?? themeData.primaryTextTheme.body2;
    final TextStyle defaultUnselectedStyle = unselectedLabelStyle ?? labelStyle ?? themeData.primaryTextTheme.body2;
    final TextStyle textStyle = selected
      ? defaultStyle
      : defaultUnselectedStyle;
Hans Muller's avatar
Hans Muller committed
126
    final Color selectedColor = labelColor ?? themeData.primaryTextTheme.body2.color;
127
    final Color unselectedColor = unselectedLabelColor ?? selectedColor.withAlpha(0xB2); // 70% alpha
128
    final Animation<double> animation = listenable;
Hans Muller's avatar
Hans Muller committed
129
    final Color color = selected
130 131
      ? Color.lerp(selectedColor, unselectedColor, animation.value)
      : Color.lerp(unselectedColor, selectedColor, animation.value);
Hans Muller's avatar
Hans Muller committed
132 133 134

    return new DefaultTextStyle(
      style: textStyle.copyWith(color: color),
135
      child: IconTheme.merge(
Hans Muller's avatar
Hans Muller committed
136 137 138 139 140 141
        data: new IconThemeData(
          size: 24.0,
          color: color,
        ),
        child: child,
      ),
142 143 144 145
    );
  }
}

Hans Muller's avatar
Hans Muller committed
146 147 148 149 150 151 152
class _TabLabelBarRenderer extends RenderFlex {
  _TabLabelBarRenderer({
    List<RenderBox> children,
    Axis direction,
    MainAxisSize mainAxisSize,
    MainAxisAlignment mainAxisAlignment,
    CrossAxisAlignment crossAxisAlignment,
153 154
    TextDirection textDirection,
    VerticalDirection verticalDirection,
Hans Muller's avatar
Hans Muller committed
155
    TextBaseline textBaseline,
156
    @required this.onPerformLayout,
157 158 159 160 161 162 163
  }) : assert(onPerformLayout != null),
       super(
         children: children,
         direction: direction,
         mainAxisSize: mainAxisSize,
         mainAxisAlignment: mainAxisAlignment,
         crossAxisAlignment: crossAxisAlignment,
164 165
         textDirection: textDirection,
         verticalDirection: verticalDirection,
166 167
         textBaseline: textBaseline,
       );
168

Hans Muller's avatar
Hans Muller committed
169
  ValueChanged<List<double>> onPerformLayout;
170 171

  @override
Hans Muller's avatar
Hans Muller committed
172 173 174 175 176 177 178 179 180 181 182 183
  void performLayout() {
    super.performLayout();
    RenderBox child = firstChild;
    final List<double> xOffsets = <double>[];
    while (child != null) {
      final FlexParentData childParentData = child.parentData;
      xOffsets.add(childParentData.offset.dx);
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
    }
    xOffsets.add(size.width); // So xOffsets[lastTabIndex + 1] is valid.
    onPerformLayout(xOffsets);
184
  }
Hans Muller's avatar
Hans Muller committed
185 186
}

Hans Muller's avatar
Hans Muller committed
187 188 189 190 191 192 193 194
// This class and its renderer class only exist to report the widths of the tabs
// upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
// or in response to input.
class _TabLabelBar extends Flex {
  _TabLabelBar({
    Key key,
    MainAxisAlignment mainAxisAlignment,
    CrossAxisAlignment crossAxisAlignment,
195 196
    TextDirection textDirection,
    VerticalDirection verticalDirection: VerticalDirection.down,
Hans Muller's avatar
Hans Muller committed
197 198 199 200 201 202 203 204 205
    List<Widget> children: const <Widget>[],
    this.onPerformLayout,
  }) : super(
    key: key,
    children: children,
    direction: Axis.horizontal,
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.center,
206 207
    textDirection: textDirection,
    verticalDirection: verticalDirection,
Hans Muller's avatar
Hans Muller committed
208 209 210 211 212 213 214 215 216 217 218
  );

  final ValueChanged<List<double>> onPerformLayout;

  @override
  RenderFlex createRenderObject(BuildContext context) {
    return new _TabLabelBarRenderer(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
219 220
      textDirection: getEffectiveTextDirection(context),
      verticalDirection: verticalDirection,
Hans Muller's avatar
Hans Muller committed
221 222
      textBaseline: textBaseline,
      onPerformLayout: onPerformLayout,
223
    );
224 225
  }

226
  @override
Hans Muller's avatar
Hans Muller committed
227 228 229
  void updateRenderObject(BuildContext context, _TabLabelBarRenderer renderObject) {
    super.updateRenderObject(context, renderObject);
    renderObject.onPerformLayout = onPerformLayout;
Hans Muller's avatar
Hans Muller committed
230
  }
Hans Muller's avatar
Hans Muller committed
231
}
Hans Muller's avatar
Hans Muller committed
232

Hans Muller's avatar
Hans Muller committed
233 234 235 236
double _indexChangeProgress(TabController controller) {
  final double controllerValue = controller.animation.value;
  final double previousIndex = controller.previousIndex.toDouble();
  final double currentIndex = controller.index.toDouble();
237 238 239 240 241 242 243 244

  // The controller's offset is changing because the user is dragging the
  // TabBarView's PageView to the left or right.
  if (!controller.indexIsChanging)
    return (currentIndex -  controllerValue).abs().clamp(0.0, 1.0);

  // The TabController animation's value is changing from previousIndex to currentIndex.
  return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs();
Hans Muller's avatar
Hans Muller committed
245
}
246

Hans Muller's avatar
Hans Muller committed
247
class _IndicatorPainter extends CustomPainter {
248 249 250 251 252 253
  _IndicatorPainter({
    this.controller,
    this.indicatorWeight,
    this.indicatorPadding,
    List<double> initialTabOffsets,
  }) : _tabOffsets = initialTabOffsets, super(repaint: controller.animation);
254

255 256 257 258 259 260
  final TabController controller;
  final double indicatorWeight;
  final EdgeInsets indicatorPadding;
  List<double> _tabOffsets;
  Color _color;
  Rect _currentRect;
261

262 263 264
  // _tabOffsets[index] is the offset of the left edge of the tab at index, and
  // _tabOffsets[_tabOffsets.length] is the right edge of the last tab.
  int get maxTabIndex => _tabOffsets.length - 2;
265

Hans Muller's avatar
Hans Muller committed
266
  Rect indicatorRect(Size tabBarSize, int tabIndex) {
267 268 269 270 271 272 273
    assert(_tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex);
    double tabLeft = _tabOffsets[tabIndex];
    double tabRight = _tabOffsets[tabIndex + 1];
    tabLeft = math.min(tabLeft + indicatorPadding.left, tabRight);
    tabRight = math.max(tabRight - indicatorPadding.right, tabLeft);
    final double tabTop = tabBarSize.height - indicatorWeight;
    return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight);
274 275
  }

Hans Muller's avatar
Hans Muller committed
276 277 278 279
  @override
  void paint(Canvas canvas, Size size) {
    if (controller.indexIsChanging) {
      final Rect targetRect = indicatorRect(size, controller.index);
280
      _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller));
Hans Muller's avatar
Hans Muller committed
281 282 283 284 285 286 287 288 289
    } else {
      final int currentIndex = controller.index;
      final Rect left = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
      final Rect middle = indicatorRect(size, currentIndex);
      final Rect right = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null;

      final double index = controller.index.toDouble();
      final double value = controller.animation.value;
      if (value == index - 1.0)
290
        _currentRect = left ?? middle;
Hans Muller's avatar
Hans Muller committed
291
      else if (value == index + 1.0)
292
        _currentRect = right ?? middle;
Hans Muller's avatar
Hans Muller committed
293
      else if (value == index)
294
         _currentRect = middle;
Hans Muller's avatar
Hans Muller committed
295
      else if (value < index)
296
        _currentRect = left == null ? middle : Rect.lerp(middle, left, index - value);
Hans Muller's avatar
Hans Muller committed
297
      else
298
        _currentRect = right == null ? middle : Rect.lerp(middle, right, value - index);
Hans Muller's avatar
Hans Muller committed
299
    }
300 301
    assert(_currentRect != null);
    canvas.drawRect(_currentRect, new Paint()..color = _color);
302 303
  }

304
  static bool _tabOffsetsNotEqual(List<double> a, List<double> b) {
Hans Muller's avatar
Hans Muller committed
305 306 307 308
    assert(a != null && b != null && a.length == b.length);
    for(int i = 0; i < a.length; i++) {
      if (a[i] != b[i])
        return true;
309
    }
Hans Muller's avatar
Hans Muller committed
310
    return false;
311 312
  }

313
  @override
Hans Muller's avatar
Hans Muller committed
314 315
  bool shouldRepaint(_IndicatorPainter old) {
    return controller != old.controller ||
316 317 318
      _tabOffsets?.length != old._tabOffsets?.length ||
      _tabOffsetsNotEqual(_tabOffsets, old._tabOffsets) ||
      _currentRect != old._currentRect;
Hans Muller's avatar
Hans Muller committed
319
  }
320 321
}

Hans Muller's avatar
Hans Muller committed
322 323
class _ChangeAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  _ChangeAnimation(this.controller);
324

Hans Muller's avatar
Hans Muller committed
325
  final TabController controller;
326 327

  @override
Hans Muller's avatar
Hans Muller committed
328 329 330 331
  Animation<double> get parent => controller.animation;

  @override
  double get value => _indexChangeProgress(controller);
332 333
}

334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
class _DragAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  _DragAnimation(this.controller, this.index);

  final TabController controller;
  final int index;

  @override
  Animation<double> get parent => controller.animation;

  @override
  double get value {
    assert(!controller.indexIsChanging);
    return (controller.animation.value - index.toDouble()).abs().clamp(0.0, 1.0);
  }
}

350 351 352 353
// This class, and TabBarScrollController, only exist to handle the the case
// where a scrollable TabBar has a non-zero initialIndex. In that case we can
// only compute the scroll position's initial scroll offset (the "correct"
// pixels value) after the TabBar viewport width and scroll limits are known.
354
class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
355 356
  _TabBarScrollPosition({
    ScrollPhysics physics,
357
    ScrollContext context,
358 359 360 361
    ScrollPosition oldPosition,
    this.tabBar,
  }) : super(
    physics: physics,
362
    context: context,
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    initialPixels: null,
    oldPosition: oldPosition,
  );

  final _TabBarState tabBar;

  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    bool result = true;
    if (pixels == null) {
      correctPixels(tabBar._initialScrollOffset(viewportDimension, minScrollExtent, maxScrollExtent));
      result = false;
    }
    return super.applyContentDimensions(minScrollExtent, maxScrollExtent) && result;
  }
}

// This class, and TabBarScrollPosition, only exist to handle the the case
// where a scrollable TabBar has a non-zero initialIndex.
class _TabBarScrollController extends ScrollController {
  _TabBarScrollController(this.tabBar);

  final _TabBarState tabBar;

  @override
388
  ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
389 390
    return new _TabBarScrollPosition(
      physics: physics,
391
      context: context,
392 393 394 395 396 397
      oldPosition: oldPosition,
      tabBar: tabBar,
    );
  }
}

398
/// A material design widget that displays a horizontal row of tabs.
399
///
400 401
/// Typically created as the [AppBar.bottom] part of an [AppBar] and in
/// conjuction with a [TabBarView].
402
///
403
/// If a [TabController] is not provided, then there must be a
404 405
/// [DefaultTabController] ancestor. The tab controller's [TabController.length]
/// must equal the length of the [tabs] list.
406 407 408 409 410
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
411
///  * [TabBarView], which displays page views that correspond to each tab.
412
class TabBar extends StatefulWidget implements PreferredSizeWidget {
413 414
  /// Creates a material design tab bar.
  ///
415
  /// The [tabs] argument must not be null and its length must match the [controller]'s
416
  /// [TabController.length].
417 418 419
  ///
  /// If a [TabController] is not provided, then there must be a
  /// [DefaultTabController] ancestor.
420
  ///
421
  /// The [indicatorWeight] parameter defaults to 2, and must not be null.
422
  ///
423
  /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.
424
  TabBar({
425
    Key key,
Hans Muller's avatar
Hans Muller committed
426 427
    @required this.tabs,
    this.controller,
428 429
    this.isScrollable: false,
    this.indicatorColor,
430 431
    this.indicatorWeight: 2.0,
    this.indicatorPadding: EdgeInsets.zero,
Hans Muller's avatar
Hans Muller committed
432
    this.labelColor,
433
    this.labelStyle,
434
    this.unselectedLabelColor,
435
    this.unselectedLabelStyle,
436
  }) : assert(tabs != null),
437
       assert(isScrollable != null),
438 439
       assert(indicatorWeight != null && indicatorWeight > 0.0),
       assert(indicatorPadding != null),
440
       super(key: key);
441

442 443 444
  /// Typically a list of two or more [Tab] widgets.
  ///
  /// The length of this list must match the [controller]'s [TabController.length].
Hans Muller's avatar
Hans Muller committed
445 446 447
  final List<Widget> tabs;

  /// This widget's selection and animation state.
448
  ///
Hans Muller's avatar
Hans Muller committed
449 450 451
  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  /// will be used.
  final TabController controller;
452 453 454 455 456 457

  /// 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.
458
  final bool isScrollable;
459

460 461 462 463
  /// 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;

464 465 466 467 468 469 470
  /// The thickness of the line that appears below the selected tab. The value
  /// of this parameter must be greater than zero.
  ///
  /// The default value of [indicatorWeight] is 2.0.
  final double indicatorWeight;

  /// The horizontal padding for the line that appears below the selected tab.
471
  /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align
472 473 474
  /// the indicator with the tab's text for [Tab] widgets and all but the
  /// shortest [Tab.text] values.
  ///
475 476 477
  /// The [EdgeInsets.top] and [EdgeInsets.bottom] values of the
  /// [indicatorPadding] are ignored.
  ///
478 479 480
  /// The default value of [indicatorPadding] is [EdgeInsets.zero].
  final EdgeInsets indicatorPadding;

481 482 483 484 485 486 487
  /// The color of selected tab labels.
  ///
  /// Unselected tab labels are rendered with the same color rendered at 70%
  /// opacity unless [unselectedLabelColor] is non-null.
  ///
  /// If this parameter is null then the color of the theme's body2 text color
  /// is used.
488 489
  final Color labelColor;

490 491 492 493 494 495
  /// The color of unselected tab labels.
  ///
  /// If this property is null, Unselected tab labels are rendered with the
  /// [labelColor] rendered at 70% opacity.
  final Color unselectedLabelColor;

496 497 498 499 500 501 502 503 504 505 506 507 508 509
  /// The text style of the selected tab labels. If [unselectedLabelStyle] is
  /// null then this text style will be used for both selected and unselected
  /// label styles.
  ///
  /// If this property is null then the text style of the theme's body2
  /// definition is used.
  final TextStyle labelStyle;

  /// The text style of the unselected tab labels
  ///
  /// If this property is null then the [labelStyle] value is used. If [labelStyle]
  /// is null then the text style of the theme's body2 definition is used.
  final TextStyle unselectedLabelStyle;

510 511 512
  /// A size whose height depends on if the tabs have both icons and text.
  ///
  /// [AppBar] uses this this size to compute its own preferred size.
513
  @override
514
  Size get preferredSize {
515 516 517
    for (Widget item in tabs) {
      if (item is Tab) {
        final Tab tab = item;
Hans Muller's avatar
Hans Muller committed
518
        if (tab.text != null && tab.icon != null)
519
          return new Size.fromHeight(_kTextAndIconTabHeight + indicatorWeight);
Hans Muller's avatar
Hans Muller committed
520
      }
521
    }
522
    return new Size.fromHeight(_kTabHeight + indicatorWeight);
523 524
  }

525
  @override
Hans Muller's avatar
Hans Muller committed
526
  _TabBarState createState() => new _TabBarState();
527
}
528

Hans Muller's avatar
Hans Muller committed
529
class _TabBarState extends State<TabBar> {
530
  ScrollController _scrollController;
Hans Muller's avatar
Hans Muller committed
531

Hans Muller's avatar
Hans Muller committed
532 533 534 535 536
  TabController _controller;
  _IndicatorPainter _indicatorPainter;
  int _currentIndex;

  void _updateTabController() {
537
    final TabController newController = widget.controller ?? DefaultTabController.of(context);
538 539 540
    assert(() {
      if (newController == null) {
        throw new FlutterError(
541 542
          'No TabController for ${widget.runtimeType}.\n'
          'When creating a ${widget.runtimeType}, you must either provide an explicit '
543
          'TabController using the "controller" property, or you must ensure that there '
544
          'is a DefaultTabController above the ${widget.runtimeType}.\n'
545 546 547 548 549
          'In this case, there was neither an explicit controller nor a default controller.'
        );
      }
      return true;
    });
Hans Muller's avatar
Hans Muller committed
550
    if (newController == _controller)
551
      return;
Hans Muller's avatar
Hans Muller committed
552

553 554 555 556
    if (_controller != null) {
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
      _controller.removeListener(_handleTabControllerTick);
    }
Hans Muller's avatar
Hans Muller committed
557 558
    _controller = newController;
    if (_controller != null) {
559 560
      _controller.animation.addListener(_handleTabControllerAnimationTick);
      _controller.addListener(_handleTabControllerTick);
Hans Muller's avatar
Hans Muller committed
561
      _currentIndex = _controller.index;
562 563 564 565 566 567 568
      final List<double> offsets = _indicatorPainter?._tabOffsets;
      _indicatorPainter = new _IndicatorPainter(
        controller: _controller,
        indicatorWeight: widget.indicatorWeight,
        indicatorPadding: widget.indicatorPadding,
        initialTabOffsets: offsets,
      );
569
    }
570 571
  }

572
  @override
573 574
  void didChangeDependencies() {
    super.didChangeDependencies();
Hans Muller's avatar
Hans Muller committed
575
    _updateTabController();
576 577
  }

578
  @override
579 580 581
  void didUpdateWidget(TabBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller != oldWidget.controller)
Hans Muller's avatar
Hans Muller committed
582
      _updateTabController();
583 584
  }

585
  @override
Hans Muller's avatar
Hans Muller committed
586
  void dispose() {
587
    if (_controller != null) {
588
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
589 590
      _controller.removeListener(_handleTabControllerTick);
    }
Hans Muller's avatar
Hans Muller committed
591 592
    // We don't own the _controller Animation, so it's not disposed here.
    super.dispose();
593
  }
594

595 596 597
  // _tabOffsets[index] is the offset of the left edge of the tab at index, and
  // _tabOffsets[_tabOffsets.length] is the right edge of the last tab.
  int get maxTabIndex => _indicatorPainter._tabOffsets.length - 2;
598

599 600 601
  double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) {
    if (!widget.isScrollable)
      return 0.0;
602
    final List<double> tabOffsets = _indicatorPainter._tabOffsets;
603 604 605 606
    assert(tabOffsets != null && index >= 0 && index <= maxTabIndex);
    final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0;
    return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent);
  }
607

608
  double _tabCenteredScrollOffset(int index) {
609
    final ScrollPosition position = _scrollController.position;
610 611 612 613 614
    return _tabScrollOffset(index, position.viewportDimension, position.minScrollExtent, position.maxScrollExtent);
  }

  double _initialScrollOffset(double viewportWidth, double minExtent, double maxExtent) {
    return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
615 616
  }

Hans Muller's avatar
Hans Muller committed
617
  void _scrollToCurrentIndex() {
618 619
    final double offset = _tabCenteredScrollOffset(_currentIndex);
    _scrollController.animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
Hans Muller's avatar
Hans Muller committed
620 621 622
  }

  void _scrollToControllerValue() {
623 624 625
    final double left = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
    final double middle = _tabCenteredScrollOffset(_currentIndex);
    final double right = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null;
Hans Muller's avatar
Hans Muller committed
626 627 628 629 630 631 632 633 634 635 636 637 638 639

    final double index = _controller.index.toDouble();
    final double value = _controller.animation.value;
    double offset;
    if (value == index - 1.0)
      offset = left ?? middle;
    else if (value == index + 1.0)
      offset = right ?? middle;
    else if (value == index)
       offset = middle;
    else if (value < index)
      offset = left == null ? middle : lerpDouble(middle, left, index - value);
    else
      offset = right == null ? middle : lerpDouble(middle, right, value - index);
640

641
    _scrollController.jumpTo(offset);
642
  }
643

644
  void _handleTabControllerAnimationTick() {
Hans Muller's avatar
Hans Muller committed
645
    assert(mounted);
646
    if (!_controller.indexIsChanging && widget.isScrollable) {
647 648
      // Sync the TabBar's scroll position with the TabBarView's PageView.
      _currentIndex = _controller.index;
Hans Muller's avatar
Hans Muller committed
649 650
      _scrollToControllerValue();
    }
651 652
  }

653 654 655 656 657 658 659
  void _handleTabControllerTick() {
    setState(() {
      // Rebuild the tabs after a (potentially animated) index change
      // has completed.
    });
  }

660
  // Called each time layout completes.
Hans Muller's avatar
Hans Muller committed
661
  void _saveTabOffsets(List<double> tabOffsets) {
662
    _indicatorPainter?._tabOffsets = tabOffsets;
663 664
  }

Hans Muller's avatar
Hans Muller committed
665
  void _handleTap(int index) {
666
    assert(index >= 0 && index < widget.tabs.length);
Hans Muller's avatar
Hans Muller committed
667
    _controller.animateTo(index);
Hans Muller's avatar
Hans Muller committed
668 669
  }

670 671 672 673
  Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) {
    return new _TabStyle(
      animation: animation,
      selected: selected,
674 675 676 677
      labelColor: widget.labelColor,
      unselectedLabelColor: widget.unselectedLabelColor,
      labelStyle: widget.labelStyle,
      unselectedLabelStyle: widget.unselectedLabelStyle,
678 679 680 681
      child: child,
    );
  }

Hans Muller's avatar
Hans Muller committed
682 683
  @override
  Widget build(BuildContext context) {
684 685 686 687 688 689
    if (_controller.length == 0) {
      return new Container(
        height: _kTabHeight + widget.indicatorWeight,
      );
    }

690
    final List<Widget> wrappedTabs = new List<Widget>.from(widget.tabs, growable: false);
Hans Muller's avatar
Hans Muller committed
691 692 693 694 695

    // If the controller was provided by DefaultTabController and we're part
    // of a Hero (typically the AppBar), then we will not be able to find the
    // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213.
    if (_controller != null) {
696 697
      _indicatorPainter._color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
      if (_indicatorPainter._color == Material.of(context).color) {
Hans Muller's avatar
Hans Muller committed
698 699 700 701 702 703 704
        // ThemeData tries to avoid this by having indicatorColor avoid being the
        // primaryColor. However, it's possible that the tab bar is on a
        // 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.
705
        _indicatorPainter._color = Colors.white;
706
      }
707

Hans Muller's avatar
Hans Muller committed
708 709
      if (_controller.index != _currentIndex) {
        _currentIndex = _controller.index;
710
        if (widget.isScrollable)
Hans Muller's avatar
Hans Muller committed
711 712
          _scrollToCurrentIndex();
      }
Hans Muller's avatar
Hans Muller committed
713

Hans Muller's avatar
Hans Muller committed
714
      final int previousIndex = _controller.previousIndex;
715

Hans Muller's avatar
Hans Muller committed
716
      if (_controller.indexIsChanging) {
717
        // The user tapped on a tab, the tab controller's animation is running.
Hans Muller's avatar
Hans Muller committed
718
        assert(_currentIndex != previousIndex);
719 720 721
        final Animation<double> animation = new _ChangeAnimation(_controller);
        wrappedTabs[_currentIndex] = _buildStyledTab(wrappedTabs[_currentIndex], true, animation);
        wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
Hans Muller's avatar
Hans Muller committed
722
      } else {
723 724 725 726 727 728 729 730 731
        // The user is dragging the TabBarView's PageView left or right.
        final int tabIndex = _currentIndex;
        final Animation<double> centerAnimation = new _DragAnimation(_controller, tabIndex);
        wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
        if (_currentIndex > 0) {
          final int tabIndex = _currentIndex - 1;
          final Animation<double> leftAnimation = new _DragAnimation(_controller, tabIndex);
          wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, leftAnimation);
        }
732
        if (_currentIndex < widget.tabs.length - 1) {
733 734 735 736
          final int tabIndex = _currentIndex + 1;
          final Animation<double> rightAnimation = new _DragAnimation(_controller, tabIndex);
          wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, rightAnimation);
        }
Hans Muller's avatar
Hans Muller committed
737
      }
Hans Muller's avatar
Hans Muller committed
738 739
    }

Hans Muller's avatar
Hans Muller committed
740 741 742
    // Add the tap handler to each tab. If the tab bar is scrollable
    // then give all of the tabs equal flexibility so that their widths
    // reflect the intrinsic width of their labels.
743 744
    final int tabCount = widget.tabs.length;
    for (int index = 0; index < tabCount; index++) {
745
      wrappedTabs[index] = new MergeSemantics(
746 747 748 749
        child: new Stack(
          children: <Widget>[
            new InkWell(
              onTap: () { _handleTap(index); },
750 751 752 753
              child: new Padding(
                padding: new EdgeInsets.only(bottom: widget.indicatorWeight),
                child: wrappedTabs[index],
              ),
754 755 756 757 758 759 760
            ),
            new Semantics(
              selected: index == _currentIndex,
              // TODO(goderbauer): I10N-ify
              label: 'Tab ${index + 1} of $tabCount',
            ),
          ],
761
        ),
Hans Muller's avatar
Hans Muller committed
762
      );
763
      if (!widget.isScrollable)
764
        wrappedTabs[index] = new Expanded(child: wrappedTabs[index]);
Hans Muller's avatar
Hans Muller committed
765 766 767 768
    }

    Widget tabBar = new CustomPaint(
      painter: _indicatorPainter,
769
      child: new _TabStyle(
770
          animation: kAlwaysDismissedAnimation,
Hans Muller's avatar
Hans Muller committed
771
          selected: false,
772 773 774 775
          labelColor: widget.labelColor,
          unselectedLabelColor: widget.unselectedLabelColor,
          labelStyle: widget.labelStyle,
          unselectedLabelStyle: widget.unselectedLabelStyle,
Hans Muller's avatar
Hans Muller committed
776 777 778 779 780
          child: new _TabLabelBar(
            onPerformLayout: _saveTabOffsets,
            children:  wrappedTabs,
          ),
        ),
781
    );
Hans Muller's avatar
Hans Muller committed
782

783
    if (widget.isScrollable) {
784
      _scrollController ??= new _TabBarScrollController(this);
785
      tabBar = new SingleChildScrollView(
Hans Muller's avatar
Hans Muller committed
786
        scrollDirection: Axis.horizontal,
787 788
        controller: _scrollController,
        child: tabBar,
789 790 791
      );
    }

Hans Muller's avatar
Hans Muller committed
792
    return tabBar;
793 794 795
  }
}

796
/// A page view that displays the widget which corresponds to the currently
Hans Muller's avatar
Hans Muller committed
797 798 799 800 801
/// selected tab. Typically used in conjuction with a [TabBar].
///
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// ancestor.
class TabBarView extends StatefulWidget {
802
  /// Creates a page view with one child per tab.
Hans Muller's avatar
Hans Muller committed
803 804 805 806 807 808
  ///
  /// The length of [children] must be the same as the [controller]'s length.
  TabBarView({
    Key key,
    @required this.children,
    this.controller,
809
    this.physics,
810
  }) : assert(children != null), super(key: key);
811

Hans Muller's avatar
Hans Muller committed
812 813 814 815 816 817 818 819 820
  /// This widget's selection and animation state.
  ///
  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  /// will be used.
  final TabController controller;

  /// One widget per tab.
  final List<Widget> children;

821 822 823 824 825 826 827 828 829 830 831
  /// How the page view should respond to user input.
  ///
  /// For example, determines how the page view continues to animate after the
  /// user stops dragging the page view.
  ///
  /// The physics are modified to snap to page boundaries using
  /// [PageScrollPhysics] prior to being used.
  ///
  /// Defaults to matching platform conventions.
  final ScrollPhysics physics;

832
  @override
Hans Muller's avatar
Hans Muller committed
833 834
  _TabBarViewState createState() => new _TabBarViewState();
}
835

836
final PageScrollPhysics _kTabBarViewPhysics = const PageScrollPhysics().applyTo(const ClampingScrollPhysics());
837

838
class _TabBarViewState extends State<TabBarView> {
Hans Muller's avatar
Hans Muller committed
839
  TabController _controller;
840
  PageController _pageController;
Hans Muller's avatar
Hans Muller committed
841 842 843 844 845
  List<Widget> _children;
  int _currentIndex;
  int _warpUnderwayCount = 0;

  void _updateTabController() {
846
    final TabController newController = widget.controller ?? DefaultTabController.of(context);
847 848 849
    assert(() {
      if (newController == null) {
        throw new FlutterError(
850 851
          'No TabController for ${widget.runtimeType}.\n'
          'When creating a ${widget.runtimeType}, you must either provide an explicit '
852
          'TabController using the "controller" property, or you must ensure that there '
853
          'is a DefaultTabController above the ${widget.runtimeType}.\n'
854 855 856 857 858
          'In this case, there was neither an explicit controller nor a default controller.'
        );
      }
      return true;
    });
Hans Muller's avatar
Hans Muller committed
859 860 861 862
    if (newController == _controller)
      return;

    if (_controller != null)
863
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
Hans Muller's avatar
Hans Muller committed
864 865
    _controller = newController;
    if (_controller != null)
866
      _controller.animation.addListener(_handleTabControllerAnimationTick);
867 868
  }

Hans Muller's avatar
Hans Muller committed
869 870 871
  @override
  void initState() {
    super.initState();
872
    _children = widget.children;
Hans Muller's avatar
Hans Muller committed
873 874
  }

Hans Muller's avatar
Hans Muller committed
875
  @override
876 877
  void didChangeDependencies() {
    super.didChangeDependencies();
Hans Muller's avatar
Hans Muller committed
878 879
    _updateTabController();
    _currentIndex = _controller?.index;
880
    _pageController = new PageController(initialPage: _currentIndex ?? 0);
881 882
  }

883
  @override
884 885 886
  void didUpdateWidget(TabBarView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller != oldWidget.controller)
Hans Muller's avatar
Hans Muller committed
887
      _updateTabController();
888 889
    if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
      _children = widget.children;
890 891
  }

892
  @override
Hans Muller's avatar
Hans Muller committed
893 894
  void dispose() {
    if (_controller != null)
895
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
Hans Muller's avatar
Hans Muller committed
896 897 898
    // We don't own the _controller Animation, so it's not disposed here.
    super.dispose();
  }
Hans Muller's avatar
Hans Muller committed
899

900 901
  void _handleTabControllerAnimationTick() {
    if (_warpUnderwayCount > 0 || !_controller.indexIsChanging)
Hans Muller's avatar
Hans Muller committed
902
      return; // This widget is driving the controller's animation.
Hans Muller's avatar
Hans Muller committed
903

Hans Muller's avatar
Hans Muller committed
904 905 906
    if (_controller.index != _currentIndex) {
      _currentIndex = _controller.index;
      _warpToCurrentIndex();
907
    }
Hans Muller's avatar
Hans Muller committed
908
  }
909

Hans Muller's avatar
Hans Muller committed
910 911 912
  Future<Null> _warpToCurrentIndex() async {
    if (!mounted)
      return new Future<Null>.value();
913

914
    if (_pageController.page == _currentIndex.toDouble())
Hans Muller's avatar
Hans Muller committed
915
      return new Future<Null>.value();
916

Hans Muller's avatar
Hans Muller committed
917 918
    final int previousIndex = _controller.previousIndex;
    if ((_currentIndex - previousIndex).abs() == 1)
919
      return _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
Hans Muller's avatar
Hans Muller committed
920 921

    assert((_currentIndex - previousIndex).abs() > 1);
922
    int initialPage;
Hans Muller's avatar
Hans Muller committed
923 924
    setState(() {
      _warpUnderwayCount += 1;
925
      _children = new List<Widget>.from(widget.children, growable: false);
Hans Muller's avatar
Hans Muller committed
926 927
      if (_currentIndex > previousIndex) {
        _children[_currentIndex - 1] = _children[previousIndex];
928
        initialPage = _currentIndex - 1;
Hans Muller's avatar
Hans Muller committed
929 930
      } else {
        _children[_currentIndex + 1] = _children[previousIndex];
931
        initialPage = _currentIndex + 1;
Hans Muller's avatar
Hans Muller committed
932 933
      }
    });
934

935
    _pageController.jumpToPage(initialPage);
Hans Muller's avatar
Hans Muller committed
936

937
    await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
Hans Muller's avatar
Hans Muller committed
938 939
    if (!mounted)
      return new Future<Null>.value();
Hans Muller's avatar
Hans Muller committed
940

Hans Muller's avatar
Hans Muller committed
941 942
    setState(() {
      _warpUnderwayCount -= 1;
943
      _children = widget.children;
Hans Muller's avatar
Hans Muller committed
944
    });
Hans Muller's avatar
Hans Muller committed
945 946
  }

947
  // Called when the PageView scrolls
Adam Barth's avatar
Adam Barth committed
948
  bool _handleScrollNotification(ScrollNotification notification) {
Hans Muller's avatar
Hans Muller committed
949 950
    if (_warpUnderwayCount > 0)
      return false;
Hans Muller's avatar
Hans Muller committed
951

952
    if (notification.depth != 0)
Hans Muller's avatar
Hans Muller committed
953
      return false;
Hans Muller's avatar
Hans Muller committed
954

955
    _warpUnderwayCount += 1;
956
    if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) {
957 958 959 960
      if ((_pageController.page - _controller.index).abs() > 1.0) {
        _controller.index = _pageController.page.floor();
        _currentIndex=_controller.index;
      }
961
      _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0);
962
    } else if (notification is ScrollEndNotification) {
963 964 965 966
      final ScrollPosition position = _pageController.position;
      final double pageTolerance = position.physics.tolerance.distance
          / (position.viewportDimension * _pageController.viewportFraction);
      _controller.index = (_pageController.page + pageTolerance).floor();
967
      _currentIndex = _controller.index;
Hans Muller's avatar
Hans Muller committed
968
    }
969
    _warpUnderwayCount -= 1;
Hans Muller's avatar
Hans Muller committed
970 971

    return false;
Hans Muller's avatar
Hans Muller committed
972 973
  }

974
  @override
Hans Muller's avatar
Hans Muller committed
975
  Widget build(BuildContext context) {
Adam Barth's avatar
Adam Barth committed
976
    return new NotificationListener<ScrollNotification>(
Hans Muller's avatar
Hans Muller committed
977
      onNotification: _handleScrollNotification,
978 979
      child: new PageView(
        controller: _pageController,
980
        physics: widget.physics == null ? _kTabBarViewPhysics : _kTabBarViewPhysics.applyTo(widget.physics),
Hans Muller's avatar
Hans Muller committed
981 982
        children: _children,
      ),
983
    );
Hans Muller's avatar
Hans Muller committed
984
  }
985
}
Hixie's avatar
Hixie committed
986

987
/// Displays a single circle with the specified border and background colors.
988 989 990
///
/// Used by [TabPageSelector] to indicate the selected page.
class TabPageSelectorIndicator extends StatelessWidget {
991
  /// Creates an indicator used by [TabPageSelector].
992
  ///
993
  /// The [backgroundColor], [borderColor], and [size] parameters must not be null.
994 995 996 997 998 999
  const TabPageSelectorIndicator({
    Key key,
    @required this.backgroundColor,
    @required this.borderColor,
    @required this.size,
  }) : assert(backgroundColor != null), assert(borderColor != null), assert(size != null), super(key: key);
1000 1001 1002 1003 1004 1005 1006

  /// The indicator circle's background color.
  final Color backgroundColor;

  /// The indicator circle's border color.
  final Color borderColor;

1007 1008 1009
  /// The indicator circle's diameter.
  final double size;

1010 1011 1012
  @override
  Widget build(BuildContext context) {
    return new Container(
1013 1014
      width: size,
      height: size,
1015 1016
      margin: const EdgeInsets.all(4.0),
      decoration: new BoxDecoration(
1017
        color: backgroundColor,
1018
        border: new Border.all(color: borderColor),
1019
        shape: BoxShape.circle,
1020 1021 1022 1023 1024
      ),
    );
  }
}

Hans Muller's avatar
Hans Muller committed
1025 1026
/// Displays a row of small circular indicators, one per tab. The selected
/// tab's indicator is highlighted. Often used in conjuction with a [TabBarView].
1027
///
Hans Muller's avatar
Hans Muller committed
1028 1029 1030 1031
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// ancestor.
class TabPageSelector extends StatelessWidget {
  /// Creates a compact widget that indicates which tab has been selected.
1032 1033 1034 1035 1036 1037 1038
  const TabPageSelector({
    Key key,
    this.controller,
    this.indicatorSize: 12.0,
    this.color,
    this.selectedColor,
  }) : assert(indicatorSize != null && indicatorSize > 0.0), super(key: key);
Hixie's avatar
Hixie committed
1039

Hans Muller's avatar
Hans Muller committed
1040 1041 1042 1043 1044 1045
  /// This widget's selection and animation state.
  ///
  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  /// will be used.
  final TabController controller;

1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060
  /// The indicator circle's diameter (the default value is 12.0).
  final double indicatorSize;

  /// The indicator cicle's fill color for unselected pages.
  ///
  /// If this parameter is null then the indicator is filled with [Colors.transparent].
  final Color color;

  /// The indicator cicle's fill color for selected pages and border color
  /// for all indicator circles.
  ///
  /// If this parameter is null then the indicator is filled with the theme's
  /// accent color, [ThemeData.accentColor].
  final Color selectedColor;

Hans Muller's avatar
Hans Muller committed
1061 1062 1063
  Widget _buildTabIndicator(
    int tabIndex,
    TabController tabController,
1064 1065
    ColorTween selectedColorTween,
    ColorTween previousColorTween,
Hans Muller's avatar
Hans Muller committed
1066
  ) {
Hixie's avatar
Hixie committed
1067
    Color background;
Hans Muller's avatar
Hans Muller committed
1068
    if (tabController.indexIsChanging) {
Hixie's avatar
Hixie committed
1069
      // The selection's animation is animating from previousValue to value.
1070
      final double t = 1.0 - _indexChangeProgress(tabController);
Hans Muller's avatar
Hans Muller committed
1071
      if (tabController.index == tabIndex)
1072
        background = selectedColorTween.lerp(t);
Hans Muller's avatar
Hans Muller committed
1073
      else if (tabController.previousIndex == tabIndex)
1074
        background = previousColorTween.lerp(t);
Hixie's avatar
Hixie committed
1075
      else
1076
        background = selectedColorTween.begin;
Hixie's avatar
Hixie committed
1077
    } else {
1078 1079 1080 1081
      // The selection's offset reflects how far the TabBarView has
      /// been dragged to the left (-1.0 to 0.0) or the right (0.0 to 1.0).
      final double offset = tabController.offset;
      if (tabController.index == tabIndex) {
1082
        background = selectedColorTween.lerp(1.0 - offset.abs());
1083
      } else if (tabController.index == tabIndex - 1 && offset > 0.0) {
1084
        background = selectedColorTween.lerp(offset);
1085
      } else if (tabController.index == tabIndex + 1 && offset < 0.0) {
1086
        background = selectedColorTween.lerp(-offset);
1087
      } else {
1088
        background = selectedColorTween.begin;
1089
      }
Hixie's avatar
Hixie committed
1090
    }
1091 1092
    return new TabPageSelectorIndicator(
      backgroundColor: background,
1093 1094
      borderColor: selectedColorTween.end,
      size: indicatorSize,
Hixie's avatar
Hixie committed
1095 1096 1097
    );
  }

1098
  @override
Hixie's avatar
Hixie committed
1099
  Widget build(BuildContext context) {
1100 1101 1102 1103
    final Color fixColor = color ?? Colors.transparent;
    final Color fixSelectedColor = selectedColor ?? Theme.of(context).accentColor;
    final ColorTween selectedColorTween = new ColorTween(begin: fixColor, end: fixSelectedColor);
    final ColorTween previousColorTween = new ColorTween(begin: fixSelectedColor, end: fixColor);
Hans Muller's avatar
Hans Muller committed
1104
    final TabController tabController = controller ?? DefaultTabController.of(context);
1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116
    assert(() {
      if (tabController == null) {
        throw new FlutterError(
          'No TabController for $runtimeType.\n'
          'When creating a $runtimeType, you must either provide an explicit TabController '
          'using the "controller" property, or you must ensure that there is a '
          'DefaultTabController above the $runtimeType.\n'
          'In this case, there was neither an explicit controller nor a default controller.'
        );
      }
      return true;
    });
Hans Muller's avatar
Hans Muller committed
1117 1118 1119 1120
    final Animation<double> animation = new CurvedAnimation(
      parent: tabController.animation,
      curve: Curves.fastOutSlowIn,
    );
Hixie's avatar
Hixie committed
1121 1122 1123 1124
    return new AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget child) {
        return new Semantics(
1125
          label: 'Page ${tabController.index + 1} of ${tabController.length}',
Hixie's avatar
Hixie committed
1126
          child: new Row(
Hans Muller's avatar
Hans Muller committed
1127
            mainAxisSize: MainAxisSize.min,
1128
            children: new List<Widget>.generate(tabController.length, (int tabIndex) {
1129
              return _buildTabIndicator(tabIndex, tabController, selectedColorTween, previousColorTween);
Hans Muller's avatar
Hans Muller committed
1130 1131
            }).toList(),
          ),
Hixie's avatar
Hixie committed
1132 1133 1134 1135 1136
        );
      }
    );
  }
}