tabs.dart 31.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';
Hans Muller's avatar
Hans Muller committed
6
import 'dart:ui' show lerpDouble;
7

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

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

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;
28
const EdgeInsets _kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0);
29

Hans Muller's avatar
Hans Muller committed
30 31 32 33 34 35 36 37 38 39 40 41 42
/// 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.
  Tab({
43
    Key key,
Hans Muller's avatar
Hans Muller committed
44 45 46 47
    this.text,
    this.icon,
  }) : super(key: key) {
    assert(text != null || icon != null);
48 49
  }

Hans Muller's avatar
Hans Muller committed
50
  /// The text to display as the tab's label.
51
  final String text;
52

Hans Muller's avatar
Hans Muller committed
53
  /// An icon to display as the tab's label.
Ian Hickson's avatar
Ian Hickson committed
54
  final Widget icon;
55

56
  Widget _buildLabelText() {
Hans Muller's avatar
Hans Muller committed
57
    return new Text(text, softWrap: false, overflow: TextOverflow.fade);
58 59
  }

60
  @override
61
  Widget build(BuildContext context) {
62
    assert(debugCheckHasMaterial(context));
Hans Muller's avatar
Hans Muller committed
63 64 65 66 67 68 69 70 71

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

Hans Muller's avatar
Hans Muller committed
87 88 89
    return new Container(
      padding: _kTabLabelPadding,
      height: height,
90
      constraints: const BoxConstraints(minWidth: _kMinTabWidth),
Hans Muller's avatar
Hans Muller committed
91
      child: new Center(child: label),
92
    );
93
  }
Hixie's avatar
Hixie committed
94

95
  @override
Hixie's avatar
Hixie committed
96 97
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
Hans Muller's avatar
Hans Muller committed
98 99 100 101
    if (text != null)
      description.add('text: $text');
    if (icon != null)
      description.add('icon: $icon');
Hixie's avatar
Hixie committed
102
  }
103 104
}

Hans Muller's avatar
Hans Muller committed
105 106 107 108 109 110
class _TabStyle extends AnimatedWidget {
  _TabStyle({
    Key key,
    Animation<double> animation,
    this.selected,
    this.labelColor,
111
    this.unselectedLabelColor,
112 113
    this.labelStyle,
    this.unselectedLabelStyle,
114
    @required this.child,
115
  }) : super(key: key, listenable: animation);
116

117 118
  final TextStyle labelStyle;
  final TextStyle unselectedLabelStyle;
Hans Muller's avatar
Hans Muller committed
119 120
  final bool selected;
  final Color labelColor;
121
  final Color unselectedLabelColor;
Hans Muller's avatar
Hans Muller committed
122
  final Widget child;
123

124
  @override
Hans Muller's avatar
Hans Muller committed
125 126
  Widget build(BuildContext context) {
    final ThemeData themeData = Theme.of(context);
127 128 129 130 131
    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
132
    final Color selectedColor = labelColor ?? themeData.primaryTextTheme.body2.color;
133
    final Color unselectedColor = unselectedLabelColor ?? selectedColor.withAlpha(0xB2); // 70% alpha
134
    final Animation<double> animation = listenable;
Hans Muller's avatar
Hans Muller committed
135
    final Color color = selected
136 137
      ? Color.lerp(selectedColor, unselectedColor, animation.value)
      : Color.lerp(unselectedColor, selectedColor, animation.value);
Hans Muller's avatar
Hans Muller committed
138 139 140 141 142 143 144 145 146 147 148

    return new DefaultTextStyle(
      style: textStyle.copyWith(color: color),
      child: new IconTheme.merge(
        context: context,
        data: new IconThemeData(
          size: 24.0,
          color: color,
        ),
        child: child,
      ),
149 150 151 152
    );
  }
}

Hans Muller's avatar
Hans Muller committed
153 154 155 156 157 158 159 160
class _TabLabelBarRenderer extends RenderFlex {
  _TabLabelBarRenderer({
    List<RenderBox> children,
    Axis direction,
    MainAxisSize mainAxisSize,
    MainAxisAlignment mainAxisAlignment,
    CrossAxisAlignment crossAxisAlignment,
    TextBaseline textBaseline,
161
    @required this.onPerformLayout,
Hans Muller's avatar
Hans Muller committed
162 163 164 165 166 167 168 169 170
  }) : super(
    children: children,
    direction: direction,
    mainAxisSize: mainAxisSize,
    mainAxisAlignment: mainAxisAlignment,
    crossAxisAlignment: crossAxisAlignment,
    textBaseline: textBaseline,
  ) {
    assert(onPerformLayout != null);
171
  }
172

Hans Muller's avatar
Hans Muller committed
173
  ValueChanged<List<double>> onPerformLayout;
174 175

