tabs.dart 51.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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 9
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
10
import 'package:flutter/gestures.dart' show DragStartBehavior;
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';
16
import 'ink_well.dart';
17
import 'material.dart';
18
import 'material_localizations.dart';
19
import 'tab_bar_theme.dart';
Hans Muller's avatar
Hans Muller committed
20
import 'tab_controller.dart';
21
import 'tab_indicator.dart';
22
import 'theme.dart';
23 24 25

const double _kTabHeight = 46.0;
const double _kTextAndIconTabHeight = 72.0;
26 27 28 29

/// Defines how the bounds of the selected tab indicator are computed.
///
/// See also:
30
///
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
///  * [TabBar], which displays a row of tabs.
///  * [TabBarView], which displays a widget for the currently selected tab.
///  * [TabBar.indicator], which defines the appearance of the selected tab
///    indicator relative to the tab's bounds.
enum TabBarIndicatorSize {
  /// The tab indicator's bounds are as wide as the space occupied by the tab
  /// in the tab bar: from the right edge of the previous tab to the left edge
  /// of the next tab.
  tab,

  /// The tab's bounds are only as wide as the (centered) tab widget itself.
  ///
  /// This value is used to align the tab's label, typically a [Tab]
  /// widget's text or icon, with the selected tab indicator.
  label,
}
47

48 49 50 51
/// A material design [TabBar] tab.
///
/// If both [icon] and [text] are provided, the text is displayed below
/// the icon.
Hans Muller's avatar
Hans Muller committed
52 53 54 55 56 57
///
/// 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].
58
///  * <https://material.io/design/components/tabs.html>
Hans Muller's avatar
Hans Muller committed
59
class Tab extends StatelessWidget {
60 61 62
  /// Creates a material design [TabBar] tab.
  ///
  /// At least one of [text], [icon], and [child] must be non-null. The [text]
63 64 65
  /// and [child] arguments must not be used at the same time. The
  /// [iconMargin] is only useful when [icon] and either one of [text] or
  /// [child] is non-null.
66
  const Tab({
67
    Key key,
Hans Muller's avatar
Hans Muller committed
68 69
    this.text,
    this.icon,
70
    this.iconMargin = const EdgeInsets.only(bottom: 10.0),
71 72
    this.child,
  }) : assert(text != null || child != null || icon != null),
73
       assert(text == null || child == null),
74
       super(key: key);
75

Hans Muller's avatar
Hans Muller committed
76
  /// The text to display as the tab's label.
77 78
  ///
  /// Must not be used in combination with [child].
79
  final String text;
80

81 82 83 84 85 86 87
  /// The widget to be used as the tab's label.
  ///
  /// Usually a [Text] widget, possibly wrapped in a [Semantics] widget.
  ///
  /// Must not be used in combination with [text].
  final Widget child;

Hans Muller's avatar
Hans Muller committed
88
  /// An icon to display as the tab's label.
Ian Hickson's avatar
Ian Hickson committed
89
  final Widget icon;
90

91 92 93 94 95 96
  /// The margin added around the tab's icon.
  ///
  /// Only useful when used in combination with [icon], and either one of
  /// [text] or [child] is non-null.
  final EdgeInsetsGeometry iconMargin;

97
  Widget _buildLabelText() {
98
    return child ?? Text(text, softWrap: false, overflow: TextOverflow.fade);
99 100
  }

101
  @override
102
  Widget build(BuildContext context) {
103
    assert(debugCheckHasMaterial(context));
Hans Muller's avatar
Hans Muller committed
104 105 106 107 108 109

    double height;
    Widget label;
    if (icon == null) {
      height = _kTabHeight;
      label = _buildLabelText();
Ian Hickson's avatar
Ian Hickson committed
110
    } else if (text == null && child == null) {
Hans Muller's avatar
Hans Muller committed
111 112
      height = _kTabHeight;
      label = icon;
113
    } else {
Hans Muller's avatar
Hans Muller committed
114
      height = _kTextAndIconTabHeight;
115
      label = Column(
116 117
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
118
        children: <Widget>[
119
          Container(
Hans Muller's avatar
Hans Muller committed
120
            child: icon,
121
            margin: iconMargin,
122
          ),
123 124
          _buildLabelText(),
        ],
125 126 127
      );
    }

128
    return SizedBox(
Hans Muller's avatar
Hans Muller committed
129
      height: height,
130
      child: Center(
131 132 133
        child: label,
        widthFactor: 1.0,
      ),
134
    );
135
  }
Hixie's avatar
Hixie committed
136

137
  @override
138 139
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
140 141
    properties.add(StringProperty('text', text, defaultValue: null));
    properties.add(DiagnosticsProperty<Widget>('icon', icon, defaultValue: null));
Hixie's avatar
Hixie committed
142
  }
143 144
}

Hans Muller's avatar
Hans Muller committed
145
class _TabStyle extends AnimatedWidget {
146
  const _TabStyle({
Hans Muller's avatar
Hans Muller committed
147 148 149 150
    Key key,
    Animation<double> animation,
    this.selected,
    this.labelColor,
151
    this.unselectedLabelColor,
152 153
    this.labelStyle,
    this.unselectedLabelStyle,
154
    @required this.child,
155
  }) : super(key: key, listenable: animation);
156

157 158
  final TextStyle labelStyle;
  final TextStyle unselectedLabelStyle;
Hans Muller's avatar
Hans Muller committed
159 160
  final bool selected;
  final Color labelColor;
161
  final Color unselectedLabelColor;
Hans Muller's avatar
Hans Muller committed
162
  final Widget child;
163

164
  @override
Hans Muller's avatar
Hans Muller committed
165 166
  Widget build(BuildContext context) {
    final ThemeData themeData = Theme.of(context);
167
    final TabBarTheme tabBarTheme = TabBarTheme.of(context);
168
    final Animation<double> animation = listenable as Animation<double>;
169

170 171 172 173
    // To enable TextStyle.lerp(style1, style2, value), both styles must have
    // the same value of inherit. Force that to be inherit=true here.
    final TextStyle defaultStyle = (labelStyle
      ?? tabBarTheme.labelStyle
174
      ?? themeData.primaryTextTheme.bodyText1
175 176
    ).copyWith(inherit: true);
    final TextStyle defaultUnselectedStyle = (unselectedLabelStyle
177 178
      ?? tabBarTheme.unselectedLabelStyle
      ?? labelStyle
179
      ?? themeData.primaryTextTheme.bodyText1
180
    ).copyWith(inherit: true);
181
    final TextStyle textStyle = selected
182 183
      ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)
      : TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value);
184

185 186
    final Color selectedColor = labelColor
       ?? tabBarTheme.labelColor
187
       ?? themeData.primaryTextTheme.bodyText1.color;
188 189 190
    final Color unselectedColor = unselectedLabelColor
      ?? tabBarTheme.unselectedLabelColor
      ?? selectedColor.withAlpha(0xB2); // 70% alpha
Hans Muller's avatar
Hans Muller committed
191
    final Color color = selected
192 193
      ? Color.lerp(selectedColor, unselectedColor, animation.value)
      : Color.lerp(unselectedColor, selectedColor, animation.value);
Hans Muller's avatar
Hans Muller committed
194

195
    return DefaultTextStyle(
Hans Muller's avatar
Hans Muller committed
196
      style: textStyle.copyWith(color: color),
197
      child: IconTheme.merge(
198
        data: IconThemeData(
Hans Muller's avatar
Hans Muller committed
199 200 201 202 203
          size: 24.0,
          color: color,
        ),
        child: child,
      ),
204 205 206 207
    );
  }
}

208
typedef _LayoutCallback = void Function(List<double> xOffsets, TextDirection textDirection, double width);
Ian Hickson's avatar
Ian Hickson committed
209

