"Fossies" - the Fresh Open Source Software Archive

Member "flutter-1.22.4/packages/flutter/lib/src/material/tabs.dart" (13 Nov 2020, 53948 Bytes) of package /linux/misc/flutter-1.22.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 // Copyright 2014 The Flutter Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style license that can be
    3 // found in the LICENSE file.
    4 
    5 // @dart = 2.8
    6 
    7 import 'dart:async';
    8 import 'dart:ui' show lerpDouble;
    9 
   10 import 'package:flutter/rendering.dart';
   11 import 'package:flutter/widgets.dart';
   12 import 'package:flutter/gestures.dart' show DragStartBehavior;
   13 
   14 import 'app_bar.dart';
   15 import 'colors.dart';
   16 import 'constants.dart';
   17 import 'debug.dart';
   18 import 'ink_well.dart';
   19 import 'material.dart';
   20 import 'material_localizations.dart';
   21 import 'tab_bar_theme.dart';
   22 import 'tab_controller.dart';
   23 import 'tab_indicator.dart';
   24 import 'theme.dart';
   25 
   26 const double _kTabHeight = 46.0;
   27 const double _kTextAndIconTabHeight = 72.0;
   28 
   29 /// Defines how the bounds of the selected tab indicator are computed.
   30 ///
   31 /// See also:
   32 ///
   33 ///  * [TabBar], which displays a row of tabs.
   34 ///  * [TabBarView], which displays a widget for the currently selected tab.
   35 ///  * [TabBar.indicator], which defines the appearance of the selected tab
   36 ///    indicator relative to the tab's bounds.
   37 enum TabBarIndicatorSize {
   38   /// The tab indicator's bounds are as wide as the space occupied by the tab
   39   /// in the tab bar: from the right edge of the previous tab to the left edge
   40   /// of the next tab.
   41   tab,
   42 
   43   /// The tab's bounds are only as wide as the (centered) tab widget itself.
   44   ///
   45   /// This value is used to align the tab's label, typically a [Tab]
   46   /// widget's text or icon, with the selected tab indicator.
   47   label,
   48 }
   49 
   50 /// A material design [TabBar] tab.
   51 ///
   52 /// If both [icon] and [text] are provided, the text is displayed below
   53 /// the icon.
   54 ///
   55 /// See also:
   56 ///
   57 ///  * [TabBar], which displays a row of tabs.
   58 ///  * [TabBarView], which displays a widget for the currently selected tab.
   59 ///  * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
   60 ///  * <https://material.io/design/components/tabs.html>
   61 class Tab extends StatelessWidget {
   62   /// Creates a material design [TabBar] tab.
   63   ///
   64   /// At least one of [text], [icon], and [child] must be non-null. The [text]
   65   /// and [child] arguments must not be used at the same time. The
   66   /// [iconMargin] is only useful when [icon] and either one of [text] or
   67   /// [child] is non-null.
   68   const Tab({
   69     Key key,
   70     this.text,
   71     this.icon,
   72     this.iconMargin = const EdgeInsets.only(bottom: 10.0),
   73     this.child,
   74   }) : assert(text != null || child != null || icon != null),
   75        assert(text == null || child == null),
   76        super(key: key);
   77 
   78   /// The text to display as the tab's label.
   79   ///
   80   /// Must not be used in combination with [child].
   81   final String text;
   82 
   83   /// The widget to be used as the tab's label.
   84   ///
   85   /// Usually a [Text] widget, possibly wrapped in a [Semantics] widget.
   86   ///
   87   /// Must not be used in combination with [text].
   88   final Widget child;
   89 
   90   /// An icon to display as the tab's label.
   91   final Widget icon;
   92 
   93   /// The margin added around the tab's icon.
   94   ///
   95   /// Only useful when used in combination with [icon], and either one of
   96   /// [text] or [child] is non-null.
   97   final EdgeInsetsGeometry iconMargin;
   98 
   99   Widget _buildLabelText() {
  100     return child ?? Text(text, softWrap: false, overflow: TextOverflow.fade);
  101   }
  102 
  103   @override
  104   Widget build(BuildContext context) {
  105     assert(debugCheckHasMaterial(context));
  106 
  107     double height;
  108     Widget label;
  109     if (icon == null) {
  110       height = _kTabHeight;
  111       label = _buildLabelText();
  112     } else if (text == null && child == null) {
  113       height = _kTabHeight;
  114       label = icon;
  115     } else {
  116       height = _kTextAndIconTabHeight;
  117       label = Column(
  118         mainAxisAlignment: MainAxisAlignment.center,
  119         crossAxisAlignment: CrossAxisAlignment.center,
  120         children: <Widget>[
  121           Container(
  122             child: icon,
  123             margin: iconMargin,
  124           ),
  125           _buildLabelText(),
  126         ],
  127       );
  128     }
  129 
  130     return SizedBox(
  131       height: height,
  132       child: Center(
  133         child: label,
  134         widthFactor: 1.0,
  135       ),
  136     );
  137   }
  138 
  139   @override
  140   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  141     super.debugFillProperties(properties);
  142     properties.add(StringProperty('text', text, defaultValue: null));
  143     properties.add(DiagnosticsProperty<Widget>('icon', icon, defaultValue: null));
  144   }
  145 }
  146 
  147 class _TabStyle extends AnimatedWidget {
  148   const _TabStyle({
  149     Key key,
  150     Animation<double> animation,
  151     this.selected,
  152     this.labelColor,
  153     this.unselectedLabelColor,
  154     this.labelStyle,
  155     this.unselectedLabelStyle,
  156     @required this.child,
  157   }) : super(key: key, listenable: animation);
  158 
  159   final TextStyle labelStyle;
  160   final TextStyle unselectedLabelStyle;
  161   final bool selected;
  162   final Color labelColor;
  163   final Color unselectedLabelColor;
  164   final Widget child;
  165 
  166   @override
  167   Widget build(BuildContext context) {
  168     final ThemeData themeData = Theme.of(context);
  169     final TabBarTheme tabBarTheme = TabBarTheme.of(context);
  170     final Animation<double> animation = listenable as Animation<double>;
  171 
  172     // To enable TextStyle.lerp(style1, style2, value), both styles must have
  173     // the same value of inherit. Force that to be inherit=true here.
  174     final TextStyle defaultStyle = (labelStyle
  175       ?? tabBarTheme.labelStyle
  176       ?? themeData.primaryTextTheme.bodyText1
  177     ).copyWith(inherit: true);
  178     final TextStyle defaultUnselectedStyle = (unselectedLabelStyle
  179       ?? tabBarTheme.unselectedLabelStyle
  180       ?? labelStyle
  181       ?? themeData.primaryTextTheme.bodyText1
  182     ).copyWith(inherit: true);
  183     final TextStyle textStyle = selected
  184       ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)
  185       : TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value);
  186 
  187     final Color selectedColor = labelColor
  188        ?? tabBarTheme.labelColor
  189        ?? themeData.primaryTextTheme.bodyText1.color;
  190     final Color unselectedColor = unselectedLabelColor
  191       ?? tabBarTheme.unselectedLabelColor
  192       ?? selectedColor.withAlpha(0xB2); // 70% alpha
  193     final Color color = selected
  194       ? Color.lerp(selectedColor, unselectedColor, animation.value)
  195       : Color.lerp(unselectedColor, selectedColor, animation.value);
  196 
  197     return DefaultTextStyle(
  198       style: textStyle.copyWith(color: color),
  199       child: IconTheme.merge(
  200         data: IconThemeData(
  201           size: 24.0,
  202           color: color,
  203         ),
  204         child: child,
  205       ),
  206     );
  207   }
  208 }
  209 
  210 typedef _LayoutCallback = void Function(List<double> xOffsets, TextDirection textDirection, double width);
  211 
  212 class _TabLabelBarRenderer extends RenderFlex {
  213   _TabLabelBarRenderer({
  214     List<RenderBox> children,
  215     @required Axis direction,
  216     @required MainAxisSize mainAxisSize,
  217     @required MainAxisAlignment mainAxisAlignment,
  218     @required CrossAxisAlignment crossAxisAlignment,
  219     @required TextDirection textDirection,
  220     @required VerticalDirection verticalDirection,
  221     @required this.onPerformLayout,
  222   }) : assert(onPerformLayout != null),
  223        assert(textDirection != null),
  224        super(
  225          children: children,
  226          direction: direction,
  227          mainAxisSize: mainAxisSize,
  228          mainAxisAlignment: mainAxisAlignment,
  229          crossAxisAlignment: crossAxisAlignment,
  230          textDirection: textDirection,
  231          verticalDirection: verticalDirection,
  232        );
  233 
  234   _LayoutCallback onPerformLayout;
  235 
  236   @override
  237   void performLayout() {
  238     super.performLayout();
  239     // xOffsets will contain childCount+1 values, giving the offsets of the
  240     // leading edge of the first tab as the first value, of the leading edge of
  241     // the each subsequent tab as each subsequent value, and of the trailing
  242     // edge of the last tab as the last value.
  243     RenderBox child = firstChild;
  244     final List<double> xOffsets = <double>[];
  245     while (child != null) {
  246       final FlexParentData childParentData = child.parentData as FlexParentData;
  247       xOffsets.add(childParentData.offset.dx);
  248       assert(child.parentData == childParentData);
  249       child = childParentData.nextSibling;
  250     }
  251     assert(textDirection != null);
  252     switch (textDirection) {
  253       case TextDirection.rtl:
  254         xOffsets.insert(0, size.width);
  255         break;
  256       case TextDirection.ltr:
  257         xOffsets.add(size.width);
  258         break;
  259     }
  260     onPerformLayout(xOffsets, textDirection, size.width);
  261   }
  262 }
  263 
  264 // This class and its renderer class only exist to report the widths of the tabs
  265 // upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
  266 // or in response to input.
  267 class _TabLabelBar extends Flex {
  268   _TabLabelBar({
  269     Key key,
  270     List<Widget> children = const <Widget>[],
  271     this.onPerformLayout,
  272   }) : super(
  273     key: key,
  274     children: children,
  275     direction: Axis.horizontal,
  276     mainAxisSize: MainAxisSize.max,
  277     mainAxisAlignment: MainAxisAlignment.start,
  278     crossAxisAlignment: CrossAxisAlignment.center,
  279     verticalDirection: VerticalDirection.down,
  280   );
  281 
  282   final _LayoutCallback onPerformLayout;
  283 
  284   @override
  285   RenderFlex createRenderObject(BuildContext context) {
  286     return _TabLabelBarRenderer(
  287       direction: direction,
  288       mainAxisAlignment: mainAxisAlignment,
  289       mainAxisSize: mainAxisSize,
  290       crossAxisAlignment: crossAxisAlignment,
  291       textDirection: getEffectiveTextDirection(context),
  292       verticalDirection: verticalDirection,
  293       onPerformLayout: onPerformLayout,
  294     );
  295   }
  296 
  297   @override
  298   void updateRenderObject(BuildContext context, _TabLabelBarRenderer renderObject) {
  299     super.updateRenderObject(context, renderObject);
  300     renderObject.onPerformLayout = onPerformLayout;
  301   }
  302 }
  303 
  304 double _indexChangeProgress(TabController controller) {
  305   final double controllerValue = controller.animation.value;
  306   final double previousIndex = controller.previousIndex.toDouble();
  307   final double currentIndex = controller.index.toDouble();
  308 
  309   // The controller's offset is changing because the user is dragging the
  310   // TabBarView's PageView to the left or right.
  311   if (!controller.indexIsChanging)
  312     return (currentIndex - controllerValue).abs().clamp(0.0, 1.0) as double;
  313 
  314   // The TabController animation's value is changing from previousIndex to currentIndex.
  315   return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs();
  316 }
  317 
  318 class _IndicatorPainter extends CustomPainter {
  319   _IndicatorPainter({
  320     @required this.controller,
  321     @required this.indicator,
  322     @required this.indicatorSize,
  323     @required this.tabKeys,
  324     _IndicatorPainter old,
  325   }) : assert(controller != null),
  326        assert(indicator != null),
  327        super(repaint: controller.animation) {
  328     if (old != null)
  329       saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
  330   }
  331 
  332   final TabController controller;
  333   final Decoration indicator;
  334   final TabBarIndicatorSize indicatorSize;
  335   final List<GlobalKey> tabKeys;
  336 
  337   List<double> _currentTabOffsets;
  338   TextDirection _currentTextDirection;
  339   Rect _currentRect;
  340   BoxPainter _painter;
  341   bool _needsPaint = false;
  342   void markNeedsPaint() {
  343     _needsPaint = true;
  344   }
  345 
  346   void dispose() {
  347     _painter?.dispose();
  348   }
  349 
  350   void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) {
  351     _currentTabOffsets = tabOffsets;
  352     _currentTextDirection = textDirection;
  353   }
  354 
  355   // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
  356   // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
  357   int get maxTabIndex => _currentTabOffsets.length - 2;
  358 
  359   double centerOf(int tabIndex) {
  360     assert(_currentTabOffsets != null);
  361     assert(_currentTabOffsets.isNotEmpty);
  362     assert(tabIndex >= 0);
  363     assert(tabIndex <= maxTabIndex);
  364     return (_currentTabOffsets[tabIndex] + _currentTabOffsets[tabIndex + 1]) / 2.0;
  365   }
  366 
  367   Rect indicatorRect(Size tabBarSize, int tabIndex) {
  368     assert(_currentTabOffsets != null);
  369     assert(_currentTextDirection != null);
  370     assert(_currentTabOffsets.isNotEmpty);
  371     assert(tabIndex >= 0);
  372     assert(tabIndex <= maxTabIndex);
  373     double tabLeft, tabRight;
  374     switch (_currentTextDirection) {
  375       case TextDirection.rtl:
  376         tabLeft = _currentTabOffsets[tabIndex + 1];
  377         tabRight = _currentTabOffsets[tabIndex];
  378         break;
  379       case TextDirection.ltr:
  380         tabLeft = _currentTabOffsets[tabIndex];
  381         tabRight = _currentTabOffsets[tabIndex + 1];
  382         break;
  383     }
  384 
  385     if (indicatorSize == TabBarIndicatorSize.label) {
  386       final double tabWidth = tabKeys[tabIndex].currentContext.size.width;
  387       final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0;
  388       tabLeft += delta;
  389       tabRight -= delta;
  390     }
  391 
  392     return Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height);
  393   }
  394 
  395   @override
  396   void paint(Canvas canvas, Size size) {
  397     _needsPaint = false;
  398     _painter ??= indicator.createBoxPainter(markNeedsPaint);
  399 
  400     if (controller.indexIsChanging) {
  401       // The user tapped on a tab, the tab controller's animation is running.
  402       final Rect targetRect = indicatorRect(size, controller.index);
  403       _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller));
  404     } else {
  405       // The user is dragging the TabBarView's PageView left or right.
  406       final int currentIndex = controller.index;
  407       final Rect previous = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
  408       final Rect middle = indicatorRect(size, currentIndex);
  409       final Rect next = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null;
  410       final double index = controller.index.toDouble();
  411       final double value = controller.animation.value;
  412       if (value == index - 1.0)
  413         _currentRect = previous ?? middle;
  414       else if (value == index + 1.0)
  415         _currentRect = next ?? middle;
  416       else if (value == index)
  417         _currentRect = middle;
  418       else if (value < index)
  419         _currentRect = previous == null ? middle : Rect.lerp(middle, previous, index - value);
  420       else
  421         _currentRect = next == null ? middle : Rect.lerp(middle, next, value - index);
  422     }
  423     assert(_currentRect != null);
  424 
  425     final ImageConfiguration configuration = ImageConfiguration(
  426       size: _currentRect.size,
  427       textDirection: _currentTextDirection,
  428     );
  429     _painter.paint(canvas, _currentRect.topLeft, configuration);
  430   }
  431 
  432   static bool _tabOffsetsEqual(List<double> a, List<double> b) {
  433     // TODO(shihaohong): The following null check should be replaced when a fix
  434     // for https://github.com/flutter/flutter/issues/40014 is available.
  435     if (a == null || b == null || a.length != b.length)
  436       return false;
  437     for (int i = 0; i < a.length; i += 1) {
  438       if (a[i] != b[i])
  439         return false;
  440     }
  441     return true;
  442   }
  443 
  444   @override
  445   bool shouldRepaint(_IndicatorPainter old) {
  446     return _needsPaint
  447         || controller != old.controller
  448         || indicator != old.indicator
  449         || tabKeys.length != old.tabKeys.length
  450         || (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets))
  451         || _currentTextDirection != old._currentTextDirection;
  452   }
  453 }
  454 
  455 class _ChangeAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  456   _ChangeAnimation(this.controller);
  457 
  458   final TabController controller;
  459 
  460   @override
  461   Animation<double> get parent => controller.animation;
  462 
  463   @override
  464   void removeStatusListener(AnimationStatusListener listener) {
  465     if (parent != null)
  466       super.removeStatusListener(listener);
  467   }
  468 
  469   @override
  470   void removeListener(VoidCallback listener) {
  471     if (parent != null)
  472       super.removeListener(listener);
  473   }
  474 
  475   @override
  476   double get value => _indexChangeProgress(controller);
  477 }
  478 
  479 class _DragAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  480   _DragAnimation(this.controller, this.index);
  481 
  482   final TabController controller;
  483   final int index;
  484 
  485   @override
  486   Animation<double> get parent => controller.animation;
  487 
  488   @override
  489   void removeStatusListener(AnimationStatusListener listener) {
  490     if (parent != null)
  491       super.removeStatusListener(listener);
  492   }
  493 
  494   @override
  495   void removeListener(VoidCallback listener) {
  496     if (parent != null)
  497       super.removeListener(listener);
  498   }
  499 
  500   @override
  501   double get value {
  502     assert(!controller.indexIsChanging);
  503     final double controllerMaxValue = (controller.length - 1).toDouble();
  504     final double controllerValue = controller.animation.value.clamp(0.0, controllerMaxValue) as double;
  505     return (controllerValue - index.toDouble()).abs().clamp(0.0, 1.0) as double;
  506   }
  507 }
  508 
  509 // This class, and TabBarScrollController, only exist to handle the case
  510 // where a scrollable TabBar has a non-zero initialIndex. In that case we can
  511 // only compute the scroll position's initial scroll offset (the "correct"
  512 // pixels value) after the TabBar viewport width and scroll limits are known.
  513 class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
  514   _TabBarScrollPosition({
  515     ScrollPhysics physics,
  516     ScrollContext context,
  517     ScrollPosition oldPosition,
  518     this.tabBar,
  519   }) : super(
  520     physics: physics,
  521     context: context,
  522     initialPixels: null,
  523     oldPosition: oldPosition,
  524   );
  525 
  526   final _TabBarState tabBar;
  527 
  528   bool _initialViewportDimensionWasZero;
  529 
  530   @override
  531   bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
  532     bool result = true;
  533     if (_initialViewportDimensionWasZero != true) {
  534       // If the viewport never had a non-zero dimension, we just want to jump
  535       // to the initial scroll position to avoid strange scrolling effects in
  536       // release mode: In release mode, the viewport temporarily may have a
  537       // dimension of zero before the actual dimension is calculated. In that
  538       // scenario, setting the actual dimension would cause a strange scroll
  539       // effect without this guard because the super call below would starts a
  540       // ballistic scroll activity.
  541       assert(viewportDimension != null);
  542       _initialViewportDimensionWasZero = viewportDimension != 0.0;
  543       correctPixels(tabBar._initialScrollOffset(viewportDimension, minScrollExtent, maxScrollExtent));
  544       result = false;
  545     }
  546     return super.applyContentDimensions(minScrollExtent, maxScrollExtent) && result;
  547   }
  548 }
  549 
  550 // This class, and TabBarScrollPosition, only exist to handle the case
  551 // where a scrollable TabBar has a non-zero initialIndex.
  552 class _TabBarScrollController extends ScrollController {
  553   _TabBarScrollController(this.tabBar);
  554 
  555   final _TabBarState tabBar;
  556 
  557   @override
  558   ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
  559     return _TabBarScrollPosition(
  560       physics: physics,
  561       context: context,
  562       oldPosition: oldPosition,
  563       tabBar: tabBar,
  564     );
  565   }
  566 }
  567 
  568 /// A material design widget that displays a horizontal row of tabs.
  569 ///
  570 /// Typically created as the [AppBar.bottom] part of an [AppBar] and in
  571 /// conjunction with a [TabBarView].
  572 ///
  573 /// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
  574 ///
  575 /// If a [TabController] is not provided, then a [DefaultTabController] ancestor
  576 /// must be provided instead. The tab controller's [TabController.length] must
  577 /// equal the length of the [tabs] list and the length of the
  578 /// [TabBarView.children] list.
  579 ///
  580 /// Requires one of its ancestors to be a [Material] widget.
  581 ///
  582 /// Uses values from [TabBarTheme] if it is set in the current context.
  583 ///
  584 /// To see a sample implementation, visit the [TabController] documentation.
  585 ///
  586 /// See also:
  587 ///
  588 ///  * [TabBarView], which displays page views that correspond to each tab.
  589 class TabBar extends StatefulWidget implements PreferredSizeWidget {
  590   /// Creates a material design tab bar.
  591   ///
  592   /// The [tabs] argument must not be null and its length must match the [controller]'s
  593   /// [TabController.length].
  594   ///
  595   /// If a [TabController] is not provided, then there must be a
  596   /// [DefaultTabController] ancestor.
  597   ///
  598   /// The [indicatorWeight] parameter defaults to 2, and must not be null.
  599   ///
  600   /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.
  601   ///
  602   /// If [indicator] is not null or provided from [TabBarTheme],
  603   /// then [indicatorWeight], [indicatorPadding], and [indicatorColor] are ignored.
  604   const TabBar({
  605     Key key,
  606     @required this.tabs,
  607     this.controller,
  608     this.isScrollable = false,
  609     this.indicatorColor,
  610     this.indicatorWeight = 2.0,
  611     this.indicatorPadding = EdgeInsets.zero,
  612     this.indicator,
  613     this.indicatorSize,
  614     this.labelColor,
  615     this.labelStyle,
  616     this.labelPadding,
  617     this.unselectedLabelColor,
  618     this.unselectedLabelStyle,
  619     this.dragStartBehavior = DragStartBehavior.start,
  620     this.mouseCursor,
  621     this.onTap,
  622     this.physics,
  623   }) : assert(tabs != null),
  624        assert(isScrollable != null),
  625        assert(dragStartBehavior != null),
  626        assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)),
  627        assert(indicator != null || (indicatorPadding != null)),
  628        super(key: key);
  629 
  630   /// Typically a list of two or more [Tab] widgets.
  631   ///
  632   /// The length of this list must match the [controller]'s [TabController.length]
  633   /// and the length of the [TabBarView.children] list.
  634   final List<Widget> tabs;
  635 
  636   /// This widget's selection and animation state.
  637   ///
  638   /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  639   /// will be used.
  640   final TabController controller;
  641 
  642   /// Whether this tab bar can be scrolled horizontally.
  643   ///
  644   /// If [isScrollable] is true, then each tab is as wide as needed for its label
  645   /// and the entire [TabBar] is scrollable. Otherwise each tab gets an equal
  646   /// share of the available space.
  647   final bool isScrollable;
  648 
  649   /// The color of the line that appears below the selected tab.
  650   ///
  651   /// If this parameter is null, then the value of the Theme's indicatorColor
  652   /// property is used.
  653   ///
  654   /// If [indicator] is specified or provided from [TabBarTheme],
  655   /// this property is ignored.
  656   final Color indicatorColor;
  657 
  658   /// The thickness of the line that appears below the selected tab.
  659   ///
  660   /// The value of this parameter must be greater than zero and its default
  661   /// value is 2.0.
  662   ///
  663   /// If [indicator] is specified or provided from [TabBarTheme],
  664   /// this property is ignored.
  665   final double indicatorWeight;
  666 
  667   /// The horizontal padding for the line that appears below the selected tab.
  668   ///
  669   /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align
  670   /// the indicator with the tab's text for [Tab] widgets and all but the
  671   /// shortest [Tab.text] values.
  672   ///
  673   /// The [EdgeInsets.top] and [EdgeInsets.bottom] values of the
  674   /// [indicatorPadding] are ignored.
  675   ///
  676   /// The default value of [indicatorPadding] is [EdgeInsets.zero].
  677   ///
  678   /// If [indicator] is specified or provided from [TabBarTheme],
  679   /// this property is ignored.
  680   final EdgeInsetsGeometry indicatorPadding;
  681 
  682   /// Defines the appearance of the selected tab indicator.
  683   ///
  684   /// If [indicator] is specified or provided from [TabBarTheme],
  685   /// the [indicatorColor], [indicatorWeight], and [indicatorPadding]
  686   /// properties are ignored.
  687   ///
  688   /// The default, underline-style, selected tab indicator can be defined with
  689   /// [UnderlineTabIndicator].
  690   ///
  691   /// The indicator's size is based on the tab's bounds. If [indicatorSize]
  692   /// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space
  693   /// occupied by the tab in the tab bar. If [indicatorSize] is
  694   /// [TabBarIndicatorSize.label], then the tab's bounds are only as wide as
  695   /// the tab widget itself.
  696   final Decoration indicator;
  697 
  698   /// Defines how the selected tab indicator's size is computed.
  699   ///
  700   /// The size of the selected tab indicator is defined relative to the
  701   /// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab]
  702   /// (the default) or relative to the bounds of the tab's widget if
  703   /// [indicatorSize] is [TabBarIndicatorSize.label].
  704   ///
  705   /// The selected tab's location appearance can be refined further with
  706   /// the [indicatorColor], [indicatorWeight], [indicatorPadding], and
  707   /// [indicator] properties.
  708   final TabBarIndicatorSize indicatorSize;
  709 
  710   /// The color of selected tab labels.
  711   ///
  712   /// Unselected tab labels are rendered with the same color rendered at 70%
  713   /// opacity unless [unselectedLabelColor] is non-null.
  714   ///
  715   /// If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s
  716   /// bodyText1 text color is used.
  717   final Color labelColor;
  718 
  719   /// The color of unselected tab labels.
  720   ///
  721   /// If this property is null, unselected tab labels are rendered with the
  722   /// [labelColor] with 70% opacity.
  723   final Color unselectedLabelColor;
  724 
  725   /// The text style of the selected tab labels.
  726   ///
  727   /// If [unselectedLabelStyle] is null, then this text style will be used for
  728   /// both selected and unselected label styles.
  729   ///
  730   /// If this property is null, then the text style of the
  731   /// [ThemeData.primaryTextTheme]'s bodyText1 definition is used.
  732   final TextStyle labelStyle;
  733 
  734   /// The padding added to each of the tab labels.
  735   ///
  736   /// If this property is null, then kTabLabelPadding is used.
  737   final EdgeInsetsGeometry labelPadding;
  738 
  739   /// The text style of the unselected tab labels.
  740   ///
  741   /// If this property is null, then the [labelStyle] value is used. If [labelStyle]
  742   /// is null, then the text style of the [ThemeData.primaryTextTheme]'s
  743   /// bodyText1 definition is used.
  744   final TextStyle unselectedLabelStyle;
  745 
  746   /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  747   final DragStartBehavior dragStartBehavior;
  748 
  749   /// The cursor for a mouse pointer when it enters or is hovering over the
  750   /// individual tab widgets.
  751   ///
  752   /// If this property is null, [SystemMouseCursors.click] will be used.
  753   final MouseCursor mouseCursor;
  754 
  755   /// An optional callback that's called when the [TabBar] is tapped.
  756   ///
  757   /// The callback is applied to the index of the tab where the tap occurred.
  758   ///
  759   /// This callback has no effect on the default handling of taps. It's for
  760   /// applications that want to do a little extra work when a tab is tapped,
  761   /// even if the tap doesn't change the TabController's index. TabBar [onTap]
  762   /// callbacks should not make changes to the TabController since that would
  763   /// interfere with the default tap handler.
  764   final ValueChanged<int> onTap;
  765 
  766   /// How the [TabBar]'s scroll view should respond to user input.
  767   ///
  768   /// For example, determines how the scroll view continues to animate after the
  769   /// user stops dragging the scroll view.
  770   ///
  771   /// Defaults to matching platform conventions.
  772   final ScrollPhysics physics;
  773 
  774   /// A size whose height depends on if the tabs have both icons and text.
  775   ///
  776   /// [AppBar] uses this size to compute its own preferred size.
  777   @override
  778   Size get preferredSize {
  779     for (final Widget item in tabs) {
  780       if (item is Tab) {
  781         final Tab tab = item;
  782         if ((tab.text != null || tab.child != null) && tab.icon != null)
  783           return Size.fromHeight(_kTextAndIconTabHeight + indicatorWeight);
  784       }
  785     }
  786     return Size.fromHeight(_kTabHeight + indicatorWeight);
  787   }
  788 
  789   @override
  790   _TabBarState createState() => _TabBarState();
  791 }
  792 
  793 class _TabBarState extends State<TabBar> {
  794   ScrollController _scrollController;
  795   TabController _controller;
  796   _IndicatorPainter _indicatorPainter;
  797   int _currentIndex;
  798   double _tabStripWidth;
  799   List<GlobalKey> _tabKeys;
  800 
  801   @override
  802   void initState() {
  803     super.initState();
  804     // If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
  805     // the width of tab widget i. See _IndicatorPainter.indicatorRect().
  806     _tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList();
  807   }
  808 
  809   Decoration get _indicator {
  810     if (widget.indicator != null)
  811       return widget.indicator;
  812     final TabBarTheme tabBarTheme = TabBarTheme.of(context);
  813     if (tabBarTheme.indicator != null)
  814       return tabBarTheme.indicator;
  815 
  816     Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
  817     // ThemeData tries to avoid this by having indicatorColor avoid being the
  818     // primaryColor. However, it's possible that the tab bar is on a
  819     // Material that isn't the primaryColor. In that case, if the indicator
  820     // color ends up matching the material's color, then this overrides it.
  821     // When that happens, automatic transitions of the theme will likely look
  822     // ugly as the indicator color suddenly snaps to white at one end, but it's
  823     // not clear how to avoid that any further.
  824     //
  825     // The material's color might be null (if it's a transparency). In that case
  826     // there's no good way for us to find out what the color is so we don't.
  827     if (color.value == Material.of(context).color?.value)
  828       color = Colors.white;
  829 
  830     return UnderlineTabIndicator(
  831       insets: widget.indicatorPadding,
  832       borderSide: BorderSide(
  833         width: widget.indicatorWeight,
  834         color: color,
  835       ),
  836     );
  837   }
  838 
  839   // If the TabBar is rebuilt with a new tab controller, the caller should
  840   // dispose the old one. In that case the old controller's animation will be
  841   // null and should not be accessed.
  842   bool get _controllerIsValid => _controller?.animation != null;
  843 
  844   void _updateTabController() {
  845     final TabController newController = widget.controller ?? DefaultTabController.of(context);
  846     assert(() {
  847       if (newController == null) {
  848         throw FlutterError(
  849           'No TabController for ${widget.runtimeType}.\n'
  850           'When creating a ${widget.runtimeType}, you must either provide an explicit '
  851           'TabController using the "controller" property, or you must ensure that there '
  852           'is a DefaultTabController above the ${widget.runtimeType}.\n'
  853           'In this case, there was neither an explicit controller nor a default controller.'
  854         );
  855       }
  856       return true;
  857     }());
  858 
  859     if (newController == _controller)
  860       return;
  861 
  862     if (_controllerIsValid) {
  863       _controller.animation.removeListener(_handleTabControllerAnimationTick);
  864       _controller.removeListener(_handleTabControllerTick);
  865     }
  866     _controller = newController;
  867     if (_controller != null) {
  868       _controller.animation.addListener(_handleTabControllerAnimationTick);
  869       _controller.addListener(_handleTabControllerTick);
  870       _currentIndex = _controller.index;
  871     }
  872   }
  873 
  874   void _initIndicatorPainter() {
  875     _indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
  876       controller: _controller,
  877       indicator: _indicator,
  878       indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,
  879       tabKeys: _tabKeys,
  880       old: _indicatorPainter,
  881     );
  882   }
  883 
  884   @override
  885   void didChangeDependencies() {
  886     super.didChangeDependencies();
  887     assert(debugCheckHasMaterial(context));
  888     _updateTabController();
  889     _initIndicatorPainter();
  890   }
  891 
  892   @override
  893   void didUpdateWidget(TabBar oldWidget) {
  894     super.didUpdateWidget(oldWidget);
  895     if (widget.controller != oldWidget.controller) {
  896       _updateTabController();
  897       _initIndicatorPainter();
  898     } else if (widget.indicatorColor != oldWidget.indicatorColor ||
  899         widget.indicatorWeight != oldWidget.indicatorWeight ||
  900         widget.indicatorSize != oldWidget.indicatorSize ||
  901         widget.indicator != oldWidget.indicator) {
  902       _initIndicatorPainter();
  903     }
  904 
  905     if (widget.tabs.length > oldWidget.tabs.length) {
  906       final int delta = widget.tabs.length - oldWidget.tabs.length;
  907       _tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
  908     } else if (widget.tabs.length < oldWidget.tabs.length) {
  909       _tabKeys.removeRange(widget.tabs.length, oldWidget.tabs.length);
  910     }
  911   }
  912 
  913   @override
  914   void dispose() {
  915     _indicatorPainter.dispose();
  916     if (_controllerIsValid) {
  917       _controller.animation.removeListener(_handleTabControllerAnimationTick);
  918       _controller.removeListener(_handleTabControllerTick);
  919     }
  920     _controller = null;
  921     // We don't own the _controller Animation, so it's not disposed here.
  922     super.dispose();
  923   }
  924 
  925   int get maxTabIndex => _indicatorPainter.maxTabIndex;
  926 
  927   double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) {
  928     if (!widget.isScrollable)
  929       return 0.0;
  930     double tabCenter = _indicatorPainter.centerOf(index);
  931     switch (Directionality.of(context)) {
  932       case TextDirection.rtl:
  933         tabCenter = _tabStripWidth - tabCenter;
  934         break;
  935       case TextDirection.ltr:
  936         break;
  937     }
  938     return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent) as double;
  939   }
  940 
  941   double _tabCenteredScrollOffset(int index) {
  942     final ScrollPosition position = _scrollController.position;
  943     return _tabScrollOffset(index, position.viewportDimension, position.minScrollExtent, position.maxScrollExtent);
  944   }
  945 
  946   double _initialScrollOffset(double viewportWidth, double minExtent, double maxExtent) {
  947     return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
  948   }
  949 
  950   void _scrollToCurrentIndex() {
  951     final double offset = _tabCenteredScrollOffset(_currentIndex);
  952     _scrollController.animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
  953   }
  954 
  955   void _scrollToControllerValue() {
  956     final double leadingPosition = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
  957     final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
  958     final double trailingPosition = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null;
  959 
  960     final double index = _controller.index.toDouble();
  961     final double value = _controller.animation.value;
  962     double offset;
  963     if (value == index - 1.0)
  964       offset = leadingPosition ?? middlePosition;
  965     else if (value == index + 1.0)
  966       offset = trailingPosition ?? middlePosition;
  967     else if (value == index)
  968       offset = middlePosition;
  969     else if (value < index)
  970       offset = leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value);
  971     else
  972       offset = trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index);
  973 
  974     _scrollController.jumpTo(offset);
  975   }
  976 
  977   void _handleTabControllerAnimationTick() {
  978     assert(mounted);
  979     if (!_controller.indexIsChanging && widget.isScrollable) {
  980       // Sync the TabBar's scroll position with the TabBarView's PageView.
  981       _currentIndex = _controller.index;
  982       _scrollToControllerValue();
  983     }
  984   }
  985 
  986   void _handleTabControllerTick() {
  987     if (_controller.index != _currentIndex) {
  988       _currentIndex = _controller.index;
  989       if (widget.isScrollable)
  990         _scrollToCurrentIndex();
  991     }
  992     setState(() {
  993       // Rebuild the tabs after a (potentially animated) index change
  994       // has completed.
  995     });
  996   }
  997 
  998   // Called each time layout completes.
  999   void _saveTabOffsets(List<double> tabOffsets, TextDirection textDirection, double width) {
 1000     _tabStripWidth = width;
 1001     _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
 1002   }
 1003 
 1004   void _handleTap(int index) {
 1005     assert(index >= 0 && index < widget.tabs.length);
 1006     _controller.animateTo(index);
 1007     if (widget.onTap != null) {
 1008       widget.onTap(index);
 1009     }
 1010   }
 1011 
 1012   Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) {
 1013     return _TabStyle(
 1014       animation: animation,
 1015       selected: selected,
 1016       labelColor: widget.labelColor,
 1017       unselectedLabelColor: widget.unselectedLabelColor,
 1018       labelStyle: widget.labelStyle,
 1019       unselectedLabelStyle: widget.unselectedLabelStyle,
 1020       child: child,
 1021     );
 1022   }
 1023 
 1024   @override
 1025   Widget build(BuildContext context) {
 1026     assert(debugCheckHasMaterialLocalizations(context));
 1027     assert(() {
 1028       if (_controller.length != widget.tabs.length) {
 1029         throw FlutterError(
 1030           "Controller's length property (${_controller.length}) does not match the "
 1031           "number of tabs (${widget.tabs.length}) present in TabBar's tabs property."
 1032         );
 1033       }
 1034       return true;
 1035     }());
 1036     final MaterialLocalizations localizations = MaterialLocalizations.of(context);
 1037     if (_controller.length == 0) {
 1038       return Container(
 1039         height: _kTabHeight + widget.indicatorWeight,
 1040       );
 1041     }
 1042 
 1043     final TabBarTheme tabBarTheme = TabBarTheme.of(context);
 1044 
 1045     final List<Widget> wrappedTabs = List<Widget>(widget.tabs.length);
 1046     for (int i = 0; i < widget.tabs.length; i += 1) {
 1047       wrappedTabs[i] = Center(
 1048         heightFactor: 1.0,
 1049         child: Padding(
 1050           padding: widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding,
 1051           child: KeyedSubtree(
 1052             key: _tabKeys[i],
 1053             child: widget.tabs[i],
 1054           ),
 1055         ),
 1056       );
 1057 
 1058     }
 1059 
 1060     // If the controller was provided by DefaultTabController and we're part
 1061     // of a Hero (typically the AppBar), then we will not be able to find the
 1062     // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213.
 1063     if (_controller != null) {
 1064       final int previousIndex = _controller.previousIndex;
 1065 
 1066       if (_controller.indexIsChanging) {
 1067         // The user tapped on a tab, the tab controller's animation is running.
 1068         assert(_currentIndex != previousIndex);
 1069         final Animation<double> animation = _ChangeAnimation(_controller);
 1070         wrappedTabs[_currentIndex] = _buildStyledTab(wrappedTabs[_currentIndex], true, animation);
 1071         wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
 1072       } else {
 1073         // The user is dragging the TabBarView's PageView left or right.
 1074         final int tabIndex = _currentIndex;
 1075         final Animation<double> centerAnimation = _DragAnimation(_controller, tabIndex);
 1076         wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
 1077         if (_currentIndex > 0) {
 1078           final int tabIndex = _currentIndex - 1;
 1079           final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex));
 1080           wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation);
 1081         }
 1082         if (_currentIndex < widget.tabs.length - 1) {
 1083           final int tabIndex = _currentIndex + 1;
 1084           final Animation<double> nextAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex));
 1085           wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation);
 1086         }
 1087       }
 1088     }
 1089 
 1090     // Add the tap handler to each tab. If the tab bar is not scrollable,
 1091     // then give all of the tabs equal flexibility so that they each occupy
 1092     // the same share of the tab bar's overall width.
 1093     final int tabCount = widget.tabs.length;
 1094     for (int index = 0; index < tabCount; index += 1) {
 1095       wrappedTabs[index] = InkWell(
 1096         mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click,
 1097         onTap: () { _handleTap(index); },
 1098         child: Padding(
 1099           padding: EdgeInsets.only(bottom: widget.indicatorWeight),
 1100           child: Stack(
 1101             children: <Widget>[
 1102               wrappedTabs[index],
 1103               Semantics(
 1104                 selected: index == _currentIndex,
 1105                 label: localizations.tabLabel(tabIndex: index + 1, tabCount: tabCount),
 1106               ),
 1107             ],
 1108           ),
 1109         ),
 1110       );
 1111       if (!widget.isScrollable)
 1112         wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
 1113     }
 1114 
 1115     Widget tabBar = CustomPaint(
 1116       painter: _indicatorPainter,
 1117       child: _TabStyle(
 1118         animation: kAlwaysDismissedAnimation,
 1119         selected: false,
 1120         labelColor: widget.labelColor,
 1121         unselectedLabelColor: widget.unselectedLabelColor,
 1122         labelStyle: widget.labelStyle,
 1123         unselectedLabelStyle: widget.unselectedLabelStyle,
 1124         child: _TabLabelBar(
 1125           onPerformLayout: _saveTabOffsets,
 1126           children: wrappedTabs,
 1127         ),
 1128       ),
 1129     );
 1130 
 1131     if (widget.isScrollable) {
 1132       _scrollController ??= _TabBarScrollController(this);
 1133       tabBar = SingleChildScrollView(
 1134         dragStartBehavior: widget.dragStartBehavior,
 1135         scrollDirection: Axis.horizontal,
 1136         controller: _scrollController,
 1137         physics: widget.physics,
 1138         child: tabBar,
 1139       );
 1140     }
 1141 
 1142     return tabBar;
 1143   }
 1144 }
 1145 
 1146 /// A page view that displays the widget which corresponds to the currently
 1147 /// selected tab.
 1148 ///
 1149 /// This widget is typically used in conjunction with a [TabBar].
 1150 ///
 1151 /// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
 1152 ///
 1153 /// If a [TabController] is not provided, then there must be a [DefaultTabController]
 1154 /// ancestor.
 1155 ///
 1156 /// The tab controller's [TabController.length] must equal the length of the
 1157 /// [children] list and the length of the [TabBar.tabs] list.
 1158 ///
 1159 /// To see a sample implementation, visit the [TabController] documentation.
 1160 class TabBarView extends StatefulWidget {
 1161   /// Creates a page view with one child per tab.
 1162   ///
 1163   /// The length of [children] must be the same as the [controller]'s length.
 1164   const TabBarView({
 1165     Key key,
 1166     @required this.children,
 1167     this.controller,
 1168     this.physics,
 1169     this.dragStartBehavior = DragStartBehavior.start,
 1170   }) : assert(children != null),
 1171        assert(dragStartBehavior != null),
 1172        super(key: key);
 1173 
 1174   /// This widget's selection and animation state.
 1175   ///
 1176   /// If [TabController] is not provided, then the value of [DefaultTabController.of]
 1177   /// will be used.
 1178   final TabController controller;
 1179 
 1180   /// One widget per tab.
 1181   ///
 1182   /// Its length must match the length of the [TabBar.tabs]
 1183   /// list, as well as the [controller]'s [TabController.length].
 1184   final List<Widget> children;
 1185 
 1186   /// How the page view should respond to user input.
 1187   ///
 1188   /// For example, determines how the page view continues to animate after the
 1189   /// user stops dragging the page view.
 1190   ///
 1191   /// The physics are modified to snap to page boundaries using
 1192   /// [PageScrollPhysics] prior to being used.
 1193   ///
 1194   /// Defaults to matching platform conventions.
 1195   final ScrollPhysics physics;
 1196 
 1197   /// {@macro flutter.widgets.scrollable.dragStartBehavior}
 1198   final DragStartBehavior dragStartBehavior;
 1199 
 1200   @override
 1201   _TabBarViewState createState() => _TabBarViewState();
 1202 }
 1203 
 1204 class _TabBarViewState extends State<TabBarView> {
 1205   TabController _controller;
 1206   PageController _pageController;
 1207   List<Widget> _children;
 1208   List<Widget> _childrenWithKey;
 1209   int _currentIndex;
 1210   int _warpUnderwayCount = 0;
 1211 
 1212   // If the TabBarView is rebuilt with a new tab controller, the caller should
 1213   // dispose the old one. In that case the old controller's animation will be
 1214   // null and should not be accessed.
 1215   bool get _controllerIsValid => _controller?.animation != null;
 1216 
 1217   void _updateTabController() {
 1218     final TabController newController = widget.controller ?? DefaultTabController.of(context);
 1219     assert(() {
 1220       if (newController == null) {
 1221         throw FlutterError(
 1222           'No TabController for ${widget.runtimeType}.\n'
 1223           'When creating a ${widget.runtimeType}, you must either provide an explicit '
 1224           'TabController using the "controller" property, or you must ensure that there '
 1225           'is a DefaultTabController above the ${widget.runtimeType}.\n'
 1226           'In this case, there was neither an explicit controller nor a default controller.'
 1227         );
 1228       }
 1229       return true;
 1230     }());
 1231 
 1232     if (newController == _controller)
 1233       return;
 1234 
 1235     if (_controllerIsValid)
 1236       _controller.animation.removeListener(_handleTabControllerAnimationTick);
 1237     _controller = newController;
 1238     if (_controller != null)
 1239       _controller.animation.addListener(_handleTabControllerAnimationTick);
 1240   }
 1241 
 1242   @override
 1243   void initState() {
 1244     super.initState();
 1245     _updateChildren();
 1246   }
 1247 
 1248   @override
 1249   void didChangeDependencies() {
 1250     super.didChangeDependencies();
 1251     _updateTabController();
 1252     _currentIndex = _controller?.index;
 1253     _pageController = PageController(initialPage: _currentIndex ?? 0);
 1254   }
 1255 
 1256   @override
 1257   void didUpdateWidget(TabBarView oldWidget) {
 1258     super.didUpdateWidget(oldWidget);
 1259     if (widget.controller != oldWidget.controller)
 1260       _updateTabController();
 1261     if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
 1262       _updateChildren();
 1263   }
 1264 
 1265   @override
 1266   void dispose() {
 1267     if (_controllerIsValid)
 1268       _controller.animation.removeListener(_handleTabControllerAnimationTick);
 1269     _controller = null;
 1270     // We don't own the _controller Animation, so it's not disposed here.
 1271     super.dispose();
 1272   }
 1273 
 1274   void _updateChildren() {
 1275     _children = widget.children;
 1276     _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
 1277   }
 1278 
 1279   void _handleTabControllerAnimationTick() {
 1280     if (_warpUnderwayCount > 0 || !_controller.indexIsChanging)
 1281       return; // This widget is driving the controller's animation.
 1282 
 1283     if (_controller.index != _currentIndex) {
 1284       _currentIndex = _controller.index;
 1285       _warpToCurrentIndex();
 1286     }
 1287   }
 1288 
 1289   Future<void> _warpToCurrentIndex() async {
 1290     if (!mounted)
 1291       return Future<void>.value();
 1292 
 1293     if (_pageController.page == _currentIndex.toDouble())
 1294       return Future<void>.value();
 1295 
 1296     final int previousIndex = _controller.previousIndex;
 1297     if ((_currentIndex - previousIndex).abs() == 1) {
 1298       _warpUnderwayCount += 1;
 1299       await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
 1300       _warpUnderwayCount -= 1;
 1301       return Future<void>.value();
 1302     }
 1303 
 1304     assert((_currentIndex - previousIndex).abs() > 1);
 1305     final int initialPage = _currentIndex > previousIndex
 1306         ? _currentIndex - 1
 1307         : _currentIndex + 1;
 1308     final List<Widget> originalChildren = _childrenWithKey;
 1309     setState(() {
 1310       _warpUnderwayCount += 1;
 1311 
 1312       _childrenWithKey = List<Widget>.from(_childrenWithKey, growable: false);
 1313       final Widget temp = _childrenWithKey[initialPage];
 1314       _childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
 1315       _childrenWithKey[previousIndex] = temp;
 1316     });
 1317     _pageController.jumpToPage(initialPage);
 1318 
 1319     await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
 1320     if (!mounted)
 1321       return Future<void>.value();
 1322     setState(() {
 1323       _warpUnderwayCount -= 1;
 1324       if (widget.children != _children) {
 1325         _updateChildren();
 1326       } else {
 1327         _childrenWithKey = originalChildren;
 1328       }
 1329     });
 1330   }
 1331 
 1332   // Called when the PageView scrolls
 1333   bool _handleScrollNotification(ScrollNotification notification) {
 1334     if (_warpUnderwayCount > 0)
 1335       return false;
 1336 
 1337     if (notification.depth != 0)
 1338       return false;
 1339 
 1340     _warpUnderwayCount += 1;
 1341     if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) {
 1342       if ((_pageController.page - _controller.index).abs() > 1.0) {
 1343         _controller.index = _pageController.page.floor();
 1344         _currentIndex =_controller.index;
 1345       }
 1346       _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0) as double;
 1347     } else if (notification is ScrollEndNotification) {
 1348       _controller.index = _pageController.page.round();
 1349       _currentIndex = _controller.index;
 1350       if (!_controller.indexIsChanging)
 1351         _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0) as double;
 1352     }
 1353     _warpUnderwayCount -= 1;
 1354 
 1355     return false;
 1356   }
 1357 
 1358   @override
 1359   Widget build(BuildContext context) {
 1360     assert(() {
 1361       if (_controller.length != widget.children.length) {
 1362         throw FlutterError(
 1363           "Controller's length property (${_controller.length}) does not match the "
 1364           "number of tabs (${widget.children.length}) present in TabBar's tabs property."
 1365         );
 1366       }
 1367       return true;
 1368     }());
 1369     return NotificationListener<ScrollNotification>(
 1370       onNotification: _handleScrollNotification,
 1371       child: PageView(
 1372         dragStartBehavior: widget.dragStartBehavior,
 1373         controller: _pageController,
 1374         physics: widget.physics == null
 1375           ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
 1376           : const PageScrollPhysics().applyTo(widget.physics),
 1377         children: _childrenWithKey,
 1378       ),
 1379     );
 1380   }
 1381 }
 1382 
 1383 /// Displays a single circle with the specified border and background colors.
 1384 ///
 1385 /// Used by [TabPageSelector] to indicate the selected page.
 1386 class TabPageSelectorIndicator extends StatelessWidget {
 1387   /// Creates an indicator used by [TabPageSelector].
 1388   ///
 1389   /// The [backgroundColor], [borderColor], and [size] parameters must not be null.
 1390   const TabPageSelectorIndicator({
 1391     Key key,
 1392     @required this.backgroundColor,
 1393     @required this.borderColor,
 1394     @required this.size,
 1395   }) : assert(backgroundColor != null),
 1396        assert(borderColor != null),
 1397        assert(size != null),
 1398        super(key: key);
 1399 
 1400   /// The indicator circle's background color.
 1401   final Color backgroundColor;
 1402 
 1403   /// The indicator circle's border color.
 1404   final Color borderColor;
 1405 
 1406   /// The indicator circle's diameter.
 1407   final double size;
 1408 
 1409   @override
 1410   Widget build(BuildContext context) {
 1411     return Container(
 1412       width: size,
 1413       height: size,
 1414       margin: const EdgeInsets.all(4.0),
 1415       decoration: BoxDecoration(
 1416         color: backgroundColor,
 1417         border: Border.all(color: borderColor),
 1418         shape: BoxShape.circle,
 1419       ),
 1420     );
 1421   }
 1422 }
 1423 
 1424 /// Displays a row of small circular indicators, one per tab.
 1425 ///
 1426 /// The selected tab's indicator is highlighted. Often used in conjunction with
 1427 /// a [TabBarView].
 1428 ///
 1429 /// If a [TabController] is not provided, then there must be a
 1430 /// [DefaultTabController] ancestor.
 1431 class TabPageSelector extends StatelessWidget {
 1432   /// Creates a compact widget that indicates which tab has been selected.
 1433   const TabPageSelector({
 1434     Key key,
 1435     this.controller,
 1436     this.indicatorSize = 12.0,
 1437     this.color,
 1438     this.selectedColor,
 1439   }) : assert(indicatorSize != null && indicatorSize > 0.0),
 1440        super(key: key);
 1441 
 1442   /// This widget's selection and animation state.
 1443   ///
 1444   /// If [TabController] is not provided, then the value of
 1445   /// [DefaultTabController.of] will be used.
 1446   final TabController controller;
 1447 
 1448   /// The indicator circle's diameter (the default value is 12.0).
 1449   final double indicatorSize;
 1450 
 1451   /// The indicator circle's fill color for unselected pages.
 1452   ///
 1453   /// If this parameter is null, then the indicator is filled with [Colors.transparent].
 1454   final Color color;
 1455 
 1456   /// The indicator circle's fill color for selected pages and border color
 1457   /// for all indicator circles.
 1458   ///
 1459   /// If this parameter is null, then the indicator is filled with the theme's
 1460   /// accent color, [ThemeData.accentColor].
 1461   final Color selectedColor;
 1462 
 1463   Widget _buildTabIndicator(
 1464     int tabIndex,
 1465     TabController tabController,
 1466     ColorTween selectedColorTween,
 1467     ColorTween previousColorTween,
 1468   ) {
 1469     Color background;
 1470     if (tabController.indexIsChanging) {
 1471       // The selection's animation is animating from previousValue to value.
 1472       final double t = 1.0 - _indexChangeProgress(tabController);
 1473       if (tabController.index == tabIndex)
 1474         background = selectedColorTween.lerp(t);
 1475       else if (tabController.previousIndex == tabIndex)
 1476         background = previousColorTween.lerp(t);
 1477       else
 1478         background = selectedColorTween.begin;
 1479     } else {
 1480       // The selection's offset reflects how far the TabBarView has / been dragged
 1481       // to the previous page (-1.0 to 0.0) or the next page (0.0 to 1.0).
 1482       final double offset = tabController.offset;
 1483       if (tabController.index == tabIndex) {
 1484         background = selectedColorTween.lerp(1.0 - offset.abs());
 1485       } else if (tabController.index == tabIndex - 1 && offset > 0.0) {
 1486         background = selectedColorTween.lerp(offset);
 1487       } else if (tabController.index == tabIndex + 1 && offset < 0.0) {
 1488         background = selectedColorTween.lerp(-offset);
 1489       } else {
 1490         background = selectedColorTween.begin;
 1491       }
 1492     }
 1493     return TabPageSelectorIndicator(
 1494       backgroundColor: background,
 1495       borderColor: selectedColorTween.end,
 1496       size: indicatorSize,
 1497     );
 1498   }
 1499 
 1500   @override
 1501   Widget build(BuildContext context) {
 1502     final Color fixColor = color ?? Colors.transparent;
 1503     final Color fixSelectedColor = selectedColor ?? Theme.of(context).accentColor;
 1504     final ColorTween selectedColorTween = ColorTween(begin: fixColor, end: fixSelectedColor);
 1505     final ColorTween previousColorTween = ColorTween(begin: fixSelectedColor, end: fixColor);
 1506     final TabController tabController = controller ?? DefaultTabController.of(context);
 1507     assert(() {
 1508       if (tabController == null) {
 1509         throw FlutterError(
 1510           'No TabController for $runtimeType.\n'
 1511           'When creating a $runtimeType, you must either provide an explicit TabController '
 1512           'using the "controller" property, or you must ensure that there is a '
 1513           'DefaultTabController above the $runtimeType.\n'
 1514           'In this case, there was neither an explicit controller nor a default controller.'
 1515         );
 1516       }
 1517       return true;
 1518     }());
 1519     final Animation<double> animation = CurvedAnimation(
 1520       parent: tabController.animation,
 1521       curve: Curves.fastOutSlowIn,
 1522     );
 1523     return AnimatedBuilder(
 1524       animation: animation,
 1525       builder: (BuildContext context, Widget child) {
 1526         return Semantics(
 1527           label: 'Page ${tabController.index + 1} of ${tabController.length}',
 1528           child: Row(
 1529             mainAxisSize: MainAxisSize.min,
 1530             children: List<Widget>.generate(tabController.length, (int tabIndex) {
 1531               return _buildTabIndicator(tabIndex, tabController, selectedColorTween, previousColorTween);
 1532             }).toList(),
 1533           ),
 1534         );
 1535       },
 1536     );
 1537   }
 1538 }