  @override
Hans Muller's avatar
Hans Muller committed
176 177 178 179 180 181 182 183 184 185 186 187
  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);
188
  }
Hans Muller's avatar
Hans Muller committed
189 190
}

Hans Muller's avatar
Hans Muller committed
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
// 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,
    List<Widget> children: const <Widget>[],
    this.onPerformLayout,
  }) : super(
    key: key,
    children: children,
    direction: Axis.horizontal,
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.center,
  );

  final ValueChanged<List<double>> onPerformLayout;

  @override
  RenderFlex createRenderObject(BuildContext context) {
    return new _TabLabelBarRenderer(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
      textBaseline: textBaseline,
      onPerformLayout: onPerformLayout,
221
    );
222 223
  }

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

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

  // 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
243
}
244

Hans Muller's avatar
Hans Muller committed
245 246
class _IndicatorPainter extends CustomPainter {
  _IndicatorPainter(this.controller) : super(repaint: controller.animation);
247

Hans Muller's avatar
Hans Muller committed
248 249 250 251
  TabController controller;
  List<double> tabOffsets;
  Color color;
  Rect currentRect;
252

Hans Muller's avatar
Hans Muller committed
253 254 255
  // 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;
256

Hans Muller's avatar
Hans Muller committed
257 258 259 260 261 262
  Rect indicatorRect(Size tabBarSize, int tabIndex) {
    assert(tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex);
    final double tabLeft = tabOffsets[tabIndex];
    final double tabRight = tabOffsets[tabIndex + 1];
    final double tabTop = tabBarSize.height - _kTabIndicatorHeight;
    return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, _kTabIndicatorHeight);
263 264
  }

Hans Muller's avatar
Hans Muller committed
265 266 267 268
  @override
  void paint(Canvas canvas, Size size) {
    if (controller.indexIsChanging) {
      final Rect targetRect = indicatorRect(size, controller.index);
269
      currentRect = Rect.lerp(targetRect, currentRect ?? targetRect, _indexChangeProgress(controller));
Hans Muller's avatar
Hans Muller committed
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
    } 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)
        currentRect = left ?? middle;
      else if (value == index + 1.0)
        currentRect = right ?? middle;
      else if (value == index)
         currentRect = middle;
      else if (value < index)
        currentRect = left == null ? middle : Rect.lerp(middle, left, index - value);
      else
        currentRect = right == null ? middle : Rect.lerp(middle, right, value - index);
    }
    assert(currentRect != null);
    canvas.drawRect(currentRect, new Paint()..color = color);
291 292
  }

Hans Muller's avatar
Hans Muller committed
293 294 295 296 297
  static bool tabOffsetsNotEqual(List<double> a, List<double> b) {
    assert(a != null && b != null && a.length == b.length);
    for(int i = 0; i < a.length; i++) {
      if (a[i] != b[i])
        return true;
298
    }
Hans Muller's avatar
Hans Muller committed
299
    return false;
300 301
  }

302
  @override
Hans Muller's avatar
Hans Muller committed
303 304 305
  bool shouldRepaint(_IndicatorPainter old) {
    return controller != old.controller ||
      tabOffsets?.length != old.tabOffsets?.length ||
306 307
      tabOffsetsNotEqual(tabOffsets, old.tabOffsets) ||
      currentRect != old.currentRect;
Hans Muller's avatar
Hans Muller committed
308
  }
309 310
}

Hans Muller's avatar
Hans Muller committed
311 312
class _ChangeAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  _ChangeAnimation(this.controller);
313

Hans Muller's avatar
Hans Muller committed
314
  final TabController controller;
315 316

  @override
Hans Muller's avatar
Hans Muller committed
317 318 319 320
  Animation<double> get parent => controller.animation;

  @override
  double get value => _indexChangeProgress(controller);
321 322
}

323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
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);
  }
}