Hans Muller's avatar
Hans Muller committed
210 211 212
class _TabLabelBarRenderer extends RenderFlex {
  _TabLabelBarRenderer({
    List<RenderBox> children,
Ian Hickson's avatar
Ian Hickson committed
213 214 215 216 217 218
    @required Axis direction,
    @required MainAxisSize mainAxisSize,
    @required MainAxisAlignment mainAxisAlignment,
    @required CrossAxisAlignment crossAxisAlignment,
    @required TextDirection textDirection,
    @required VerticalDirection verticalDirection,
219
    @required this.onPerformLayout,
220
  }) : assert(onPerformLayout != null),
Ian Hickson's avatar
Ian Hickson committed
221
       assert(textDirection != null),
222 223 224 225 226 227
       super(
         children: children,
         direction: direction,
         mainAxisSize: mainAxisSize,
         mainAxisAlignment: mainAxisAlignment,
         crossAxisAlignment: crossAxisAlignment,
228 229
         textDirection: textDirection,
         verticalDirection: verticalDirection,
230
       );
231

Ian Hickson's avatar
Ian Hickson committed
232
  _LayoutCallback onPerformLayout;
233 234

  @override
Hans Muller's avatar
Hans Muller committed
235 236
  void performLayout() {
    super.performLayout();
Ian Hickson's avatar
Ian Hickson committed
237 238 239 240
    // xOffsets will contain childCount+1 values, giving the offsets of the
    // leading edge of the first tab as the first value, of the leading edge of
    // the each subsequent tab as each subsequent value, and of the trailing
    // edge of the last tab as the last value.
Hans Muller's avatar
Hans Muller committed
241 242 243
    RenderBox child = firstChild;
    final List<double> xOffsets = <double>[];
    while (child != null) {
244
      final FlexParentData childParentData = child.parentData as FlexParentData;
Hans Muller's avatar
Hans Muller committed
245 246 247 248
      xOffsets.add(childParentData.offset.dx);
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
    }
Ian Hickson's avatar
Ian Hickson committed
249 250 251 252 253 254 255 256 257 258
    assert(textDirection != null);
    switch (textDirection) {
      case TextDirection.rtl:
        xOffsets.insert(0, size.width);
        break;
      case TextDirection.ltr:
        xOffsets.add(size.width);
        break;
    }
    onPerformLayout(xOffsets, textDirection, size.width);
259
  }
Hans Muller's avatar
Hans Muller committed
260 261
}

Hans Muller's avatar
Hans Muller committed
262 263 264 265 266 267
// 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,
268
    List<Widget> children = const <Widget>[],
Hans Muller's avatar
Hans Muller committed
269 270 271 272 273 274 275 276
    this.onPerformLayout,
  }) : super(
    key: key,
    children: children,
    direction: Axis.horizontal,
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.center,
Ian Hickson's avatar
Ian Hickson committed
277
    verticalDirection: VerticalDirection.down,
Hans Muller's avatar
Hans Muller committed
278 279
  );

Ian Hickson's avatar
Ian Hickson committed
280
  final _LayoutCallback onPerformLayout;
Hans Muller's avatar
Hans Muller committed
281 282 283

  @override
  RenderFlex createRenderObject(BuildContext context) {
284
    return _TabLabelBarRenderer(
Hans Muller's avatar
Hans Muller committed
285 286 287 288
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
289 290
      textDirection: getEffectiveTextDirection(context),
      verticalDirection: verticalDirection,
Hans Muller's avatar
Hans Muller committed
291
      onPerformLayout: onPerformLayout,
292
    );
293 294
  }

295
  @override
Hans Muller's avatar
Hans Muller committed
296 297 298
  void updateRenderObject(BuildContext context, _TabLabelBarRenderer renderObject) {
    super.updateRenderObject(context, renderObject);
    renderObject.onPerformLayout = onPerformLayout;
Hans Muller's avatar
Hans Muller committed
299
  }
Hans Muller's avatar
Hans Muller committed
300
}
Hans Muller's avatar
Hans Muller committed
301

Hans Muller's avatar
Hans Muller committed
302 303 304 305
double _indexChangeProgress(TabController controller) {
  final double controllerValue = controller.animation.value;
  final double previousIndex = controller.previousIndex.toDouble();
  final double currentIndex = controller.index.toDouble();
306 307 308 309

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

  // 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
314
}
315

Hans Muller's avatar
Hans Muller committed
316
class _IndicatorPainter extends CustomPainter {
317
  _IndicatorPainter({
Ian Hickson's avatar
Ian Hickson committed
318
    @required this.controller,
319 320 321
    @required this.indicator,
    @required this.indicatorSize,
    @required this.tabKeys,
Ian Hickson's avatar
Ian Hickson committed
322 323
    _IndicatorPainter old,
  }) : assert(controller != null),
324
       assert(indicator != null),
Ian Hickson's avatar
Ian Hickson committed
325 326 327 328
       super(repaint: controller.animation) {
    if (old != null)
      saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
  }
329

330
  final TabController controller;
331 332 333
  final Decoration indicator;
  final TabBarIndicatorSize indicatorSize;
  final List<GlobalKey> tabKeys;
Ian Hickson's avatar
Ian Hickson committed
334 335 336

  List<double> _currentTabOffsets;
  TextDirection _currentTextDirection;
337
  Rect _currentRect;
338 339 340 341 342 343 344 345 346
  BoxPainter _painter;
  bool _needsPaint = false;
  void markNeedsPaint() {
    _needsPaint = true;
  }

  void dispose() {
    _painter?.dispose();
  }
347

Ian Hickson's avatar
Ian Hickson committed
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
  void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) {
    _currentTabOffsets = tabOffsets;
    _currentTextDirection = textDirection;
  }

  // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
  // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
  int get maxTabIndex => _currentTabOffsets.length - 2;

  double centerOf(int tabIndex) {
    assert(_currentTabOffsets != null);
    assert(_currentTabOffsets.isNotEmpty);
    assert(tabIndex >= 0);
    assert(tabIndex <= maxTabIndex);
    return (_currentTabOffsets[tabIndex] + _currentTabOffsets[tabIndex + 1]) / 2.0;
  }
364

Hans Muller's avatar
Hans Muller committed
365
  Rect indicatorRect(Size tabBarSize, int tabIndex) {
Ian Hickson's avatar
Ian Hickson committed
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
    assert(_currentTabOffsets != null);
    assert(_currentTextDirection != null);
    assert(_currentTabOffsets.isNotEmpty);
    assert(tabIndex >= 0);
    assert(tabIndex <= maxTabIndex);
    double tabLeft, tabRight;
    switch (_currentTextDirection) {
      case TextDirection.rtl:
        tabLeft = _currentTabOffsets[tabIndex + 1];
        tabRight = _currentTabOffsets[tabIndex];
        break;
      case TextDirection.ltr:
        tabLeft = _currentTabOffsets[tabIndex];
        tabRight = _currentTabOffsets[tabIndex + 1];
        break;
    }
382 383 384 385 386 387 388 389

    if (indicatorSize == TabBarIndicatorSize.label) {
      final double tabWidth = tabKeys[tabIndex].currentContext.size.width;
      final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0;
      tabLeft += delta;
      tabRight -= delta;
    }

390
    return Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height);
391 392
  }

Hans Muller's avatar
Hans Muller committed
393 394
  @override
  void paint(Canvas canvas, Size size) {
395 396 397
    _needsPaint = false;
    _painter ??= indicator.createBoxPainter(markNeedsPaint);

Hans Muller's avatar
Hans Muller committed
398
    if (controller.indexIsChanging) {
Ian Hickson's avatar
Ian Hickson committed
399
      // The user tapped on a tab, the tab controller's animation is running.
Hans Muller's avatar
Hans Muller committed
400
      final Rect targetRect = indicatorRect(size, controller.index);
401
      _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller));
Hans Muller's avatar
Hans Muller committed
402
    } else {
Ian Hickson's avatar
Ian Hickson committed
403
      // The user is dragging the TabBarView's PageView left or right.
Hans Muller's avatar
Hans Muller committed
404
      final int currentIndex = controller.index;
Ian Hickson's avatar
Ian Hickson committed
405
      final Rect previous = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
Hans Muller's avatar
Hans Muller committed
406
      final Rect middle = indicatorRect(size, currentIndex);
Ian Hickson's avatar
Ian Hickson committed
407
      final Rect next = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null;
Hans Muller's avatar
Hans Muller committed
408 409 410
      final double index = controller.index.toDouble();
      final double value = controller.animation.value;
      if (value == index - 1.0)
Ian Hickson's avatar
Ian Hickson committed
411
        _currentRect = previous ?? middle;
Hans Muller's avatar
Hans Muller committed
412
      else if (value == index + 1.0)
Ian Hickson's avatar
Ian Hickson committed
413
        _currentRect = next ?? middle;
Hans Muller's avatar
Hans Muller committed
414
      else if (value == index)
415
        _currentRect = middle;
Hans Muller's avatar
Hans Muller committed
416
      else if (value < index)
Ian Hickson's avatar
Ian Hickson committed
417
        _currentRect = previous == null ? middle : Rect.lerp(middle, previous, index - value);
Hans Muller's avatar
Hans Muller committed
418
      else
Ian Hickson's avatar
Ian Hickson committed
419
        _currentRect = next == null ? middle : Rect.lerp(middle, next, value - index);
Hans Muller's avatar
Hans Muller committed
420
    }
421
    assert(_currentRect != null);
422

423
    final ImageConfiguration configuration = ImageConfiguration(
424 425 426 427
      size: _currentRect.size,
      textDirection: _currentTextDirection,
    );
    _painter.paint(canvas, _currentRect.topLeft, configuration);
428 429
  }

Ian Hickson's avatar
Ian Hickson committed
430
  static bool _tabOffsetsEqual(List<double> a, List<double> b) {
431 432 433
    // TODO(shihaohong): The following null check should be replaced when a fix
    // for https://github.com/flutter/flutter/issues/40014 is available.
    if (a == null || b == null || a.length != b.length)
Ian Hickson's avatar
Ian Hickson committed
434
      return false;
435
    for (int i = 0; i < a.length; i += 1) {
Hans Muller's avatar
Hans Muller committed
436
      if (a[i] != b[i])
Ian Hickson's avatar
Ian Hickson committed
437
        return false;
438
    }
Ian Hickson's avatar
Ian Hickson committed
439
    return true;
440 441
  }

442
  @override
Hans Muller's avatar
Hans Muller committed
443
  bool shouldRepaint(_IndicatorPainter old) {
444 445 446 447
    return _needsPaint
        || controller != old.controller
        || indicator != old.indicator
        || tabKeys.length != old.tabKeys.length
Ian Hickson's avatar
Ian Hickson committed
448 449
        || (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets))
        || _currentTextDirection != old._currentTextDirection;
Hans Muller's avatar
Hans Muller committed
450
  }
451 452
}

Hans Muller's avatar
Hans Muller committed
453 454
class _ChangeAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  _ChangeAnimation(this.controller);
455

Hans Muller's avatar
Hans Muller committed
456
  final TabController controller;
457 458

  @override
Hans Muller's avatar
Hans Muller committed
459 460
  Animation<double> get parent => controller.animation;

461 462 463 464 465 466 467 468 469 470 471 472
  @override
  void removeStatusListener(AnimationStatusListener listener) {
    if (parent != null)
      super.removeStatusListener(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
    if (parent != null)
      super.removeListener(listener);
  }

Hans Muller's avatar
Hans Muller committed
473 474
  @override
  double get value => _indexChangeProgress(controller);
475 476
}

477 478 479 480 481 482 483 484 485
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;

486 487
  @override
  void removeStatusListener(AnimationStatusListener listener) {
488
    if (parent != null)
489 490 491 492 493
      super.removeStatusListener(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
494
    if (parent != null)
495 496 497
      super.removeListener(listener);
  }

498 499 500
  @override
  double get value {
    assert(!controller.indexIsChanging);
501
    return (controller.animation.value - index.toDouble()).abs().clamp(0.0, 1.0) as double;
502 503 504
  }
}

505
// This class, and TabBarScrollController, only exist to handle the case
506 507 508
// 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.
509
class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
510 511
  _TabBarScrollPosition({
    ScrollPhysics physics,
512
    ScrollContext context,
513 514 515 516
    ScrollPosition oldPosition,
    this.tabBar,
  }) : super(
    physics: physics,
517
    context: context,
518 519 520 521 522 523
    initialPixels: null,
    oldPosition: oldPosition,
  );

  final _TabBarState tabBar;

524 525
  bool _initialViewportDimensionWasZero;

526 527 528
  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    bool result = true;
529 530 531 532 533 534 535 536 537 538
    if (_initialViewportDimensionWasZero != true) {
      // If the viewport never had a non-zero dimension, we just want to jump
      // to the initial scroll position to avoid strange scrolling effects in
      // release mode: In release mode, the viewport temporarily may have a
      // dimension of zero before the actual dimension is calculated. In that
      // scenario, setting the actual dimension would cause a strange scroll
      // effect without this guard because the super call below would starts a
      // ballistic scroll activity.
      assert(viewportDimension != null);
      _initialViewportDimensionWasZero = viewportDimension != 0.0;
539 540 541 542 543 544 545
      correctPixels(tabBar._initialScrollOffset(viewportDimension, minScrollExtent, maxScrollExtent));
      result = false;
    }
    return super.applyContentDimensions(minScrollExtent, maxScrollExtent) && result;
  }
}

546
// This class, and TabBarScrollPosition, only exist to handle the case
547 548 549 550 551 552 553
// where a scrollable TabBar has a non-zero initialIndex.
class _TabBarScrollController extends ScrollController {
  _TabBarScrollController(this.tabBar);

  final _TabBarState tabBar;

  @override
554
  ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
555
    return _TabBarScrollPosition(
556
      physics: physics,
557
      context: context,
558 559 560 561 562 563
      oldPosition: oldPosition,
      tabBar: tabBar,
    );
  }
}

564
/// A material design widget that displays a horizontal row of tabs.
565
///
566
/// Typically created as the [AppBar.bottom] part of an [AppBar] and in
567
/// conjunction with a [TabBarView].
568
///
569 570
/// If a [TabController] is not provided, then a [DefaultTabController] ancestor
/// must be provided instead. The tab controller's [TabController.length] must
571 572
/// equal the length of the [tabs] list and the length of the
/// [TabBarView.children] list.
573 574 575
///
/// Requires one of its ancestors to be a [Material] widget.
///
576
/// Uses values from [TabBarTheme] if it is set in the current context.
577
///
578 579
/// To see a sample implementation, visit the [TabController] documentation.
///
580 581
/// See also:
///
582
///  * [TabBarView], which displays page views that correspond to each tab.
583
class TabBar extends StatefulWidget implements PreferredSizeWidget {
584 585
  /// Creates a material design tab bar.
  ///
586
  /// The [tabs] argument must not be null and its length must match the [controller]'s
587
  /// [TabController.length].
588 589 590
  ///
  /// If a [TabController] is not provided, then there must be a
  /// [DefaultTabController] ancestor.
591
  ///
592
  /// The [indicatorWeight] parameter defaults to 2, and must not be null.
593
  ///
594
  /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.
595 596 597
  ///
  /// If [indicator] is not null, then [indicatorWeight], [indicatorPadding], and
  /// [indicatorColor] are ignored.
598
  const TabBar({
599
    Key key,
Hans Muller's avatar
Hans Muller committed
600 601
    @required this.tabs,
    this.controller,
602
    this.isScrollable = false,
603
    this.indicatorColor,
604 605
    this.indicatorWeight = 2.0,
    this.indicatorPadding = EdgeInsets.zero,
606 607
    this.indicator,
    this.indicatorSize,
Hans Muller's avatar
Hans Muller committed
608
    this.labelColor,
609
    this.labelStyle,
610
    this.labelPadding,
611
    this.unselectedLabelColor,
612
    this.unselectedLabelStyle,
613
    this.dragStartBehavior = DragStartBehavior.start,
614
    this.mouseCursor,
615
    this.onTap,
616
  }) : assert(tabs != null),
617
       assert(isScrollable != null),
618
       assert(dragStartBehavior != null),
619 620
       assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)),
       assert(indicator != null || (indicatorPadding != null)),