339
/// A material design widget that displays a horizontal row of tabs.
340
///
341 342
/// Typically created as part of an [AppBar] and in conjuction with a
/// [TabBarView].
343
///
344 345 346 347 348 349 350 351 352
/// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
///  * [TabBarView], which displays the contents that the tab bar is selecting
///    between.
Hans Muller's avatar
Hans Muller committed
353
class TabBar extends StatefulWidget implements AppBarBottomWidget {
354 355
  /// Creates a material design tab bar.
  ///
356
  /// The [tabs] argument must not be null and must have more than one widget.
357 358 359
  ///
  /// If a [TabController] is not provided, then there must be a
  /// [DefaultTabController] ancestor.
360
  TabBar({
361
    Key key,
Hans Muller's avatar
Hans Muller committed
362 363
    @required this.tabs,
    this.controller,
364 365
    this.isScrollable: false,
    this.indicatorColor,
Hans Muller's avatar
Hans Muller committed
366
    this.labelColor,
367
    this.labelStyle,
368
    this.unselectedLabelColor,
369
    this.unselectedLabelStyle,
Hans Muller's avatar
Hans Muller committed
370 371 372
  }) : super(key: key) {
    assert(tabs != null && tabs.length > 1);
    assert(isScrollable != null);
373
  }
374

Hans Muller's avatar
Hans Muller committed
375 376 377 378
  /// Typically a list of [Tab] widgets.
  final List<Widget> tabs;

  /// This widget's selection and animation state.
379
  ///
Hans Muller's avatar
Hans Muller committed
380 381 382
  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  /// will be used.
  final TabController controller;
383 384 385 386 387 388

  /// 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.
389
  final bool isScrollable;
390

391 392 393 394
  /// 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;

395 396 397 398 399 400 401
  /// 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.
402 403
  final Color labelColor;

404 405 406 407 408 409
  /// 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;

410 411 412 413 414 415 416 417 418 419 420 421 422 423
  /// 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;

424 425
  @override
  double get bottomHeight {
Hans Muller's avatar
Hans Muller committed
426 427 428 429 430 431
    for (Widget widget in tabs) {
      if (widget is Tab) {
        final Tab tab = widget;
        if (tab.text != null && tab.icon != null)
          return _kTextAndIconTabHeight + _kTabIndicatorHeight;
      }
432 433 434 435
    }
    return _kTabHeight + _kTabIndicatorHeight;
  }

436
  @override
Hans Muller's avatar
Hans Muller committed
437
  _TabBarState createState() => new _TabBarState();
438
}
439

Hans Muller's avatar
Hans Muller committed
440
class _TabBarState extends State<TabBar> {
441
  final ScrollController _scrollController = new ScrollController();
Hans Muller's avatar
Hans Muller committed
442

Hans Muller's avatar
Hans Muller committed
443 444 445 446 447
  TabController _controller;
  _IndicatorPainter _indicatorPainter;
  int _currentIndex;

  void _updateTabController() {
448
    final TabController newController = config.controller ?? DefaultTabController.of(context);
449 450 451 452 453 454 455 456 457 458 459 460
    assert(() {
      if (newController == null) {
        throw new FlutterError(
          'No TabController for ${config.runtimeType}.\n'
          'When creating a ${config.runtimeType}, you must either provide an explicit '
          'TabController using the "controller" property, or you must ensure that there '
          'is a DefaultTabController above the ${config.runtimeType}.\n'
          'In this case, there was neither an explicit controller nor a default controller.'
        );
      }
      return true;
    });
Hans Muller's avatar
Hans Muller committed
461
    if (newController == _controller)
462
      return;
Hans Muller's avatar
Hans Muller committed
463

464 465 466 467
    if (_controller != null) {
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
      _controller.removeListener(_handleTabControllerTick);
    }
Hans Muller's avatar
Hans Muller committed
468 469
    _controller = newController;
    if (_controller != null) {
470 471
      _controller.animation.addListener(_handleTabControllerAnimationTick);
      _controller.addListener(_handleTabControllerTick);
Hans Muller's avatar
Hans Muller committed
472 473 474
      _currentIndex = _controller.index;
      final List<double> offsets = _indicatorPainter?.tabOffsets;
      _indicatorPainter = new _IndicatorPainter(_controller)..tabOffsets = offsets;
475
    }
476 477
  }

478
  @override
479 480
  void didChangeDependencies() {
    super.didChangeDependencies();
Hans Muller's avatar
Hans Muller committed
481
    _updateTabController();
482 483
  }

484
  @override
Hans Muller's avatar
Hans Muller committed
485 486 487 488
  void didUpdateConfig(TabBar oldConfig) {
    super.didUpdateConfig(oldConfig);
    if (config.controller != oldConfig.controller)
      _updateTabController();
489 490
  }

491
  @override
Hans Muller's avatar
Hans Muller committed
492
  void dispose() {
493
    if (_controller != null) {
494
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
495 496
      _controller.removeListener(_handleTabControllerTick);
    }
Hans Muller's avatar
Hans Muller committed
497 498
    // We don't own the _controller Animation, so it's not disposed here.
    super.dispose();
499
  }
500

Hans Muller's avatar
Hans Muller committed
501 502 503
  // 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;
504

505
  double _tabCenteredScrollOffset(int tabIndex) {
Hans Muller's avatar
Hans Muller committed
506 507
    final List<double> tabOffsets = _indicatorPainter.tabOffsets;
    assert(tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex);
508

509
    final ScrollPosition position = _scrollController.position;
Hans Muller's avatar
Hans Muller committed
510
    final double tabCenter = (tabOffsets[tabIndex] + tabOffsets[tabIndex + 1]) / 2.0;
511 512
    return (tabCenter - position.viewportDimension / 2.0)
      .clamp(position.minScrollExtent, position.maxScrollExtent);
513 514
  }

Hans Muller's avatar
Hans Muller committed
515
  void _scrollToCurrentIndex() {
516 517
    final double offset = _tabCenteredScrollOffset(_currentIndex);
    _scrollController.animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
Hans Muller's avatar
Hans Muller committed
518 519 520
  }

  void _scrollToControllerValue() {
521 522 523
    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
524 525 526 527 528 529 530 531 532 533 534 535 536 537

    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);
538

539
    _scrollController.jumpTo(offset);
540
  }
541

542
  void _handleTabControllerAnimationTick() {
Hans Muller's avatar
Hans Muller committed
543
    assert(mounted);
544 545 546
    if (!_controller.indexIsChanging && config.isScrollable) {
      // Sync the TabBar's scroll position with the TabBarView's PageView.
      _currentIndex = _controller.index;
Hans Muller's avatar
Hans Muller committed
547 548
      _scrollToControllerValue();
    }
549 550
  }

551 552 553 554 555 556 557
  void _handleTabControllerTick() {
    setState(() {
      // Rebuild the tabs after a (potentially animated) index change
      // has completed.
    });
  }

Hans Muller's avatar
Hans Muller committed
558 559
  void _saveTabOffsets(List<double> tabOffsets) {
    _indicatorPainter?.tabOffsets = tabOffsets;
560 561
  }

Hans Muller's avatar
Hans Muller committed
562 563 564
  void _handleTap(int index) {
    assert(index >= 0 && index < config.tabs.length);
    _controller.animateTo(index);
Hans Muller's avatar
Hans Muller committed
565 566
  }

567 568 569 570 571 572 573 574 575 576 577 578
  Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) {
    return new _TabStyle(
      animation: animation,
      selected: selected,
      labelColor: config.labelColor,
      unselectedLabelColor: config.unselectedLabelColor,
      labelStyle: config.labelStyle,
      unselectedLabelStyle: config.unselectedLabelStyle,
      child: child,
    );
  }

Hans Muller's avatar
Hans Muller committed
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
  @override
  Widget build(BuildContext context) {
    final List<Widget> wrappedTabs = new List<Widget>.from(config.tabs, growable: false);

    // 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) {
      _indicatorPainter.color = config.indicatorColor ?? Theme.of(context).indicatorColor;
      if (_indicatorPainter.color == Material.of(context).color) {
        // 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.
        _indicatorPainter.color = Colors.white;
597
      }
598

Hans Muller's avatar
Hans Muller committed
599 600 601 602 603
      if (_controller.index != _currentIndex) {
        _currentIndex = _controller.index;
        if (config.isScrollable)
          _scrollToCurrentIndex();
      }
Hans Muller's avatar
Hans Muller committed
604

Hans Muller's avatar
Hans Muller committed
605
      final int previousIndex = _controller.previousIndex;
606

Hans Muller's avatar
Hans Muller committed
607
      if (_controller.indexIsChanging) {
608
        // The user tapped on a tab, the tab controller's animation is running.
Hans Muller's avatar
Hans Muller committed
609
        assert(_currentIndex != previousIndex);
610 611 612
        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
613
      } else {
614 615 616 617 618 619 620 621 622 623 624 625 626 627
        // 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);
        }
        if (_currentIndex < config.tabs.length - 1) {
          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
628
      }
Hans Muller's avatar
Hans Muller committed
629 630
    }