621
       super(key: key);
622

623 624
  /// Typically a list of two or more [Tab] widgets.
  ///
625 626
  /// The length of this list must match the [controller]'s [TabController.length]
  /// and the length of the [TabBarView.children] list.
Hans Muller's avatar
Hans Muller committed
627 628 629
  final List<Widget> tabs;

  /// This widget's selection and animation state.
630
  ///
Hans Muller's avatar
Hans Muller committed
631 632 633
  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  /// will be used.
  final TabController controller;
634 635 636

  /// Whether this tab bar can be scrolled horizontally.
  ///
637
  /// If [isScrollable] is true, then each tab is as wide as needed for its label
638 639
  /// and the entire [TabBar] is scrollable. Otherwise each tab gets an equal
  /// share of the available space.
640
  final bool isScrollable;
641

642 643 644 645
  /// 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.
646 647
  ///
  /// If [indicator] is specified, this property is ignored.
648 649
  final Color indicatorColor;

650
  /// The thickness of the line that appears below the selected tab.
651
  ///
652 653
  /// The value of this parameter must be greater than zero and its default
  /// value is 2.0.
654 655
  ///
  /// If [indicator] is specified, this property is ignored.
656 657 658
  final double indicatorWeight;

  /// The horizontal padding for the line that appears below the selected tab.
659
  ///
660
  /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align
661 662 663
  /// the indicator with the tab's text for [Tab] widgets and all but the
  /// shortest [Tab.text] values.
  ///
664 665 666
  /// The [EdgeInsets.top] and [EdgeInsets.bottom] values of the
  /// [indicatorPadding] are ignored.
  ///
667
  /// The default value of [indicatorPadding] is [EdgeInsets.zero].
668 669
  ///
  /// If [indicator] is specified, this property is ignored.
Ian Hickson's avatar
Ian Hickson committed
670
  final EdgeInsetsGeometry indicatorPadding;
671

672 673 674 675 676 677 678 679 680 681 682
  /// Defines the appearance of the selected tab indicator.
  ///
  /// If [indicator] is specified, the [indicatorColor], [indicatorWeight],
  /// and [indicatorPadding] properties are ignored.
  ///
  /// The default, underline-style, selected tab indicator can be defined with
  /// [UnderlineTabIndicator].
  ///
  /// The indicator's size is based on the tab's bounds. If [indicatorSize]
  /// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space
  /// occupied by the tab in the tab bar. If [indicatorSize] is
683
  /// [TabBarIndicatorSize.label], then the tab's bounds are only as wide as
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
  /// the tab widget itself.
  final Decoration indicator;

  /// Defines how the selected tab indicator's size is computed.
  ///
  /// The size of the selected tab indicator is defined relative to the
  /// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab]
  /// (the default) or relative to the bounds of the tab's widget if
  /// [indicatorSize] is [TabBarIndicatorSize.label].
  ///
  /// The selected tab's location appearance can be refined further with
  /// the [indicatorColor], [indicatorWeight], [indicatorPadding], and
  /// [indicator] properties.
  final TabBarIndicatorSize indicatorSize;

699 700 701 702 703
  /// The color of selected tab labels.
  ///
  /// Unselected tab labels are rendered with the same color rendered at 70%
  /// opacity unless [unselectedLabelColor] is non-null.
  ///
704
  /// If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s
705
  /// bodyText1 text color is used.
706 707
  final Color labelColor;

708 709
  /// The color of unselected tab labels.
  ///
710 711
  /// If this property is null, unselected tab labels are rendered with the
  /// [labelColor] with 70% opacity.
712 713
  final Color unselectedLabelColor;

714
  /// The text style of the selected tab labels.
715
  ///
716 717 718 719
  /// 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
720
  /// [ThemeData.primaryTextTheme]'s bodyText1 definition is used.
721 722
  final TextStyle labelStyle;

723 724
  /// The padding added to each of the tab labels.
  ///
725
  /// If this property is null, then kTabLabelPadding is used.
726 727
  final EdgeInsetsGeometry labelPadding;

728 729
  /// The text style of the unselected tab labels
  ///
730 731
  /// If this property is null, then the [labelStyle] value is used. If [labelStyle]
  /// is null, then the text style of the [ThemeData.primaryTextTheme]'s
732
  /// bodyText1 definition is used.
733 734
  final TextStyle unselectedLabelStyle;

735 736 737
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

738 739 740 741 742 743
  /// The cursor for a mouse pointer when it enters or is hovering over the
  /// individual tab widgets.
  ///
  /// If this property is null, [SystemMouseCursors.click] will be used.
  final MouseCursor mouseCursor;

744 745 746 747 748 749 750 751 752 753 754
  /// An optional callback that's called when the [TabBar] is tapped.
  ///
  /// The callback is applied to the index of the tab where the tap occurred.
  ///
  /// This callback has no effect on the default handling of taps. It's for
  /// applications that want to do a little extra work when a tab is tapped,
  /// even if the tap doesn't change the TabController's index. TabBar [onTap]
  /// callbacks should not make changes to the TabController since that would
  /// interfere with the default tap handler.
  final ValueChanged<int> onTap;

755 756
  /// A size whose height depends on if the tabs have both icons and text.
  ///
757
  /// [AppBar] uses this size to compute its own preferred size.
758
  @override
759
  Size get preferredSize {
760
    for (final Widget item in tabs) {
761 762
      if (item is Tab) {
        final Tab tab = item;
763
        if ((tab.text != null || tab.child != null) && tab.icon != null)
764
          return Size.fromHeight(_kTextAndIconTabHeight + indicatorWeight);
Hans Muller's avatar
Hans Muller committed
765
      }
766
    }
767
    return Size.fromHeight(_kTabHeight + indicatorWeight);
768 769
  }

770
  @override
771
  _TabBarState createState() => _TabBarState();
772
}
773

Hans Muller's avatar
Hans Muller committed
774
class _TabBarState extends State<TabBar> {
775
  ScrollController _scrollController;
Hans Muller's avatar
Hans Muller committed
776 777 778
  TabController _controller;
  _IndicatorPainter _indicatorPainter;
  int _currentIndex;
Ian Hickson's avatar
Ian Hickson committed
779
  double _tabStripWidth;
780 781 782 783 784 785 786
  List<GlobalKey> _tabKeys;

  @override
  void initState() {
    super.initState();
    // If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
    // the width of tab widget i. See _IndicatorPainter.indicatorRect().
787
    _tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList();
788 789 790 791 792
  }

  Decoration get _indicator {
    if (widget.indicator != null)
      return widget.indicator;
793 794 795
    final TabBarTheme tabBarTheme = TabBarTheme.of(context);
    if (tabBarTheme.indicator != null)
      return tabBarTheme.indicator;
796

797
    Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
798 799 800 801 802 803 804
    // 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 matching the material's color, 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.
Ian Hickson's avatar
Ian Hickson committed
805 806 807 808
    //
    // The material's color might be null (if it's a transparency). In that case
    // there's no good way for us to find out what the color is so we don't.
    if (color.value == Material.of(context).color?.value)
809 810
      color = Colors.white;

811
    return UnderlineTabIndicator(
812
      insets: widget.indicatorPadding,
813
      borderSide: BorderSide(
814 815 816 817 818
        width: widget.indicatorWeight,
        color: color,
      ),
    );
  }
Hans Muller's avatar
Hans Muller committed
819

820 821 822 823 824
  // If the TabBar is rebuilt with a new tab controller, the caller should
  // dispose the old one. In that case the old controller's animation will be
  // null and should not be accessed.
  bool get _controllerIsValid => _controller?.animation != null;

Hans Muller's avatar
Hans Muller committed
825
  void _updateTabController() {
826
    final TabController newController = widget.controller ?? DefaultTabController.of(context);
827 828
    assert(() {
      if (newController == null) {
829
        throw FlutterError(
830 831
          'No TabController for ${widget.runtimeType}.\n'
          'When creating a ${widget.runtimeType}, you must either provide an explicit '
832
          'TabController using the "controller" property, or you must ensure that there '
833
          'is a DefaultTabController above the ${widget.runtimeType}.\n'
834 835 836 837
          'In this case, there was neither an explicit controller nor a default controller.'
        );
      }
      return true;
838
    }());
839

Hans Muller's avatar
Hans Muller committed
840
    if (newController == _controller)
841
      return;
Hans Muller's avatar
Hans Muller committed
842

843
    if (_controllerIsValid) {
844 845 846
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
      _controller.removeListener(_handleTabControllerTick);
    }
Hans Muller's avatar
Hans Muller committed
847 848
    _controller = newController;
    if (_controller != null) {
849 850
      _controller.animation.addListener(_handleTabControllerAnimationTick);
      _controller.addListener(_handleTabControllerTick);
Hans Muller's avatar
Hans Muller committed
851
      _currentIndex = _controller.index;
852
    }
853 854
  }

855
  void _initIndicatorPainter() {
856
    _indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
857 858
      controller: _controller,
      indicator: _indicator,
859
      indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,
860 861 862 863 864
      tabKeys: _tabKeys,
      old: _indicatorPainter,
    );
  }

865
  @override
866 867
  void didChangeDependencies() {
    super.didChangeDependencies();
Ian Hickson's avatar
Ian Hickson committed
868
    assert(debugCheckHasMaterial(context));
Hans Muller's avatar
Hans Muller committed
869
    _updateTabController();
870
    _initIndicatorPainter();
871 872
  }

873
  @override
874 875
  void didUpdateWidget(TabBar oldWidget) {
    super.didUpdateWidget(oldWidget);
876
    if (widget.controller != oldWidget.controller) {
Hans Muller's avatar
Hans Muller committed
877
      _updateTabController();
878 879
      _initIndicatorPainter();
    } else if (widget.indicatorColor != oldWidget.indicatorColor ||
880 881
        widget.indicatorWeight != oldWidget.indicatorWeight ||
        widget.indicatorSize != oldWidget.indicatorSize ||
882
        widget.indicator != oldWidget.indicator) {
883
      _initIndicatorPainter();
884
    }
885 886 887

    if (widget.tabs.length > oldWidget.tabs.length) {
      final int delta = widget.tabs.length - oldWidget.tabs.length;
888
      _tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
889 890 891
    } else if (widget.tabs.length < oldWidget.tabs.length) {
      _tabKeys.removeRange(widget.tabs.length, oldWidget.tabs.length);
    }
892 893
  }

894
  @override
Hans Muller's avatar
Hans Muller committed
895
  void dispose() {
896
    _indicatorPainter.dispose();
897
    if (_controllerIsValid) {
898
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
899 900
      _controller.removeListener(_handleTabControllerTick);
    }
901
    _controller = null;
Hans Muller's avatar
Hans Muller committed
902 903
    // We don't own the _controller Animation, so it's not disposed here.
    super.dispose();
904
  }
905

Ian Hickson's avatar
Ian Hickson committed
906
  int get maxTabIndex => _indicatorPainter.maxTabIndex;
907

908 909 910
  double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) {
    if (!widget.isScrollable)
      return 0.0;
Ian Hickson's avatar
Ian Hickson committed
911 912 913 914 915 916 917 918
    double tabCenter = _indicatorPainter.centerOf(index);
    switch (Directionality.of(context)) {
      case TextDirection.rtl:
        tabCenter = _tabStripWidth - tabCenter;
        break;
      case TextDirection.ltr:
        break;
    }
919
    return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent) as double;
920
  }
921

922
  double _tabCenteredScrollOffset(int index) {
923
    final ScrollPosition position = _scrollController.position;
924 925 926 927 928
    return _tabScrollOffset(index, position.viewportDimension, position.minScrollExtent, position.maxScrollExtent);
  }

  double _initialScrollOffset(double viewportWidth, double minExtent, double maxExtent) {
    return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
929 930
  }

Hans Muller's avatar
Hans Muller committed
931
  void _scrollToCurrentIndex() {
932 933
    final double offset = _tabCenteredScrollOffset(_currentIndex);
    _scrollController.animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
Hans Muller's avatar
Hans Muller committed
934 935 936
  }

  void _scrollToControllerValue() {
Ian Hickson's avatar
Ian Hickson committed
937 938 939
    final double leadingPosition = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
    final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
    final double trailingPosition = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null;
Hans Muller's avatar
Hans Muller committed
940 941 942 943 944

    final double index = _controller.index.toDouble();
    final double value = _controller.animation.value;
    double offset;
    if (value == index - 1.0)
Ian Hickson's avatar
Ian Hickson committed
945
      offset = leadingPosition ?? middlePosition;
Hans Muller's avatar
Hans Muller committed
946
    else if (value == index + 1.0)
Ian Hickson's avatar
Ian Hickson committed
947
      offset = trailingPosition ?? middlePosition;
Hans Muller's avatar
Hans Muller committed
948
    else if (value == index)
949
      offset = middlePosition;
Hans Muller's avatar
Hans Muller committed
950
    else if (value < index)
Ian Hickson's avatar
Ian Hickson committed
951
      offset = leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value);
Hans Muller's avatar
Hans Muller committed
952
    else
Ian Hickson's avatar
Ian Hickson committed
953
      offset = trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index);
954

955
    _scrollController.jumpTo(offset);
956
  }
957

958
  void _handleTabControllerAnimationTick() {
Hans Muller's avatar
Hans Muller committed
959
    assert(mounted);
960
    if (!_controller.indexIsChanging && widget.isScrollable) {
961 962
      // Sync the TabBar's scroll position with the TabBarView's PageView.
      _currentIndex = _controller.index;
Hans Muller's avatar
Hans Muller committed
963 964
      _scrollToControllerValue();
    }
965 966
  }

967
  void _handleTabControllerTick() {
Ian Hickson's avatar
Ian Hickson committed
968 969 970 971 972
    if (_controller.index != _currentIndex) {
      _currentIndex = _controller.index;
      if (widget.isScrollable)
        _scrollToCurrentIndex();
    }
973 974 975 976 977 978
    setState(() {
      // Rebuild the tabs after a (potentially animated) index change
      // has completed.
    });
  }

979
  // Called each time layout completes.