Hans Muller's avatar
Hans Muller committed
631 632 633 634 635 636 637 638 639
    // 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.
    for (int index = 0; index < config.tabs.length; index++) {
      wrappedTabs[index] = new InkWell(
        onTap: () { _handleTap(index); },
        child: wrappedTabs[index],
      );
      if (!config.isScrollable)
640
        wrappedTabs[index] = new Expanded(child: wrappedTabs[index]);
Hans Muller's avatar
Hans Muller committed
641 642 643 644 645 646 647
    }

    Widget tabBar = new CustomPaint(
      painter: _indicatorPainter,
      child: new Padding(
        padding: const EdgeInsets.only(bottom: _kTabIndicatorHeight),
        child: new _TabStyle(
648
          animation: kAlwaysDismissedAnimation,
Hans Muller's avatar
Hans Muller committed
649 650
          selected: false,
          labelColor: config.labelColor,
651
          unselectedLabelColor: config.unselectedLabelColor,
652 653
          labelStyle: config.labelStyle,
          unselectedLabelStyle: config.unselectedLabelStyle,
Hans Muller's avatar
Hans Muller committed
654 655 656 657 658 659
          child: new _TabLabelBar(
            onPerformLayout: _saveTabOffsets,
            children:  wrappedTabs,
          ),
        ),
      ),
660
    );
Hans Muller's avatar
Hans Muller committed
661

662
    if (config.isScrollable) {
663
      tabBar = new SingleChildScrollView(
Hans Muller's avatar
Hans Muller committed
664
        scrollDirection: Axis.horizontal,
665 666
        controller: _scrollController,
        child: tabBar,
667 668 669
      );
    }

Hans Muller's avatar
Hans Muller committed
670
    return tabBar;
671 672 673
  }
}

674
/// A page view that displays the widget which corresponds to the currently
Hans Muller's avatar
Hans Muller committed
675 676 677 678 679
/// 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 {
680
  /// Creates a page view with one child per tab.
Hans Muller's avatar
Hans Muller committed
681 682 683 684 685 686 687 688
  ///
  /// The length of [children] must be the same as the [controller]'s length.
  TabBarView({
    Key key,
    @required this.children,
    this.controller,
  }) : super(key: key) {
    assert(children != null && children.length > 1);
689
  }
690

Hans Muller's avatar
Hans Muller committed
691 692 693 694 695 696 697 698 699
  /// 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;

700
  @override
Hans Muller's avatar
Hans Muller committed
701 702
  _TabBarViewState createState() => new _TabBarViewState();
}
703

704
final PageScrollPhysics _kTabBarViewPhysics = const PageScrollPhysics().applyTo(const ClampingScrollPhysics());
705

706
class _TabBarViewState extends State<TabBarView> {
Hans Muller's avatar
Hans Muller committed
707
  TabController _controller;
708
  PageController _pageController;
Hans Muller's avatar
Hans Muller committed
709 710 711 712 713
  List<Widget> _children;
  int _currentIndex;
  int _warpUnderwayCount = 0;