Ian Hickson's avatar
Ian Hickson committed
980 981 982
  void _saveTabOffsets(List<double> tabOffsets, TextDirection textDirection, double width) {
    _tabStripWidth = width;
    _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
983 984
  }

Hans Muller's avatar
Hans Muller committed
985
  void _handleTap(int index) {
986
    assert(index >= 0 && index < widget.tabs.length);
Hans Muller's avatar
Hans Muller committed
987
    _controller.animateTo(index);
988 989 990
    if (widget.onTap != null) {
      widget.onTap(index);
    }
Hans Muller's avatar
Hans Muller committed
991 992
  }

993
  Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) {
994
    return _TabStyle(
995 996
      animation: animation,
      selected: selected,
997 998 999 1000
      labelColor: widget.labelColor,
      unselectedLabelColor: widget.unselectedLabelColor,
      labelStyle: widget.labelStyle,
      unselectedLabelStyle: widget.unselectedLabelStyle,
1001 1002 1003 1004
      child: child,
    );
  }

Hans Muller's avatar
Hans Muller committed
1005 1006
  @override
  Widget build(BuildContext context) {
1007
    assert(debugCheckHasMaterialLocalizations(context));
1008 1009 1010
    assert(() {
      if (_controller.length != widget.tabs.length) {
        throw FlutterError(
1011 1012
          "Controller's length property (${_controller.length}) does not match the "
          "number of tabs (${widget.tabs.length}) present in TabBar's tabs property."
1013 1014 1015 1016
        );
      }
      return true;
    }());
1017
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
1018
    if (_controller.length == 0) {
1019
      return Container(
1020 1021 1022 1023
        height: _kTabHeight + widget.indicatorWeight,
      );
    }

1024 1025
    final TabBarTheme tabBarTheme = TabBarTheme.of(context);

1026
    final List<Widget> wrappedTabs = List<Widget>(widget.tabs.length);
1027
    for (int i = 0; i < widget.tabs.length; i += 1) {
1028
      wrappedTabs[i] = Center(
1029
        heightFactor: 1.0,
1030
        child: Padding(
1031
          padding: widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding,
1032
          child: KeyedSubtree(
1033 1034 1035 1036 1037 1038 1039
            key: _tabKeys[i],
            child: widget.tabs[i],
          ),
        ),
      );

    }
Hans Muller's avatar
Hans Muller committed
1040 1041 1042 1043 1044 1045

    // 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) {
      final int previousIndex = _controller.previousIndex;
1046

Hans Muller's avatar
Hans Muller committed
1047
      if (_controller.indexIsChanging) {
1048
        // The user tapped on a tab, the tab controller's animation is running.
Hans Muller's avatar
Hans Muller committed
1049
        assert(_currentIndex != previousIndex);
1050
        final Animation<double> animation = _ChangeAnimation(_controller);
1051 1052
        wrappedTabs[_currentIndex] = _buildStyledTab(wrappedTabs[_currentIndex], true, animation);
        wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
Hans Muller's avatar
Hans Muller committed
1053
      } else {
1054 1055
        // The user is dragging the TabBarView's PageView left or right.
        final int tabIndex = _currentIndex;
1056
        final Animation<double> centerAnimation = _DragAnimation(_controller, tabIndex);
1057 1058 1059
        wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
        if (_currentIndex > 0) {
          final int tabIndex = _currentIndex - 1;
1060
          final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex));
1061
          wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation);
1062
        }
1063
        if (_currentIndex < widget.tabs.length - 1) {
1064
          final int tabIndex = _currentIndex + 1;
1065
          final Animation<double> nextAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex));
1066
          wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation);
1067
        }
Hans Muller's avatar
Hans Muller committed
1068
      }
Hans Muller's avatar
Hans Muller committed
1069 1070
    }

1071
    // Add the tap handler to each tab. If the tab bar is not scrollable,
1072 1073
    // then give all of the tabs equal flexibility so that they each occupy
    // the same share of the tab bar's overall width.
1074
    final int tabCount = widget.tabs.length;
Ian Hickson's avatar
Ian Hickson committed
1075
    for (int index = 0; index < tabCount; index += 1) {
1076
      wrappedTabs[index] = InkWell(
1077
        mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click,
1078
        onTap: () { _handleTap(index); },
1079 1080 1081
        child: Padding(
          padding: EdgeInsets.only(bottom: widget.indicatorWeight),
          child: Stack(
1082 1083
            children: <Widget>[
              wrappedTabs[index],
1084
              Semantics(
1085
                selected: index == _currentIndex,
1086
                label: localizations.tabLabel(tabIndex: index + 1, tabCount: tabCount),
1087
              ),
1088
            ],
1089
          ),
1090
        ),
Hans Muller's avatar
Hans Muller committed
1091
      );
1092
      if (!widget.isScrollable)
1093
        wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
Hans Muller's avatar
Hans Muller committed
1094 1095
    }

1096
    Widget tabBar = CustomPaint(
Hans Muller's avatar
Hans Muller committed
1097
      painter: _indicatorPainter,
1098
      child: _TabStyle(
Ian Hickson's avatar
Ian Hickson committed
1099 1100 1101 1102 1103 1104
        animation: kAlwaysDismissedAnimation,
        selected: false,
        labelColor: widget.labelColor,
        unselectedLabelColor: widget.unselectedLabelColor,
        labelStyle: widget.labelStyle,
        unselectedLabelStyle: widget.unselectedLabelStyle,
1105
        child: _TabLabelBar(
Ian Hickson's avatar
Ian Hickson committed
1106
          onPerformLayout: _saveTabOffsets,
1107
          children: wrappedTabs,
Hans Muller's avatar
Hans Muller committed
1108
        ),
Ian Hickson's avatar
Ian Hickson committed
1109
      ),
1110
    );
Hans Muller's avatar
Hans Muller committed
1111

1112
    if (widget.isScrollable) {
1113 1114
      _scrollController ??= _TabBarScrollController(this);
      tabBar = SingleChildScrollView(
1115
        dragStartBehavior: widget.dragStartBehavior,
Hans Muller's avatar
Hans Muller committed
1116
        scrollDirection: Axis.horizontal,
1117 1118
        controller: _scrollController,
        child: tabBar,
1119 1120 1121
      );
    }

Hans Muller's avatar
Hans Muller committed
1122
    return tabBar;
1123 1124 1125
  }
}

1126
/// A page view that displays the widget which corresponds to the currently
1127 1128 1129
/// selected tab.
///
/// This widget is typically used in conjunction with a [TabBar].
Hans Muller's avatar
Hans Muller committed
1130 1131 1132
///
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// ancestor.
1133 1134 1135 1136 1137
///
/// The tab controller's [TabController.length] must equal the length of the
/// [children] list and the length of the [TabBar.tabs] list.
///
/// To see a sample implementation, visit the [TabController] documentation.
Hans Muller's avatar
Hans Muller committed
1138
class TabBarView extends StatefulWidget {
1139
  /// Creates a page view with one child per tab.
Hans Muller's avatar
Hans Muller committed
1140 1141
  ///
  /// The length of [children] must be the same as the [controller]'s length.
1142
  const TabBarView({
Hans Muller's avatar
Hans Muller committed
1143 1144 1145
    Key key,
    @required this.children,
    this.controller,
1146
    this.physics,
1147
    this.dragStartBehavior = DragStartBehavior.start,
1148 1149 1150
  }) : assert(children != null),
       assert(dragStartBehavior != null),
       super(key: key);
1151

Hans Muller's avatar
Hans Muller committed
1152 1153 1154 1155 1156 1157
  /// This widget's selection and animation state.
  ///
  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  /// will be used.
  final TabController controller;

1158 1159 1160
  /// One widget per tab.
  ///
  /// Its length must match the length of the [TabBar.tabs]
1161
  /// list, as well as the [controller]'s [TabController.length].
Hans Muller's avatar
Hans Muller committed
1162 1163
  final List<Widget> children;

1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174
  /// 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;

1175 1176 1177
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

1178
  @override
1179
  _TabBarViewState createState() => _TabBarViewState();
Hans Muller's avatar
Hans Muller committed
1180
}
1181

1182
final PageScrollPhysics _kTabBarViewPhysics = const PageScrollPhysics().applyTo(const ClampingScrollPhysics());
1183

1184
class _TabBarViewState extends State<TabBarView> {
Hans Muller's avatar
Hans Muller committed
1185
  TabController _controller;
1186
  PageController _pageController;
Hans Muller's avatar
Hans Muller committed
1187
  List<Widget> _children;
1188
  List<Widget> _childrenWithKey;
Hans Muller's avatar
Hans Muller committed
1189 1190 1191
  int _currentIndex;
  int _warpUnderwayCount = 0;

1192 1193 1194 1195 1196
  // If the TabBarView is rebuilt with a new tab controller, the caller should
  // dispose the old one. In that case the old controller's animation will be
  // null and should not be accessed.
  bool get _controllerIsValid => _controller?.animation != null;

Hans Muller's avatar
Hans Muller committed
1197
  void _updateTabController() {
1198
    final TabController newController = widget.controller ?? DefaultTabController.of(context);
1199 1200
    assert(() {
      if (newController == null) {
1201
        throw FlutterError(
1202 1203
          'No TabController for ${widget.runtimeType}.\n'
          'When creating a ${widget.runtimeType}, you must either provide an explicit '
1204
          'TabController using the "controller" property, or you must ensure that there '
1205
          'is a DefaultTabController above the ${widget.runtimeType}.\n'
1206 1207 1208 1209
          'In this case, there was neither an explicit controller nor a default controller.'
        );
      }
      return true;
1210
    }());
1211

Hans Muller's avatar
Hans Muller committed
1212 1213 1214
    if (newController == _controller)
      return;

1215
    if (_controllerIsValid)
1216
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
Hans Muller's avatar
Hans Muller committed
1217 1218
    _controller = newController;
    if (_controller != null)
1219
      _controller.animation.addListener(_handleTabControllerAnimationTick);
1220 1221
  }

Hans Muller's avatar
Hans Muller committed
1222 1223 1224
  @override
  void initState() {
    super.initState();
1225
    _updateChildren();
Hans Muller's avatar
Hans Muller committed
1226 1227
  }

Hans Muller's avatar
Hans Muller committed
1228
  @override
1229 1230
  void didChangeDependencies() {
    super.didChangeDependencies();
Hans Muller's avatar
Hans Muller committed
1231 1232
    _updateTabController();
    _currentIndex = _controller?.index;
1233
    _pageController = PageController(initialPage: _currentIndex ?? 0);
1234 1235
  }

1236
  @override
1237 1238 1239
  void didUpdateWidget(TabBarView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller != oldWidget.controller)
Hans Muller's avatar
Hans Muller committed
1240
      _updateTabController();
1241
    if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
1242
      _updateChildren();
1243 1244
  }

1245
  @override
Hans Muller's avatar
Hans Muller committed
1246
  void dispose() {
1247
    if (_controllerIsValid)
1248
      _controller.animation.removeListener(_handleTabControllerAnimationTick);
1249
    _controller = null;
Hans Muller's avatar
Hans Muller committed
1250 1251 1252
    // We don't own the _controller Animation, so it's not disposed here.
    super.dispose();
  }
Hans Muller's avatar
Hans Muller committed
1253

1254 1255 1256 1257 1258
  void _updateChildren() {
    _children = widget.children;
    _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
  }

1259 1260
  void _handleTabControllerAnimationTick() {
    if (_warpUnderwayCount > 0 || !_controller.indexIsChanging)
Hans Muller's avatar
Hans Muller committed
1261
      return; // This widget is driving the controller's animation.
Hans Muller's avatar
Hans Muller committed
1262

Hans Muller's avatar
Hans Muller committed
1263 1264 1265
    if (_controller.index != _currentIndex) {
      _currentIndex = _controller.index;
      _warpToCurrentIndex();
1266
    }
Hans Muller's avatar
Hans Muller committed
1267
  }
1268

1269
  Future<void> _warpToCurrentIndex() async {
Hans Muller's avatar
Hans Muller committed
1270
    if (!mounted)
1271
      return Future<void>.value();
1272

1273
    if (_pageController.page == _currentIndex.toDouble())
1274
      return Future<void>.value();
1275

Hans Muller's avatar
Hans Muller committed
1276
    final int previousIndex = _controller.previousIndex;
1277 1278 1279 1280 1281 1282
    if ((_currentIndex - previousIndex).abs() == 1) {
      _warpUnderwayCount += 1;
      await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
      _warpUnderwayCount -= 1;
      return Future<void>.value();
    }
Hans Muller's avatar
Hans Muller committed
1283 1284

    assert((_currentIndex - previousIndex).abs() > 1);
1285 1286 1287 1288
    final int initialPage = _currentIndex > previousIndex
        ? _currentIndex - 1
        : _currentIndex + 1;
    final List<Widget> originalChildren = _childrenWithKey;
Hans Muller's avatar
Hans Muller committed
1289 1290
    setState(() {
      _warpUnderwayCount += 1;
1291

1292 1293 1294 1295 1296
      _childrenWithKey = List<Widget>.from(_childrenWithKey, growable: false);
      final Widget temp = _childrenWithKey[initialPage];
      _childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
      _childrenWithKey[previousIndex] = temp;
    });
1297
    _pageController.jumpToPage(initialPage);
Hans Muller's avatar
Hans Muller committed
1298

1299
    await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
Hans Muller's avatar
Hans Muller committed
1300
    if (!mounted)
1301
      return Future<void>.value();
Hans Muller's avatar
Hans Muller committed
1302 1303
    setState(() {
      _warpUnderwayCount -= 1;
1304 1305 1306 1307 1308
      if (widget.children != _children) {
        _updateChildren();
      } else {
        _childrenWithKey = originalChildren;
      }
Hans Muller's avatar
Hans Muller committed
1309
    });
Hans Muller's avatar
Hans Muller committed
1310 1311
  }

1312
  // Called when the PageView scrolls
Adam Barth's avatar
Adam Barth committed
1313
  bool _handleScrollNotification(ScrollNotification notification) {
Hans Muller's avatar
Hans Muller committed
1314 1315
    if (_warpUnderwayCount > 0)
      return false;
Hans Muller's avatar
Hans Muller committed
1316

1317
    if (notification.depth != 0)
Hans Muller's avatar
Hans Muller committed
1318
      return false;
Hans Muller's avatar
Hans Muller committed
1319

1320
    _warpUnderwayCount += 1;
1321
    if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) {
1322 1323
      if ((_pageController.page - _controller.index).abs() > 1.0) {
        _controller.index = _pageController.page.floor();
1324
        _currentIndex =_controller.index;
1325
      }
1326
      _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0) as double;
1327
    } else if (notification is ScrollEndNotification) {
1328
      _controller.index = _pageController.page.round();
1329
      _currentIndex = _controller.index;
Hans Muller's avatar
Hans Muller committed
1330
    }
1331
    _warpUnderwayCount -= 1;
Hans Muller's avatar
Hans Muller committed
1332 1333

    return false;
Hans Muller's avatar
Hans Muller committed
1334 1335
  }

1336
  @override