  void _updateTabController() {
714
    final TabController newController = config.controller ?? DefaultTabController.of(context);
715 716 717 718 719 720 721 722 723 724 725 726
    assert(() {
      if (newController == null) {
        throw new FlutterError(
          'No TabController for ${config.runtimeType}.\n'
          'When creating a ${config.runtimeType}, you must either provide an explicit '
          'TabController using the "controller" property, or you must ensure that there '
          'is a DefaultTabController above the ${config.runtimeType}.\n'
          'In this case, there was neither an explicit controller nor a default controller.'
        );
      }
      return true;
    });
Hans Muller's avatar
Hans Muller committed
727 728 729 730
    if (newController == _controller)
      return;

    if (_controller != null)
731
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
Hans Muller's avatar
Hans Muller committed
732 733
    _controller = newController;
    if (_controller != null)
734
      _controller.animation.addListener(_handleTabControllerAnimationTick);
735 736
  }

Hans Muller's avatar
Hans Muller committed
737 738 739 740
  @override
  void initState() {
    super.initState();
    _children = config.children;
Hans Muller's avatar
Hans Muller committed
741 742
  }

Hans Muller's avatar
Hans Muller committed
743
  @override
744 745
  void didChangeDependencies() {
    super.didChangeDependencies();
Hans Muller's avatar
Hans Muller committed
746 747
    _updateTabController();
    _currentIndex = _controller?.index;
748
    _pageController = new PageController(initialPage: _currentIndex ?? 0);
749 750
  }

751
  @override
Hans Muller's avatar
Hans Muller committed
752 753 754 755 756 757
  void didUpdateConfig(TabBarView oldConfig) {
    super.didUpdateConfig(oldConfig);
    if (config.controller != oldConfig.controller)
      _updateTabController();
    if (config.children != oldConfig.children && _warpUnderwayCount == 0)
      _children = config.children;
758 759
  }

760
  @override
Hans Muller's avatar
Hans Muller committed
761 762
  void dispose() {
    if (_controller != null)
763
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
Hans Muller's avatar
Hans Muller committed
764 765 766
    // We don't own the _controller Animation, so it's not disposed here.
    super.dispose();
  }
Hans Muller's avatar
Hans Muller committed
767

768 769
  void _handleTabControllerAnimationTick() {
    if (_warpUnderwayCount > 0 || !_controller.indexIsChanging)
Hans Muller's avatar
Hans Muller committed
770
      return; // This widget is driving the controller's animation.
Hans Muller's avatar
Hans Muller committed
771

Hans Muller's avatar
Hans Muller committed
772 773 774
    if (_controller.index != _currentIndex) {
      _currentIndex = _controller.index;
      _warpToCurrentIndex();
775
    }
Hans Muller's avatar
Hans Muller committed
776
  }
777

Hans Muller's avatar
Hans Muller committed
778 779 780
  Future<Null> _warpToCurrentIndex() async {
    if (!mounted)
      return new Future<Null>.value();
781

782
    if (_pageController.page == _currentIndex.toDouble())
Hans Muller's avatar
Hans Muller committed
783
      return new Future<Null>.value();
784

Hans Muller's avatar
Hans Muller committed
785 786
    final int previousIndex = _controller.previousIndex;
    if ((_currentIndex - previousIndex).abs() == 1)
787
      return _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
Hans Muller's avatar
Hans Muller committed
788 789

    assert((_currentIndex - previousIndex).abs() > 1);
790
    int initialPage;
Hans Muller's avatar
Hans Muller committed
791 792 793 794 795
    setState(() {
      _warpUnderwayCount += 1;
      _children = new List<Widget>.from(config.children, growable: false);
      if (_currentIndex > previousIndex) {
        _children[_currentIndex - 1] = _children[previousIndex];
796
        initialPage = _currentIndex - 1;
Hans Muller's avatar
Hans Muller committed
797 798
      } else {
        _children[_currentIndex + 1] = _children[previousIndex];
799
        initialPage = _currentIndex + 1;
Hans Muller's avatar
Hans Muller committed
800 801
      }
    });
802

803
    _pageController.jumpToPage(initialPage);
Hans Muller's avatar
Hans Muller committed
804

805
    await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
Hans Muller's avatar
Hans Muller committed
806 807
    if (!mounted)
      return new Future<Null>.value();
Hans Muller's avatar
Hans Muller committed
808

Hans Muller's avatar
Hans Muller committed
809 810 811 812
    setState(() {
      _warpUnderwayCount -= 1;
      _children = config.children;
    });
Hans Muller's avatar
Hans Muller committed
813 814
  }

815
  // Called when the PageView scrolls
Adam Barth's avatar
Adam Barth committed
816
  bool _handleScrollNotification(ScrollNotification notification) {
Hans Muller's avatar
Hans Muller committed
817 818
    if (_warpUnderwayCount > 0)
      return false;
Hans Muller's avatar
Hans Muller committed
819

820
    if (notification.depth != 0)
Hans Muller's avatar
Hans Muller committed
821
      return false;
Hans Muller's avatar
Hans Muller committed
822

823
    _warpUnderwayCount += 1;
824
    if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) {
825 826 827 828
      if ((_pageController.page - _controller.index).abs() > 1.0) {
        _controller.index = _pageController.page.floor();
        _currentIndex=_controller.index;
      }
829
      _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0);
830 831 832
    } else if (notification is ScrollEndNotification) {
      _controller.index = _pageController.page.floor();
      _currentIndex = _controller.index;
Hans Muller's avatar
Hans Muller committed
833
    }