Hans Muller's avatar
Hans Muller committed
1337
  Widget build(BuildContext context) {
1338 1339 1340
    assert(() {
      if (_controller.length != widget.children.length) {
        throw FlutterError(
1341 1342
          "Controller's length property (${_controller.length}) does not match the "
          "number of tabs (${widget.children.length}) present in TabBar's tabs property."
1343 1344 1345 1346
        );
      }
      return true;
    }());
1347
    return NotificationListener<ScrollNotification>(
Hans Muller's avatar
Hans Muller committed
1348
      onNotification: _handleScrollNotification,
1349
      child: PageView(
1350
        dragStartBehavior: widget.dragStartBehavior,
1351
        controller: _pageController,
1352
        physics: widget.physics == null ? _kTabBarViewPhysics : _kTabBarViewPhysics.applyTo(widget.physics),
1353
        children: _childrenWithKey,
Hans Muller's avatar
Hans Muller committed
1354
      ),
1355
    );
Hans Muller's avatar
Hans Muller committed
1356
  }
1357
}
Hixie's avatar
Hixie committed
1358

1359
/// Displays a single circle with the specified border and background colors.
1360 1361 1362
///
/// Used by [TabPageSelector] to indicate the selected page.
class TabPageSelectorIndicator extends StatelessWidget {
1363
  /// Creates an indicator used by [TabPageSelector].
1364
  ///
1365
  /// The [backgroundColor], [borderColor], and [size] parameters must not be null.
1366 1367 1368 1369 1370
  const TabPageSelectorIndicator({
    Key key,
    @required this.backgroundColor,
    @required this.borderColor,
    @required this.size,
1371 1372 1373 1374
  }) : assert(backgroundColor != null),
       assert(borderColor != null),
       assert(size != null),
       super(key: key);
1375 1376 1377 1378 1379 1380 1381

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

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

1382 1383 1384
  /// The indicator circle's diameter.
  final double size;

1385 1386
  @override
  Widget build(BuildContext context) {
1387
    return Container(
1388 1389
      width: size,
      height: size,
1390
      margin: const EdgeInsets.all(4.0),
1391
      decoration: BoxDecoration(
1392
        color: backgroundColor,
1393
        border: Border.all(color: borderColor),
1394
        shape: BoxShape.circle,
1395 1396 1397 1398 1399
      ),
    );
  }
}

1400
/// Displays a row of small circular indicators, one per tab.
1401
///
1402 1403 1404 1405 1406
/// The selected tab's indicator is highlighted. Often used in conjunction with
/// a [TabBarView].
///
/// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor.
Hans Muller's avatar
Hans Muller committed
1407 1408
class TabPageSelector extends StatelessWidget {
  /// Creates a compact widget that indicates which tab has been selected.
1409 1410 1411
  const TabPageSelector({
    Key key,
    this.controller,
1412
    this.indicatorSize = 12.0,
1413 1414
    this.color,
    this.selectedColor,
1415 1416
  }) : assert(indicatorSize != null && indicatorSize > 0.0),
       super(key: key);
Hixie's avatar
Hixie committed
1417

Hans Muller's avatar
Hans Muller committed
1418 1419
  /// This widget's selection and animation state.
  ///
1420 1421
  /// If [TabController] is not provided, then the value of
  /// [DefaultTabController.of] will be used.
Hans Muller's avatar
Hans Muller committed
1422 1423
  final TabController controller;

1424 1425 1426
  /// The indicator circle's diameter (the default value is 12.0).
  final double indicatorSize;

1427
  /// The indicator circle's fill color for unselected pages.
1428
  ///
1429
  /// If this parameter is null, then the indicator is filled with [Colors.transparent].
1430 1431
  final Color color;

1432
  /// The indicator circle's fill color for selected pages and border color
1433 1434
  /// for all indicator circles.
  ///
1435
  /// If this parameter is null, then the indicator is filled with the theme's
1436 1437 1438
  /// accent color, [ThemeData.accentColor].
  final Color selectedColor;

Hans Muller's avatar
Hans Muller committed
1439 1440 1441
  Widget _buildTabIndicator(
    int tabIndex,
    TabController tabController,
1442 1443
    ColorTween selectedColorTween,
    ColorTween previousColorTween,
Hans Muller's avatar
Hans Muller committed
1444
  ) {
Hixie's avatar
Hixie committed
1445
    Color background;
Hans Muller's avatar
Hans Muller committed
1446
    if (tabController.indexIsChanging) {
Hixie's avatar
Hixie committed
1447
      // The selection's animation is animating from previousValue to value.
1448
      final double t = 1.0 - _indexChangeProgress(tabController);
Hans Muller's avatar
Hans Muller committed
1449
      if (tabController.index == tabIndex)
1450
        background = selectedColorTween.lerp(t);
Hans Muller's avatar
Hans Muller committed
1451
      else if (tabController.previousIndex == tabIndex)
1452
        background = previousColorTween.lerp(t);
Hixie's avatar
Hixie committed
1453
      else
1454
        background = selectedColorTween.begin;
Hixie's avatar
Hixie committed
1455
    } else {
Ian Hickson's avatar
Ian Hickson committed
1456 1457
      // The selection's offset reflects how far the TabBarView has / been dragged
      // to the previous page (-1.0 to 0.0) or the next page (0.0 to 1.0).
1458 1459
      final double offset = tabController.offset;
      if (tabController.index == tabIndex) {
1460
        background = selectedColorTween.lerp(1.0 - offset.abs());
1461
      } else if (tabController.index == tabIndex - 1 && offset > 0.0) {
1462
        background = selectedColorTween.lerp(offset);
1463
      } else if (tabController.index == tabIndex + 1 && offset < 0.0) {
1464
        background = selectedColorTween.lerp(-offset);
1465
      } else {
1466
        background = selectedColorTween.begin;
1467
      }
Hixie's avatar
Hixie committed
1468
    }
1469
    return TabPageSelectorIndicator(
1470
      backgroundColor: background,
1471 1472
      borderColor: selectedColorTween.end,
      size: indicatorSize,
Hixie's avatar
Hixie committed
1473 1474 1475
    );
  }

1476
  @override
Hixie's avatar
Hixie committed
1477
  Widget build(BuildContext context) {
1478 1479
    final Color fixColor = color ?? Colors.transparent;
    final Color fixSelectedColor = selectedColor ?? Theme.of(context).accentColor;
1480 1481
    final ColorTween selectedColorTween = ColorTween(begin: fixColor, end: fixSelectedColor);
    final ColorTween previousColorTween = ColorTween(begin: fixSelectedColor, end: fixColor);
Hans Muller's avatar
Hans Muller committed
1482
    final TabController tabController = controller ?? DefaultTabController.of(context);
1483 1484
    assert(() {
      if (tabController == null) {
1485
        throw FlutterError(
1486 1487 1488 1489 1490 1491 1492 1493
          '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;
1494
    }());
1495
    final Animation<double> animation = CurvedAnimation(
Hans Muller's avatar
Hans Muller committed
1496 1497 1498
      parent: tabController.animation,
      curve: Curves.fastOutSlowIn,
    );
1499
    return AnimatedBuilder(
Hixie's avatar
Hixie committed
1500 1501
      animation: animation,
      builder: (BuildContext context, Widget child) {
1502
        return Semantics(
1503
          label: 'Page ${tabController.index + 1} of ${tabController.length}',
1504
          child: Row(
Hans Muller's avatar
Hans Muller committed
1505
            mainAxisSize: MainAxisSize.min,
1506
            children: List<Widget>.generate(tabController.length, (int tabIndex) {
1507
              return _buildTabIndicator(tabIndex, tabController, selectedColorTween, previousColorTween);
Hans Muller's avatar
Hans Muller committed
1508 1509
            }).toList(),
          ),
Hixie's avatar
Hixie committed
1510
        );
1511
      },
Hixie's avatar
Hixie committed
1512 1513 1514
    );
  }
}