834
    _warpUnderwayCount -= 1;
Hans Muller's avatar
Hans Muller committed
835 836

    return false;
Hans Muller's avatar
Hans Muller committed
837 838
  }

839
  @override
Hans Muller's avatar
Hans Muller committed
840
  Widget build(BuildContext context) {
Adam Barth's avatar
Adam Barth committed
841
    return new NotificationListener<ScrollNotification>(
Hans Muller's avatar
Hans Muller committed
842
      onNotification: _handleScrollNotification,
843 844 845
      child: new PageView(
        controller: _pageController,
        physics: _kTabBarViewPhysics,
Hans Muller's avatar
Hans Muller committed
846 847
        children: _children,
      ),
848
    );
Hans Muller's avatar
Hans Muller committed
849
  }
850
}
Hixie's avatar
Hixie committed
851

Hans Muller's avatar
Hans Muller committed
852 853
/// Displays a row of small circular indicators, one per tab. The selected
/// tab's indicator is highlighted. Often used in conjuction with a [TabBarView].
854
///
Hans Muller's avatar
Hans Muller committed
855 856 857 858 859
/// 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.
  TabPageSelector({ Key key, this.controller }) : super(key: key);
Hixie's avatar
Hixie committed
860

Hans Muller's avatar
Hans Muller committed
861 862 863 864 865 866 867 868 869 870 871 872
  /// This widget's selection and animation state.
  ///
  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  /// will be used.
  final TabController controller;

  Widget _buildTabIndicator(
    int tabIndex,
    TabController tabController,
    ColorTween selectedColor,
    ColorTween previousColor,
  ) {
Hixie's avatar
Hixie committed
873
    Color background;
Hans Muller's avatar
Hans Muller committed
874
    if (tabController.indexIsChanging) {
Hixie's avatar
Hixie committed
875
      // The selection's animation is animating from previousValue to value.
Hans Muller's avatar
Hans Muller committed
876 877 878 879
      if (tabController.index == tabIndex)
        background = selectedColor.lerp(_indexChangeProgress(tabController));
      else if (tabController.previousIndex == tabIndex)
        background = previousColor.lerp(_indexChangeProgress(tabController));
Hixie's avatar
Hixie committed
880 881 882
      else
        background = selectedColor.begin;
    } else {
Hans Muller's avatar
Hans Muller committed
883
      background = tabController.index == tabIndex ? selectedColor.end : selectedColor.begin;
Hixie's avatar
Hixie committed
884 885 886 887
    }
    return new Container(
      width: 12.0,
      height: 12.0,
888
      margin: const EdgeInsets.all(4.0),
Hixie's avatar
Hixie committed
889 890 891 892 893 894 895 896
      decoration: new BoxDecoration(
        backgroundColor: background,
        border: new Border.all(color: selectedColor.end),
        shape: BoxShape.circle
      )
    );
  }

897
  @override
Hixie's avatar
Hixie committed
898
  Widget build(BuildContext context) {
899
    final Color color = Theme.of(context).accentColor;
Hixie's avatar
Hixie committed
900 901
    final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color);
    final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent);
Hans Muller's avatar
Hans Muller committed
902
    final TabController tabController = controller ?? DefaultTabController.of(context);
903 904 905 906 907 908 909 910 911 912 913 914
    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
915 916 917 918
    final Animation<double> animation = new CurvedAnimation(
      parent: tabController.animation,
      curve: Curves.fastOutSlowIn,
    );
Hixie's avatar
Hixie committed
919 920 921 922
    return new AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget child) {
        return new Semantics(
923
          label: 'Page ${tabController.index + 1} of ${tabController.length}',
Hixie's avatar
Hixie committed
924
          child: new Row(
Hans Muller's avatar
Hans Muller committed
925
            mainAxisSize: MainAxisSize.min,
926 927
            children: new List<Widget>.generate(tabController.length, (int tabIndex) {
              return _buildTabIndicator(tabIndex, tabController, selectedColor, previousColor);
Hans Muller's avatar
Hans Muller committed
928 929
            }).toList(),
          ),
Hixie's avatar
Hixie committed
930 931 932 933 934
        );
      }
    );
  }
}