{"id":337,"date":"2022-03-07T10:27:13","date_gmt":"2022-03-07T02:27:13","guid":{"rendered":"https:\/\/blog.iichen.cn\/?p=337"},"modified":"2022-03-07T10:27:13","modified_gmt":"2022-03-07T02:27:13","slug":"flutter-tabbar%e6%8c%87%e7%a4%ba%e5%99%a8","status":"publish","type":"post","link":"https:\/\/iichen.cn\/?p=337","title":{"rendered":"Flutter-Tabbar\u6307\u793a\u5668"},"content":{"rendered":"<h4>\u4f7f\u7528<\/h4>\n<pre><code class=\"language-dart line-numbers\">@override\nWidget build(BuildContext context) {\nreturn Scaffold(\n  appBar: AppBar(\n    title: Text('title'.tr),\n    bottom: HjtTabBar(\n      tabs: const [Tab(text: \"\u97f3\u4e50\"), Tab(text: \"\u52a8\u6001\"), Tab(text: \"\u8bed\u6587\")],\n      \/\/ labelPadding: EdgeInsets.symmetric(horizontal: 8),\n      controller: _controller,\n      indicatorSize: TabBarIndicatorSize.label,\n      \/\/ isScrollable: true,\n      padding: EdgeInsets.zero,\n      indicator: RRecTabIndicator(\n          radius: 12\n      ),\n      indicatorMinWidth: 6,\n      \/\/ indicator: const BoxDecoration()\n    ),\n  ),\n  body: Container(\n    child: TabBarView(\n      controller: _controller,\n      children: const [\n        Center(\n          child: Text(\"\u97f3\u4e50\"),\n        ),\n        Center(\n          child: Text(\"\u52a8\u6001\"),\n        ),\n        Center(\n          child: Text(\"\u8bed\u6587\"),\n        ),\n      ],\n    ),\n  ),\n  floatingActionButton: FloatingActionButton(\n    onPressed: () {\n\n    },\n  ),\n);\n<\/code><\/pre>\n<h4>\u6e90\u7801<\/h4>\n<pre><code class=\"language-dart line-numbers\">\/\/ Copyright 2014 The Flutter Authors. All rights reserved.\n\/\/ Use of this source code is governed by a BSD-style license that can be\n\/\/ found in the LICENSE file.\n\nimport 'dart:math' as math;\nimport 'dart:ui' show lerpDouble;\n\nimport 'package:flutter\/foundation.dart';\nimport 'package:flutter\/gestures.dart' show DragStartBehavior;\nimport 'package:flutter\/material.dart';\nimport 'package:flutter\/rendering.dart';\nimport 'package:flutter\/widgets.dart';\n\nconst double _kTabHeight = 46.0;\nconst double _kTextAndIconTabHeight = 72.0;\n\nclass _TabStyle extends AnimatedWidget {\n  const _TabStyle({\n    Key? key,\n    required Animation&lt;double&gt; animation,\n    required this.selected,\n    required this.labelColor,\n    required this.unselectedLabelColor,\n    required this.labelStyle,\n    required this.unselectedLabelStyle,\n    required this.child,\n  }) : super(key: key, listenable: animation);\n\n  final TextStyle? labelStyle;\n  final TextStyle? unselectedLabelStyle;\n  final bool selected;\n  final Color? labelColor;\n  final Color? unselectedLabelColor;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final ThemeData themeData = Theme.of(context);\n    final TabBarTheme tabBarTheme = TabBarTheme.of(context);\n    final Animation&lt;double&gt; animation = listenable as Animation&lt;double&gt;;\n\n    \/\/ To enable TextStyle.lerp(style1, style2, value), both styles must have\n    \/\/ the same value of inherit. Force that to be inherit=true here.\n    final TextStyle defaultStyle = (labelStyle\n        ?? tabBarTheme.labelStyle\n        ?? themeData.primaryTextTheme.bodyText1!\n    ).copyWith(inherit: true);\n    final TextStyle defaultUnselectedStyle = (unselectedLabelStyle\n        ?? tabBarTheme.unselectedLabelStyle\n        ?? labelStyle\n        ?? themeData.primaryTextTheme.bodyText1!\n    ).copyWith(inherit: true);\n    final TextStyle textStyle = selected\n        ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)!\n        : TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value)!;\n\n    final Color selectedColor = labelColor\n        ?? tabBarTheme.labelColor\n        ?? themeData.primaryTextTheme.bodyText1!.color!;\n    final Color unselectedColor = unselectedLabelColor\n        ?? tabBarTheme.unselectedLabelColor\n        ?? selectedColor.withAlpha(0xB2); \/\/ 70% alpha\n    final Color color = selected\n        ? Color.lerp(selectedColor, unselectedColor, animation.value)!\n        : Color.lerp(unselectedColor, selectedColor, animation.value)!;\n\n    return DefaultTextStyle(\n      style: textStyle.copyWith(color: color),\n      child: IconTheme.merge(\n        data: IconThemeData(\n          size: 24.0,\n          color: color,\n        ),\n        child: child,\n      ),\n    );\n  }\n}\n\ntypedef _LayoutCallback = void Function(List&lt;double&gt; xOffsets, TextDirection textDirection, double width);\n\nclass _TabLabelBarRenderer extends RenderFlex {\n  _TabLabelBarRenderer({\n    List&lt;RenderBox&gt;? children,\n    required Axis direction,\n    required MainAxisSize mainAxisSize,\n    required MainAxisAlignment mainAxisAlignment,\n    required CrossAxisAlignment crossAxisAlignment,\n    required TextDirection textDirection,\n    required VerticalDirection verticalDirection,\n    required this.onPerformLayout,\n  }) : assert(onPerformLayout != null),\n        assert(textDirection != null),\n        super(\n        children: children,\n        direction: direction,\n        mainAxisSize: mainAxisSize,\n        mainAxisAlignment: mainAxisAlignment,\n        crossAxisAlignment: crossAxisAlignment,\n        textDirection: textDirection,\n        verticalDirection: verticalDirection,\n      );\n\n  _LayoutCallback onPerformLayout;\n\n  @override\n  void performLayout() {\n    super.performLayout();\n    \/\/ xOffsets will contain childCount+1 values, giving the offsets of the\n    \/\/ leading edge of the first tab as the first value, of the leading edge of\n    \/\/ the each subsequent tab as each subsequent value, and of the trailing\n    \/\/ edge of the last tab as the last value.\n    RenderBox? child = firstChild;\n    final List&lt;double&gt; xOffsets = &lt;double&gt;[];\n    while (child != null) {\n      final FlexParentData childParentData = child.parentData! as FlexParentData;\n      xOffsets.add(childParentData.offset.dx);\n      assert(child.parentData == childParentData);\n      child = childParentData.nextSibling;\n    }\n    assert(textDirection != null);\n    switch (textDirection!) {\n      case TextDirection.rtl:\n        xOffsets.insert(0, size.width);\n        break;\n      case TextDirection.ltr:\n        xOffsets.add(size.width);\n        break;\n    }\n    onPerformLayout(xOffsets, textDirection!, size.width);\n  }\n}\n\n\/\/ This class and its renderer class only exist to report the widths of the tabs\n\/\/ upon layout. The tab widths are only used at paint time (see _IndicatorPainter)\n\/\/ or in response to input.\nclass _TabLabelBar extends Flex {\n  _TabLabelBar({\n    Key? key,\n    List&lt;Widget&gt; children = const &lt;Widget&gt;[],\n    required this.onPerformLayout,\n  }) : super(\n    key: key,\n    children: children,\n    direction: Axis.horizontal,\n    mainAxisSize: MainAxisSize.max,\n    mainAxisAlignment: MainAxisAlignment.start,\n    crossAxisAlignment: CrossAxisAlignment.center,\n    verticalDirection: VerticalDirection.down,\n  );\n\n  final _LayoutCallback onPerformLayout;\n\n  @override\n  RenderFlex createRenderObject(BuildContext context) {\n    return _TabLabelBarRenderer(\n      direction: direction,\n      mainAxisAlignment: mainAxisAlignment,\n      mainAxisSize: mainAxisSize,\n      crossAxisAlignment: crossAxisAlignment,\n      textDirection: getEffectiveTextDirection(context)!,\n      verticalDirection: verticalDirection,\n      onPerformLayout: onPerformLayout,\n    );\n  }\n\n  @override\n  void updateRenderObject(BuildContext context, _TabLabelBarRenderer renderObject) {\n    super.updateRenderObject(context, renderObject);\n    renderObject.onPerformLayout = onPerformLayout;\n  }\n}\n\ndouble _indexChangeProgress(TabController controller) {\n  final double controllerValue = controller.animation!.value;\n  final double previousIndex = controller.previousIndex.toDouble();\n  final double currentIndex = controller.index.toDouble();\n\n  \/\/ The controller's offset is changing because the user is dragging the\n  \/\/ TabBarView's PageView to the left or right.\n  if (!controller.indexIsChanging)\n    return (currentIndex - controllerValue).abs().clamp(0.0, 1.0);\n\n  \/\/ The TabController animation's value is changing from previousIndex to currentIndex.\n  return (controllerValue - currentIndex).abs() \/ (currentIndex - previousIndex).abs();\n}\n\n\n\/\/ \u590d\u5236\u7cfb\u7edf\u6e90\u7801 \u4e3b\u8981\u6539\u4e00\u4e0b _IndicatorPainter\u5185\u7684 indicatorRect \u548c paint\u65b9\u6cd5\nclass _IndicatorPainter extends CustomPainter {\n  _IndicatorPainter({\n    required this.controller,\n    required this.indicator,\n    required this.indicatorSize,\n    required this.tabKeys,\n    required _IndicatorPainter? old,\n    required this.indicatorPadding,\n    this.indicatorMinWidth\n  }) : assert(controller != null),\n        assert(indicator != null),\n        super(repaint: controller.animation) {\n    if (old != null)\n      saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);\n  }\n\n  final TabController controller;\n  final Decoration indicator;\n  final double? indicatorMinWidth;\n  final TabBarIndicatorSize? indicatorSize;\n  final EdgeInsetsGeometry indicatorPadding;\n  final List&lt;GlobalKey&gt; tabKeys;\n\n  \/\/ _currentTabOffsets and _currentTextDirection are set each time TabBar\n  \/\/ layout is completed. These values can be null when TabBar contains no\n  \/\/ tabs, since there are nothing to lay out.\n  List&lt;double&gt;? _currentTabOffsets;\n  TextDirection? _currentTextDirection;\n\n  Rect? _currentRect;\n  BoxPainter? _painter;\n  bool _needsPaint = false;\n  void markNeedsPaint() {\n    _needsPaint = true;\n  }\n\n  void dispose() {\n    _painter?.dispose();\n  }\n\n  void saveTabOffsets(List&lt;double&gt;? tabOffsets, TextDirection? textDirection) {\n    _currentTabOffsets = tabOffsets;\n    _currentTextDirection = textDirection;\n  }\n\n  \/\/ _currentTabOffsets[index] is the offset of the start edge of the tab at index, and\n  \/\/ _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.\n  int get maxTabIndex =&gt; _currentTabOffsets!.length - 2;\n\n  double centerOf(int tabIndex) {\n    assert(_currentTabOffsets != null);\n    assert(_currentTabOffsets!.isNotEmpty);\n    assert(tabIndex &gt;= 0);\n    assert(tabIndex &lt;= maxTabIndex);\n    return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) \/ 2.0;\n  }\n\n  Rect indicatorRect(Size tabBarSize, int tabIndex,int otherTabIndex, double scale) {\n    assert(_currentTabOffsets != null);\n    assert(_currentTextDirection != null);\n    assert(_currentTabOffsets!.isNotEmpty);\n    assert(tabIndex &gt;= 0);\n    assert(tabIndex &lt;= maxTabIndex);\n    double tabLeft, tabRight, tabWidth = 0.0, tabWidth2 = 0.0, minWidth = 0.0, res = 0.0;\n    switch (_currentTextDirection!) {\n      case TextDirection.rtl:\n        tabLeft = _currentTabOffsets![tabIndex + 1];\n        tabRight = _currentTabOffsets![tabIndex];\n        break;\n      case TextDirection.ltr:\n        tabLeft = _currentTabOffsets![tabIndex];\n        tabRight = _currentTabOffsets![tabIndex + 1];\n        break;\n    }\n\n\n    final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection);\n\n    if (indicatorSize == TabBarIndicatorSize.label) {\n      tabWidth = tabKeys[tabIndex].currentContext!.size!.width;\n      RenderBox renderBox = tabKeys[tabIndex].currentContext!.findRenderObject() as RenderBox;\n      RenderBox renderBox2 = tabKeys[otherTabIndex].currentContext!.findRenderObject() as RenderBox;\n      Offset offset = renderBox.localToGlobal(Offset.zero);\n      Offset offset2 = renderBox2.localToGlobal(Offset.zero);\n\n      final double delta = ((tabRight - tabLeft) - tabWidth) \/ 2.0;\n      tabLeft += delta;\n      tabRight -= delta;\n\n      minWidth = indicatorMinWidth??tabWidth;\n      res = scale == 0 ? minWidth : ((offset2.dx - offset.dx - minWidth)).abs() * (scale &lt; 0.5 ? scale : 1 - scale);\n    }\n\n\n    \/\/ final Rect rect = Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height);\n    final Rect rect = Rect.fromLTWH(tabLeft + tabWidth \/ 2 - minWidth \/ 2, 0.0, res &gt; minWidth ? res : minWidth, tabBarSize.height);\n\n    if (!(rect.size &gt;= insets.collapsedSize)) {\n      throw FlutterError(\n        'indicatorPadding insets should be less than Tab Size\\n'\n            'Rect Size : ${rect.size}, Insets: ${insets.toString()}',\n      );\n    }\n    return insets.deflateRect(rect);\n  }\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    _needsPaint = false;\n    _painter ??= indicator.createBoxPainter(markNeedsPaint);\n\n    final double index = controller.index.toDouble();\n    final double value = controller.animation!.value;\n    \/\/ index &gt; value \u52a8\u753b\u6267\u884c\u8d85\u8fc7 50% tab\u5b9e\u9645\u5207\u6362\u5230\u4e0b\u4e00\u4e2a  abs(index - value) \u8868\u793a\u7f29\u653e\u6bd4\u4f8b\n    final bool ltr = index &gt; value;\n    final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex);\n    final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex);\n    final Rect fromRect = indicatorRect(size, from, to, (value - index).abs());\n    final Rect toRect = indicatorRect(size, to, from,(value - index).abs());\n    _currentRect = Rect.lerp(fromRect, toRect, (value - from).abs());\n    assert(_currentRect != null);\n\n    final ImageConfiguration configuration = ImageConfiguration(\n      size: _currentRect!.size,\n      textDirection: _currentTextDirection,\n    );\n    _painter!.paint(canvas, _currentRect!.topLeft, configuration);\n  }\n\n  @override\n  bool shouldRepaint(_IndicatorPainter old) {\n    return _needsPaint\n        || controller != old.controller\n        || indicator != old.indicator\n        || tabKeys.length != old.tabKeys.length\n        || (!listEquals(_currentTabOffsets, old._currentTabOffsets))\n        || _currentTextDirection != old._currentTextDirection;\n  }\n}\n\nclass _ChangeAnimation extends Animation&lt;double&gt; with AnimationWithParentMixin&lt;double&gt; {\n  _ChangeAnimation(this.controller);\n\n  final TabController controller;\n\n  @override\n  Animation&lt;double&gt; get parent =&gt; controller.animation!;\n\n  @override\n  void removeStatusListener(AnimationStatusListener listener) {\n    if (controller.animation != null)\n      super.removeStatusListener(listener);\n  }\n\n  @override\n  void removeListener(VoidCallback listener) {\n    if (controller.animation != null)\n      super.removeListener(listener);\n  }\n\n  @override\n  double get value =&gt; _indexChangeProgress(controller);\n}\n\nclass _DragAnimation extends Animation&lt;double&gt; with AnimationWithParentMixin&lt;double&gt; {\n  _DragAnimation(this.controller, this.index);\n\n  final TabController controller;\n  final int index;\n\n  @override\n  Animation&lt;double&gt; get parent =&gt; controller.animation!;\n\n  @override\n  void removeStatusListener(AnimationStatusListener listener) {\n    if (controller.animation != null)\n      super.removeStatusListener(listener);\n  }\n\n  @override\n  void removeListener(VoidCallback listener) {\n    if (controller.animation != null)\n      super.removeListener(listener);\n  }\n\n  @override\n  double get value {\n    assert(!controller.indexIsChanging);\n    final double controllerMaxValue = (controller.length - 1).toDouble();\n    final double controllerValue = controller.animation!.value.clamp(0.0, controllerMaxValue);\n    return (controllerValue - index.toDouble()).abs().clamp(0.0, 1.0);\n  }\n}\n\n\/\/ This class, and TabBarScrollController, only exist to handle the case\n\/\/ where a scrollable TabBar has a non-zero initialIndex. In that case we can\n\/\/ only compute the scroll position's initial scroll offset (the \"correct\"\n\/\/ pixels value) after the TabBar viewport width and scroll limits are known.\nclass _TabBarScrollPosition extends ScrollPositionWithSingleContext {\n  _TabBarScrollPosition({\n    required ScrollPhysics physics,\n    required ScrollContext context,\n    required ScrollPosition? oldPosition,\n    required this.tabBar,\n  }) : super(\n    physics: physics,\n    context: context,\n    initialPixels: null,\n    oldPosition: oldPosition,\n  );\n\n  final _HjtTabBarState tabBar;\n\n  bool? _initialViewportDimensionWasZero;\n\n  @override\n  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {\n    bool result = true;\n    if (_initialViewportDimensionWasZero != true) {\n      \/\/ If the viewport never had a non-zero dimension, we just want to jump\n      \/\/ to the initial scroll position to avoid strange scrolling effects in\n      \/\/ release mode: In release mode, the viewport temporarily may have a\n      \/\/ dimension of zero before the actual dimension is calculated. In that\n      \/\/ scenario, setting the actual dimension would cause a strange scroll\n      \/\/ effect without this guard because the super call below would starts a\n      \/\/ ballistic scroll activity.\n      assert(viewportDimension != null);\n      _initialViewportDimensionWasZero = viewportDimension != 0.0;\n      correctPixels(tabBar._initialScrollOffset(viewportDimension, minScrollExtent, maxScrollExtent));\n      result = false;\n    }\n    return super.applyContentDimensions(minScrollExtent, maxScrollExtent) &amp;&amp; result;\n  }\n}\n\n\/\/ This class, and TabBarScrollPosition, only exist to handle the case\n\/\/ where a scrollable TabBar has a non-zero initialIndex.\nclass _TabBarScrollController extends ScrollController {\n  _TabBarScrollController(this.tabBar);\n\n  final _HjtTabBarState tabBar;\n\n  @override\n  ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {\n    return _TabBarScrollPosition(\n      physics: physics,\n      context: context,\n      oldPosition: oldPosition,\n      tabBar: tabBar,\n    );\n  }\n}\n\n\/\/\/ A material design widget that displays a horizontal row of tabs.\n\/\/\/\n\/\/\/ Typically created as the [AppBar.bottom] part of an [AppBar] and in\n\/\/\/ conjunction with a [TabBarView].\n\/\/\/\n\/\/\/ {@youtube 560 315 https:\/\/www.youtube.com\/watch?v=POtoEH-5l40}\n\/\/\/\n\/\/\/ If a [TabController] is not provided, then a [DefaultTabController] ancestor\n\/\/\/ must be provided instead. The tab controller's [TabController.length] must\n\/\/\/ equal the length of the [tabs] list and the length of the\n\/\/\/ [TabBarView.children] list.\n\/\/\/\n\/\/\/ Requires one of its ancestors to be a [Material] widget.\n\/\/\/\n\/\/\/ Uses values from [TabBarTheme] if it is set in the current context.\n\/\/\/\n\/\/\/ {@tool dartpad --template=stateless_widget_material}\n\/\/\/ This sample shows the implementation of [HjtTabBar] and [TabBarView] using a [DefaultTabController].\n\/\/\/ Each [Tab] corresponds to a child of the [TabBarView] in the order they are written.\n\/\/\/\n\/\/\/ ```dart\n\/\/\/ Widget build(BuildContext context) {\n\/\/\/   return DefaultTabController(\n\/\/\/     initialIndex: 1,\n\/\/\/     length: 3,\n\/\/\/     child: Scaffold(\n\/\/\/       appBar: AppBar(\n\/\/\/         title: const Text('TabBar Widget'),\n\/\/\/         bottom: const TabBar(\n\/\/\/           tabs: &lt;Widget&gt;[\n\/\/\/             Tab(\n\/\/\/               icon: Icon(Icons.cloud_outlined),\n\/\/\/             ),\n\/\/\/             Tab(\n\/\/\/               icon: Icon(Icons.beach_access_sharp),\n\/\/\/             ),\n\/\/\/             Tab(\n\/\/\/               icon: Icon(Icons.brightness_5_sharp),\n\/\/\/             ),\n\/\/\/           ],\n\/\/\/         ),\n\/\/\/       ),\n\/\/\/       body: const TabBarView(\n\/\/\/         children: &lt;Widget&gt;[\n\/\/\/           Center(\n\/\/\/             child: Text(\"It's cloudy here\"),\n\/\/\/           ),\n\/\/\/           Center(\n\/\/\/             child: Text(\"It's rainy here\"),\n\/\/\/           ),\n\/\/\/           Center(\n\/\/\/             child: Text(\"It's sunny here\"),\n\/\/\/           ),\n\/\/\/         ],\n\/\/\/       ),\n\/\/\/     ),\n\/\/\/   );\n\/\/\/ }\n\/\/\/ ```\n\/\/\/ {@end-tool}\n\/\/\/\n\/\/\/ {@tool dartpad --template=stateful_widget_material_ticker}\n\/\/\/ [HjtTabBar] can also be implemented by using a [TabController] which provides more options\n\/\/\/ to control the behavior of the [HjtTabBar] and [TabBarView]. This can be used instead of\n\/\/\/ a [DefaultTabController], demonstrated below.\n\/\/\/\n\/\/\/ ```dart\n\/\/\/\n\/\/\/ late TabController _tabController;\n\/\/\/\n\/\/\/  @override\n\/\/\/  void initState() {\n\/\/\/    super.initState();\n\/\/\/    _tabController = TabController(length: 3, vsync: this);\n\/\/\/  }\n\/\/\/\n\/\/\/  @override\n\/\/\/  Widget build(BuildContext context) {\n\/\/\/    return Scaffold(\n\/\/\/      appBar: AppBar(\n\/\/\/        title: const Text('TabBar Widget'),\n\/\/\/        bottom: TabBar(\n\/\/\/          controller: _tabController,\n\/\/\/          tabs: const &lt;Widget&gt;[\n\/\/\/            Tab(\n\/\/\/              icon: Icon(Icons.cloud_outlined),\n\/\/\/            ),\n\/\/\/            Tab(\n\/\/\/             icon: Icon(Icons.beach_access_sharp),\n\/\/\/            ),\n\/\/\/            Tab(\n\/\/\/              icon: Icon(Icons.brightness_5_sharp),\n\/\/\/            ),\n\/\/\/          ],\n\/\/\/        ),\n\/\/\/      ),\n\/\/\/      body: TabBarView(\n\/\/\/        controller: _tabController,\n\/\/\/        children: const &lt;Widget&gt;[\n\/\/\/          Center(\n\/\/\/            child: Text(\"It's cloudy here\"),\n\/\/\/          ),\n\/\/\/          Center(\n\/\/\/            child: Text(\"It's rainy here\"),\n\/\/\/          ),\n\/\/\/          Center(\n\/\/\/             child: Text(\"It's sunny here\"),\n\/\/\/          ),\n\/\/\/        ],\n\/\/\/      ),\n\/\/\/    );\n\/\/\/  }\n\/\/\/ ```\n\/\/\/ {@end-tool}\n\/\/\/\n\/\/\/ See also:\n\/\/\/\n\/\/\/  * [TabBarView], which displays page views that correspond to each tab.\n\/\/\/  * [HjtTabBar], which is used to display the [Tab] that corresponds to each page of the [TabBarView].\nclass HjtTabBar extends StatefulWidget implements PreferredSizeWidget {\n  \/\/\/ Creates a material design tab bar.\n  \/\/\/\n  \/\/\/ The [tabs] argument must not be null and its length must match the [controller]'s\n  \/\/\/ [TabController.length].\n  \/\/\/\n  \/\/\/ If a [TabController] is not provided, then there must be a\n  \/\/\/ [DefaultTabController] ancestor.\n  \/\/\/\n  \/\/\/ The [indicatorWeight] parameter defaults to 2, and must not be null.\n  \/\/\/\n  \/\/\/ The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.\n  \/\/\/\n  \/\/\/ If [indicator] is not null or provided from [TabBarTheme],\n  \/\/\/ then [indicatorWeight], [indicatorPadding], and [indicatorColor] are ignored.\n  const HjtTabBar({\n    Key? key,\n    required this.tabs,\n    this.controller,\n    this.isScrollable = false,\n    this.padding,\n    this.indicatorColor,\n    this.automaticIndicatorColorAdjustment = true,\n    this.indicatorWeight = 2.0,\n    this.indicatorPadding = EdgeInsets.zero,\n    this.indicator,\n    this.indicatorSize,\n    this.labelColor,\n    this.labelStyle,\n    this.labelPadding,\n    this.unselectedLabelColor,\n    this.unselectedLabelStyle,\n    this.dragStartBehavior = DragStartBehavior.start,\n    this.overlayColor,\n    this.mouseCursor,\n    this.enableFeedback,\n    this.onTap,\n    this.physics,\n    this.indicatorMinWidth,\n  }) : assert(tabs != null),\n        assert(isScrollable != null),\n        assert(dragStartBehavior != null),\n        assert(indicator != null || (indicatorWeight != null &amp;&amp; indicatorWeight &gt; 0.0)),\n        assert(indicator != null || (indicatorPadding != null)),\n        super(key: key);\n\n  \/\/\/ Typically a list of two or more [Tab] widgets.\n  \/\/\/\n  \/\/\/ The length of this list must match the [controller]'s [TabController.length]\n  \/\/\/ and the length of the [TabBarView.children] list.\n  final List&lt;Widget&gt; tabs;\n\n  \/\/\/ This widget's selection and animation state.\n  \/\/\/\n  \/\/\/ If [TabController] is not provided, then the value of [DefaultTabController.of]\n  \/\/\/ will be used.\n  final TabController? controller;\n\n  \/\/\/ Whether this tab bar can be scrolled horizontally.\n  \/\/\/\n  \/\/\/ If [isScrollable] is true, then each tab is as wide as needed for its label\n  \/\/\/ and the entire [HjtTabBar] is scrollable. Otherwise each tab gets an equal\n  \/\/\/ share of the available space.\n  final bool isScrollable;\n\n  \/\/\/ The amount of space by which to inset the tab bar.\n  \/\/\/\n  \/\/\/ When [isScrollable] is false, this will yield the same result as if you had wrapped your\n  \/\/\/ [HjtTabBar] in a [Padding] widget. When [isScrollable] is true, the scrollable itself is inset,\n  \/\/\/ allowing the padding to scroll with the tab bar, rather than enclosing it.\n  final EdgeInsetsGeometry? padding;\n\n  \/\/\/ The color of the line that appears below the selected tab.\n  \/\/\/\n  \/\/\/ If this parameter is null, then the value of the Theme's indicatorColor\n  \/\/\/ property is used.\n  \/\/\/\n  \/\/\/ If [indicator] is specified or provided from [TabBarTheme],\n  \/\/\/ this property is ignored.\n  final Color? indicatorColor;\n\n  \/\/\/ The thickness of the line that appears below the selected tab.\n  \/\/\/\n  \/\/\/ The value of this parameter must be greater than zero and its default\n  \/\/\/ value is 2.0.\n  \/\/\/\n  \/\/\/ If [indicator] is specified or provided from [TabBarTheme],\n  \/\/\/ this property is ignored.\n  final double indicatorWeight;\n\n  final double? indicatorMinWidth;\n\n\n  \/\/\/ Padding for indicator.\n  \/\/\/ This property will now no longer be ignored even if indicator is declared\n  \/\/\/ or provided by [TabBarTheme]\n  \/\/\/\n  \/\/\/ For [isScrollable] tab bars, specifying [kTabLabelPadding] will align\n  \/\/\/ the indicator with the tab's text for [Tab] widgets and all but the\n  \/\/\/ shortest [Tab.text] values.\n  \/\/\/\n  \/\/\/ The default value of [indicatorPadding] is [EdgeInsets.zero].\n  final EdgeInsetsGeometry indicatorPadding;\n\n  \/\/\/ Defines the appearance of the selected tab indicator.\n  \/\/\/\n  \/\/\/ If [indicator] is specified or provided from [TabBarTheme],\n  \/\/\/ the [indicatorColor], and [indicatorWeight] properties are ignored.\n  \/\/\/\n  \/\/\/ The default, underline-style, selected tab indicator can be defined with\n  \/\/\/ [UnderlineTabIndicator].\n  \/\/\/\n  \/\/\/ The indicator's size is based on the tab's bounds. If [indicatorSize]\n  \/\/\/ is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space\n  \/\/\/ occupied by the tab in the tab bar. If [indicatorSize] is\n  \/\/\/ [TabBarIndicatorSize.label], then the tab's bounds are only as wide as\n  \/\/\/ the tab widget itself.\n  final Decoration? indicator;\n\n  \/\/\/ Whether this tab bar should automatically adjust the [indicatorColor].\n  \/\/\/\n  \/\/\/ If [automaticIndicatorColorAdjustment] is true,\n  \/\/\/ then the [indicatorColor] will be automatically adjusted to [Colors.white]\n  \/\/\/ when the [indicatorColor] is same as [Material.color] of the [Material] parent widget.\n  final bool automaticIndicatorColorAdjustment;\n\n  \/\/\/ Defines how the selected tab indicator's size is computed.\n  \/\/\/\n  \/\/\/ The size of the selected tab indicator is defined relative to the\n  \/\/\/ tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab]\n  \/\/\/ (the default) or relative to the bounds of the tab's widget if\n  \/\/\/ [indicatorSize] is [TabBarIndicatorSize.label].\n  \/\/\/\n  \/\/\/ The selected tab's location appearance can be refined further with_\n  \/\/\/ the [indicatorColor], [indicatorWeight], [indicatorPadding], and\n  \/\/\/ [indicator] properties.\n  final TabBarIndicatorSize? indicatorSize;\n\n  \/\/\/ The color of selected tab labels.\n  \/\/\/\n  \/\/\/ Unselected tab labels are rendered with the same color rendered at 70%\n  \/\/\/ opacity unless [unselectedLabelColor] is non-null.\n  \/\/\/\n  \/\/\/ If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s\n  \/\/\/ bodyText1 text color is used.\n  final Color? labelColor;\n\n  \/\/\/ The color of unselected tab labels.\n  \/\/\/\n  \/\/\/ If this property is null, unselected tab labels are rendered with the\n  \/\/\/ [labelColor] with 70% opacity.\n  final Color? unselectedLabelColor;\n\n  \/\/\/ The text style of the selected tab labels.\n  \/\/\/\n  \/\/\/ If [unselectedLabelStyle] is null, then this text style will be used for\n  \/\/\/ both selected and unselected label styles.\n  \/\/\/\n  \/\/\/ If this property is null, then the text style of the\n  \/\/\/ [ThemeData.primaryTextTheme]'s bodyText1 definition is used.\n  final TextStyle? labelStyle;\n\n  \/\/\/ The padding added to each of the tab labels.\n  \/\/\/\n  \/\/\/ If there are few tabs with both icon and text and few\n  \/\/\/ tabs with only icon or text, this padding is vertically\n  \/\/\/ adjusted to provide uniform padding to all tabs.\n  \/\/\/\n  \/\/\/ If this property is null, then kTabLabelPadding is used.\n  final EdgeInsetsGeometry? labelPadding;\n\n  \/\/\/ The text style of the unselected tab labels.\n  \/\/\/\n  \/\/\/ If this property is null, then the [labelStyle] value is used. If [labelStyle]\n  \/\/\/ is null, then the text style of the [ThemeData.primaryTextTheme]'s\n  \/\/\/ bodyText1 definition is used.\n  final TextStyle? unselectedLabelStyle;\n\n  \/\/\/ Defines the ink response focus, hover, and splash colors.\n  \/\/\/\n  \/\/\/ If non-null, it is resolved against one of [MaterialState.focused],\n  \/\/\/ [MaterialState.hovered], and [MaterialState.pressed].\n  \/\/\/\n  \/\/\/ [MaterialState.pressed] triggers a ripple (an ink splash), per\n  \/\/\/ the current Material Design spec. The [overlayColor] doesn't map\n  \/\/\/ a state to [InkResponse.highlightColor] because a separate highlight\n  \/\/\/ is not used by the current design guidelines. See\n  \/\/\/ https:\/\/material.io\/design\/interaction\/states.html#pressed\n  \/\/\/\n  \/\/\/ If the overlay color is null or resolves to null, then the default values\n  \/\/\/ for [InkResponse.focusColor], [InkResponse.hoverColor], [InkResponse.splashColor]\n  \/\/\/ will be used instead.\n  final MaterialStateProperty&lt;Color?&gt;? overlayColor;\n\n  \/\/\/ {@macro flutter.widgets.scrollable.dragStartBehavior}\n  final DragStartBehavior dragStartBehavior;\n\n  \/\/\/ The cursor for a mouse pointer when it enters or is hovering over the\n  \/\/\/ individual tab widgets.\n  \/\/\/\n  \/\/\/ If this property is null, [SystemMouseCursors.click] will be used.\n  final MouseCursor? mouseCursor;\n\n  \/\/\/ Whether detected gestures should provide acoustic and\/or haptic feedback.\n  \/\/\/\n  \/\/\/ For example, on Android a tap will produce a clicking sound and a long-press\n  \/\/\/ will produce a short vibration, when feedback is enabled.\n  \/\/\/\n  \/\/\/ Defaults to true.\n  final bool? enableFeedback;\n\n  \/\/\/ An optional callback that's called when the [HjtTabBar] is tapped.\n  \/\/\/\n  \/\/\/ The callback is applied to the index of the tab where the tap occurred.\n  \/\/\/\n  \/\/\/ This callback has no effect on the default handling of taps. It's for\n  \/\/\/ applications that want to do a little extra work when a tab is tapped,\n  \/\/\/ even if the tap doesn't change the TabController's index. TabBar [onTap]\n  \/\/\/ callbacks should not make changes to the TabController since that would\n  \/\/\/ interfere with the default tap handler.\n  final ValueChanged&lt;int&gt;? onTap;\n\n  \/\/\/ How the [HjtTabBar]'s scroll view should respond to user input.\n  \/\/\/\n  \/\/\/ For example, determines how the scroll view continues to animate after the\n  \/\/\/ user stops dragging the scroll view.\n  \/\/\/\n  \/\/\/ Defaults to matching platform conventions.\n  final ScrollPhysics? physics;\n\n  \/\/\/ A size whose height depends on if the tabs have both icons and text.\n  \/\/\/\n  \/\/\/ [AppBar] uses this size to compute its own preferred size.\n  @override\n  Size get preferredSize {\n    double maxHeight = _kTabHeight;\n    for (final Widget item in tabs) {\n      if (item is PreferredSizeWidget) {\n        final double itemHeight = item.preferredSize.height;\n        maxHeight = math.max(itemHeight, maxHeight);\n      }\n    }\n    return Size.fromHeight(maxHeight + indicatorWeight);\n  }\n\n  \/\/\/ Returns whether the [HjtTabBar] contains a tab with both text and icon.\n  \/\/\/\n  \/\/\/ [HjtTabBar] uses this to give uniform padding to all tabs in cases where\n  \/\/\/ there are some tabs with both text and icon and some which contain only\n  \/\/\/ text or icon.\n  bool get tabHasTextAndIcon {\n    for (final Widget item in tabs) {\n      if (item is PreferredSizeWidget) {\n        if (item.preferredSize.height == _kTextAndIconTabHeight)\n          return true;\n      }\n    }\n    return false;\n  }\n\n  @override\n  State&lt;HjtTabBar&gt; createState() =&gt; _HjtTabBarState();\n}\n\nclass _HjtTabBarState extends State&lt;HjtTabBar&gt; {\n  ScrollController? _scrollController;\n  TabController? _controller;\n  _IndicatorPainter? _indicatorPainter;\n  int? _currentIndex;\n  late double _tabStripWidth;\n  late List&lt;GlobalKey&gt; _tabKeys;\n\n  @override\n  void initState() {\n    super.initState();\n    \/\/ If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find\n    \/\/ the width of tab widget i. See _IndicatorPainter.indicatorRect().\n    _tabKeys = widget.tabs.map((Widget tab) =&gt; GlobalKey()).toList();\n  }\n\n  Decoration get _indicator {\n    if (widget.indicator != null)\n      return widget.indicator!;\n    final TabBarTheme tabBarTheme = TabBarTheme.of(context);\n    if (tabBarTheme.indicator != null)\n      return tabBarTheme.indicator!;\n\n    Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;\n    \/\/ ThemeData tries to avoid this by having indicatorColor avoid being the\n    \/\/ primaryColor. However, it's possible that the tab bar is on a\n    \/\/ Material that isn't the primaryColor. In that case, if the indicator\n    \/\/ color ends up matching the material's color, then this overrides it.\n    \/\/ When that happens, automatic transitions of the theme will likely look\n    \/\/ ugly as the indicator color suddenly snaps to white at one end, but it's\n    \/\/ not clear how to avoid that any further.\n    \/\/\n    \/\/ The material's color might be null (if it's a transparency). In that case\n    \/\/ there's no good way for us to find out what the color is so we don't.\n    \/\/\n    \/\/ TODO(xu-baolin): Remove automatic adjustment to white color indicator\n    \/\/ with a better long-term solution.\n    \/\/ https:\/\/github.com\/flutter\/flutter\/pull\/68171#pullrequestreview-517753917\n    if (widget.automaticIndicatorColorAdjustment &amp;&amp; color.value == Material.of(context)?.color?.value)\n      color = Colors.white;\n\n    return UnderlineTabIndicator(\n      borderSide: BorderSide(\n        width: widget.indicatorWeight,\n        color: color,\n      ),\n    );\n  }\n\n  \/\/ If the TabBar is rebuilt with a new tab controller, the caller should\n  \/\/ dispose the old one. In that case the old controller's animation will be\n  \/\/ null and should not be accessed.\n  bool get _controllerIsValid =&gt; _controller?.animation != null;\n\n  void _updateTabController() {\n    final TabController? newController = widget.controller ?? DefaultTabController.of(context);\n    assert(() {\n      if (newController == null) {\n        throw FlutterError(\n          'No TabController for ${widget.runtimeType}.\\n'\n              'When creating a ${widget.runtimeType}, you must either provide an explicit '\n              'TabController using the \"controller\" property, or you must ensure that there '\n              'is a DefaultTabController above the ${widget.runtimeType}.\\n'\n              'In this case, there was neither an explicit controller nor a default controller.',\n        );\n      }\n      return true;\n    }());\n\n    if (newController == _controller)\n      return;\n\n    if (_controllerIsValid) {\n      _controller!.animation!.removeListener(_handleTabControllerAnimationTick);\n      _controller!.removeListener(_handleTabControllerTick);\n    }\n    _controller = newController;\n    if (_controller != null) {\n      _controller!.animation!.addListener(_handleTabControllerAnimationTick);\n      _controller!.addListener(_handleTabControllerTick);\n      _currentIndex = _controller!.index;\n    }\n  }\n\n  void _initIndicatorPainter() {\n    _indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(\n      controller: _controller!,\n      indicator: _indicator,\n      indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,\n      indicatorPadding: widget.indicatorPadding,\n      tabKeys: _tabKeys,\n      old: _indicatorPainter,\n      indicatorMinWidth:widget.indicatorMinWidth\n    );\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    assert(debugCheckHasMaterial(context));\n    _updateTabController();\n    _initIndicatorPainter();\n  }\n\n  @override\n  void didUpdateWidget(HjtTabBar oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (widget.controller != oldWidget.controller) {\n      _updateTabController();\n      _initIndicatorPainter();\n    } else if (widget.indicatorColor != oldWidget.indicatorColor ||\n        widget.indicatorWeight != oldWidget.indicatorWeight ||\n        widget.indicatorSize != oldWidget.indicatorSize ||\n        widget.indicator != oldWidget.indicator) {\n      _initIndicatorPainter();\n    }\n\n    if (widget.tabs.length &gt; oldWidget.tabs.length) {\n      final int delta = widget.tabs.length - oldWidget.tabs.length;\n      _tabKeys.addAll(List&lt;GlobalKey&gt;.generate(delta, (int n) =&gt; GlobalKey()));\n    } else if (widget.tabs.length &lt; oldWidget.tabs.length) {\n      _tabKeys.removeRange(widget.tabs.length, oldWidget.tabs.length);\n    }\n  }\n\n  @override\n  void dispose() {\n    _indicatorPainter!.dispose();\n    if (_controllerIsValid) {\n      _controller!.animation!.removeListener(_handleTabControllerAnimationTick);\n      _controller!.removeListener(_handleTabControllerTick);\n    }\n    _controller = null;\n    \/\/ We don't own the _controller Animation, so it's not disposed here.\n    super.dispose();\n  }\n\n  int get maxTabIndex =&gt; _indicatorPainter!.maxTabIndex;\n\n  double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) {\n    if (!widget.isScrollable)\n      return 0.0;\n    double tabCenter = _indicatorPainter!.centerOf(index);\n    switch (Directionality.of(context)) {\n      case TextDirection.rtl:\n        tabCenter = _tabStripWidth - tabCenter;\n        break;\n      case TextDirection.ltr:\n        break;\n    }\n    return (tabCenter - viewportWidth \/ 2.0).clamp(minExtent, maxExtent);\n  }\n\n  double _tabCenteredScrollOffset(int index) {\n    final ScrollPosition position = _scrollController!.position;\n    return _tabScrollOffset(index, position.viewportDimension, position.minScrollExtent, position.maxScrollExtent);\n  }\n\n  double _initialScrollOffset(double viewportWidth, double minExtent, double maxExtent) {\n    return _tabScrollOffset(_currentIndex!, viewportWidth, minExtent, maxExtent);\n  }\n\n  void _scrollToCurrentIndex() {\n    final double offset = _tabCenteredScrollOffset(_currentIndex!);\n    _scrollController!.animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);\n  }\n\n  void _scrollToControllerValue() {\n    final double? leadingPosition = _currentIndex! &gt; 0 ? _tabCenteredScrollOffset(_currentIndex! - 1) : null;\n    final double middlePosition = _tabCenteredScrollOffset(_currentIndex!);\n    final double? trailingPosition = _currentIndex! &lt; maxTabIndex ? _tabCenteredScrollOffset(_currentIndex! + 1) : null;\n\n    final double index = _controller!.index.toDouble();\n    final double value = _controller!.animation!.value;\n    final double offset;\n    if (value == index - 1.0)\n      offset = leadingPosition ?? middlePosition;\n    else if (value == index + 1.0)\n      offset = trailingPosition ?? middlePosition;\n    else if (value == index)\n      offset = middlePosition;\n    else if (value &lt; index)\n      offset = leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value)!;\n    else\n      offset = trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index)!;\n\n    _scrollController!.jumpTo(offset);\n  }\n\n  void _handleTabControllerAnimationTick() {\n    assert(mounted);\n    if (!_controller!.indexIsChanging &amp;&amp; widget.isScrollable) {\n      \/\/ Sync the TabBar's scroll position with the TabBarView's PageView.\n      _currentIndex = _controller!.index;\n      _scrollToControllerValue();\n    }\n  }\n\n  void _handleTabControllerTick() {\n    if (_controller!.index != _currentIndex) {\n      _currentIndex = _controller!.index;\n      if (widget.isScrollable)\n        _scrollToCurrentIndex();\n    }\n    setState(() {\n      \/\/ Rebuild the tabs after a (potentially animated) index change\n      \/\/ has completed.\n    });\n  }\n\n  \/\/ Called each time layout completes.\n  void _saveTabOffsets(List&lt;double&gt; tabOffsets, TextDirection textDirection, double width) {\n    _tabStripWidth = width;\n    _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);\n  }\n\n  void _handleTap(int index) {\n    assert(index &gt;= 0 &amp;&amp; index &lt; widget.tabs.length);\n    _controller!.animateTo(index);\n    widget.onTap?.call(index);\n  }\n\n  Widget _buildStyledTab(Widget child, bool selected, Animation&lt;double&gt; animation) {\n    return _TabStyle(\n      animation: animation,\n      selected: selected,\n      labelColor: widget.labelColor,\n      unselectedLabelColor: widget.unselectedLabelColor,\n      labelStyle: widget.labelStyle,\n      unselectedLabelStyle: widget.unselectedLabelStyle,\n      child: child,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    assert(debugCheckHasMaterialLocalizations(context));\n    assert(() {\n      if (_controller!.length != widget.tabs.length) {\n        throw FlutterError(\n          \"Controller's length property (${_controller!.length}) does not match the \"\n              \"number of tabs (${widget.tabs.length}) present in TabBar's tabs property.\",\n        );\n      }\n      return true;\n    }());\n    final MaterialLocalizations localizations = MaterialLocalizations.of(context);\n    if (_controller!.length == 0) {\n      return Container(\n        height: _kTabHeight + widget.indicatorWeight,\n      );\n    }\n\n    final TabBarTheme tabBarTheme = TabBarTheme.of(context);\n\n    final List&lt;Widget&gt; wrappedTabs = List&lt;Widget&gt;.generate(widget.tabs.length, (int index) {\n      const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)\/2.0;\n      EdgeInsetsGeometry? adjustedPadding;\n\n      if (widget.tabs[index] is PreferredSizeWidget) {\n        final PreferredSizeWidget tab = widget.tabs[index] as PreferredSizeWidget;\n        if (widget.tabHasTextAndIcon &amp;&amp; tab.preferredSize.height == _kTabHeight) {\n          if (widget.labelPadding != null || tabBarTheme.labelPadding != null) {\n            adjustedPadding = (widget.labelPadding ?? tabBarTheme.labelPadding!).add(const EdgeInsets.symmetric(vertical: verticalAdjustment));\n          }\n          else {\n            adjustedPadding = const EdgeInsets.symmetric(vertical: verticalAdjustment, horizontal: 16.0);\n          }\n        }\n      }\n\n      return Center(\n        heightFactor: 1.0,\n        child: Padding(\n          padding: adjustedPadding ?? widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding,\n          child: KeyedSubtree(\n            key: _tabKeys[index],\n            child: widget.tabs[index],\n          ),\n        ),\n      );\n    });\n\n    \/\/ If the controller was provided by DefaultTabController and we're part\n    \/\/ of a Hero (typically the AppBar), then we will not be able to find the\n    \/\/ controller during a Hero transition. See https:\/\/github.com\/flutter\/flutter\/issues\/213.\n    if (_controller != null) {\n      final int previousIndex = _controller!.previousIndex;\n\n      if (_controller!.indexIsChanging) {\n        \/\/ The user tapped on a tab, the tab controller's animation is running.\n        assert(_currentIndex != previousIndex);\n        final Animation&lt;double&gt; animation = _ChangeAnimation(_controller!);\n        wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation);\n        wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);\n      } else {\n        \/\/ The user is dragging the TabBarView's PageView left or right.\n        final int tabIndex = _currentIndex!;\n        final Animation&lt;double&gt; centerAnimation = _DragAnimation(_controller!, tabIndex);\n        wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);\n        if (_currentIndex! &gt; 0) {\n          final int tabIndex = _currentIndex! - 1;\n          final Animation&lt;double&gt; previousAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));\n          wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation);\n        }\n        if (_currentIndex! &lt; widget.tabs.length - 1) {\n          final int tabIndex = _currentIndex! + 1;\n          final Animation&lt;double&gt; nextAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));\n          wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation);\n        }\n      }\n    }\n\n    \/\/ Add the tap handler to each tab. If the tab bar is not scrollable,\n    \/\/ then give all of the tabs equal flexibility so that they each occupy\n    \/\/ the same share of the tab bar's overall width.\n    final int tabCount = widget.tabs.length;\n    for (int index = 0; index &lt; tabCount; index += 1) {\n      wrappedTabs[index] = InkWell(\n        mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click,\n        onTap: () { _handleTap(index); },\n        enableFeedback: widget.enableFeedback ?? true,\n        overlayColor: widget.overlayColor,\n        child: Padding(\n          padding: EdgeInsets.only(bottom: widget.indicatorWeight),\n          child: Stack(\n            children: &lt;Widget&gt;[\n              wrappedTabs[index],\n              Semantics(\n                selected: index == _currentIndex,\n                label: localizations.tabLabel(tabIndex: index + 1, tabCount: tabCount),\n              ),\n            ],\n          ),\n        ),\n      );\n      if (!widget.isScrollable)\n        wrappedTabs[index] = Expanded(child: wrappedTabs[index]);\n    }\n\n    Widget tabBar = CustomPaint(\n      painter: _indicatorPainter,\n      child: _TabStyle(\n        animation: kAlwaysDismissedAnimation,\n        selected: false,\n        labelColor: widget.labelColor,\n        unselectedLabelColor: widget.unselectedLabelColor,\n        labelStyle: widget.labelStyle,\n        unselectedLabelStyle: widget.unselectedLabelStyle,\n        child: _TabLabelBar(\n          onPerformLayout: _saveTabOffsets,\n          children: wrappedTabs,\n        ),\n      ),\n    );\n\n    if (widget.isScrollable) {\n      _scrollController ??= _TabBarScrollController(this);\n      tabBar = SingleChildScrollView(\n        dragStartBehavior: widget.dragStartBehavior,\n        scrollDirection: Axis.horizontal,\n        controller: _scrollController,\n        padding: widget.padding,\n        physics: widget.physics,\n        child: tabBar,\n      );\n    } else if (widget.padding != null) {\n      tabBar = Padding(\n        padding: widget.padding!,\n        child: tabBar,\n      );\n    }\n\n    return tabBar;\n  }\n}\n\n\/\/\/ Displays a single circle with the specified border and background colors.\n\/\/\/\n\/\/\/ Used by [TabPageSelector] to indicate the selected page.\nclass TabPageSelectorIndicator extends StatelessWidget {\n  \/\/\/ Creates an indicator used by [TabPageSelector].\n  \/\/\/\n  \/\/\/ The [backgroundColor], [borderColor], and [size] parameters must not be null.\n  const TabPageSelectorIndicator({\n    Key? key,\n    required this.backgroundColor,\n    required this.borderColor,\n    required this.size,\n  }) : assert(backgroundColor != null),\n        assert(borderColor != null),\n        assert(size != null),\n        super(key: key);\n\n  \/\/\/ The indicator circle's background color.\n  final Color backgroundColor;\n\n  \/\/\/ The indicator circle's border color.\n  final Color borderColor;\n\n  \/\/\/ The indicator circle's diameter.\n  final double size;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: size,\n      height: size,\n      margin: const EdgeInsets.all(4.0),\n      decoration: BoxDecoration(\n        color: backgroundColor,\n        border: Border.all(color: borderColor),\n        shape: BoxShape.circle,\n      ),\n    );\n  }\n}\n\n\n\n\n\/\/ \u5b9a\u4e49Tabbar\u7684 label\u6837\u5f0f\nclass RRecTabIndicator extends Decoration {\n  const RRecTabIndicator(\n      {this.borderSide = const BorderSide(width: 2.0, color: Colors.white),\n        this.insets = EdgeInsets.zero,\n        this.radius = 0,\n        this.color = Colors.white});\n\n  final double radius;\n  final Color color;\n  final BorderSide borderSide;\n  final EdgeInsetsGeometry insets;\n\n  @override\n  Decoration? lerpFrom(Decoration? a, double t) {\n    if (a is RRecTabIndicator) {\n      return RRecTabIndicator(\n        borderSide: BorderSide.lerp(a.borderSide, borderSide, t),\n        insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,\n      );\n    }\n    return super.lerpFrom(a, t);\n  }\n\n  @override\n  Decoration? lerpTo(Decoration? b, double t) {\n    if (b is RRecTabIndicator) {\n      return RRecTabIndicator(\n        borderSide: BorderSide.lerp(borderSide, b.borderSide, t),\n        insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!,\n      );\n    }\n    return super.lerpTo(b, t);\n  }\n\n  @override\n  _UnderlinePainter createBoxPainter([VoidCallback? onChanged]) {\n    return _UnderlinePainter(this, onChanged);\n  }\n\n  Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {\n    final Rect indicator = insets.resolve(textDirection).deflateRect(rect);\n    return Rect.fromLTWH(\n      indicator.left,\n      indicator.bottom - borderSide.width,\n      indicator.width,\n      borderSide.width,\n    );\n  }\n\n  @override\n  Path getClipPath(Rect rect, TextDirection textDirection) {\n    return Path()..addRect(_indicatorRectFor(rect, textDirection));\n  }\n}\n\nclass _UnderlinePainter extends BoxPainter {\n  _UnderlinePainter(this.decoration, VoidCallback? onChanged)\n      : super(onChanged);\n\n  final RRecTabIndicator decoration;\n\n  @override\n  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {\n    final Rect rect = offset &amp; configuration.size!;\n    final TextDirection textDirection = configuration.textDirection!;\n    final Rect indicator = decoration._indicatorRectFor(rect, textDirection);\n    final Paint paint = decoration.borderSide.toPaint()\n      ..strokeCap = StrokeCap.square\n      ..color = decoration.color;\n    final RRect rRect =\n    RRect.fromRectAndRadius(indicator, Radius.circular(decoration.radius));\n    canvas.drawRRect(rRect, paint);\n  }\n}\n\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>\u4f7f\u7528 @override Widget build(BuildContext context) { retur [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12,8],"tags":[],"class_list":["post-337","post","type-post","status-publish","format-standard","hentry","category-flutter","category-8"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v24.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Flutter-Tabbar\u6307\u793a\u5668 - IIchen<\/title>\n<meta name=\"description\" content=\"Flutter-Tabbar\u6307\u793a\u5668\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/iichen.cn\/?p=337\" \/>\n<meta property=\"og:locale\" content=\"zh_CN\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Flutter-Tabbar\u6307\u793a\u5668 - IIchen\" \/>\n<meta property=\"og:description\" content=\"Flutter-Tabbar\u6307\u793a\u5668\" \/>\n<meta property=\"og:url\" content=\"https:\/\/iichen.cn\/?p=337\" \/>\n<meta property=\"og:site_name\" content=\"IIchen\" \/>\n<meta property=\"article:published_time\" content=\"2022-03-07T02:27:13+00:00\" \/>\n<meta name=\"author\" content=\"iichen\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u4f5c\u8005\" \/>\n\t<meta name=\"twitter:data1\" content=\"iichen\" \/>\n\t<meta name=\"twitter:label2\" content=\"\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4\" \/>\n\t<meta name=\"twitter:data2\" content=\"25 \u5206\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/iichen.cn\/?p=337#article\",\"isPartOf\":{\"@id\":\"https:\/\/iichen.cn\/?p=337\"},\"author\":{\"name\":\"iichen\",\"@id\":\"https:\/\/iichen.cn\/#\/schema\/person\/4a47edf85ab49841df9e8f6aee40b77c\"},\"headline\":\"Flutter-Tabbar\u6307\u793a\u5668\",\"datePublished\":\"2022-03-07T02:27:13+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/iichen.cn\/?p=337\"},\"wordCount\":1,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/iichen.cn\/#\/schema\/person\/4a47edf85ab49841df9e8f6aee40b77c\"},\"articleSection\":[\"Flutter\",\"\u7b14\u8bb0\"],\"inLanguage\":\"zh-Hans\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/iichen.cn\/?p=337#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/iichen.cn\/?p=337\",\"url\":\"https:\/\/iichen.cn\/?p=337\",\"name\":\"Flutter-Tabbar\u6307\u793a\u5668 - IIchen\",\"isPartOf\":{\"@id\":\"https:\/\/iichen.cn\/#website\"},\"datePublished\":\"2022-03-07T02:27:13+00:00\",\"description\":\"Flutter-Tabbar\u6307\u793a\u5668\",\"breadcrumb\":{\"@id\":\"https:\/\/iichen.cn\/?p=337#breadcrumb\"},\"inLanguage\":\"zh-Hans\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/iichen.cn\/?p=337\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/iichen.cn\/?p=337#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"\u9996\u9875\",\"item\":\"https:\/\/iichen.cn\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Flutter-Tabbar\u6307\u793a\u5668\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/iichen.cn\/#website\",\"url\":\"https:\/\/iichen.cn\/\",\"name\":\"IIchen\",\"description\":\"Just do it!\",\"publisher\":{\"@id\":\"https:\/\/iichen.cn\/#\/schema\/person\/4a47edf85ab49841df9e8f6aee40b77c\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/iichen.cn\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"zh-Hans\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\/\/iichen.cn\/#\/schema\/person\/4a47edf85ab49841df9e8f6aee40b77c\",\"name\":\"iichen\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"zh-Hans\",\"@id\":\"https:\/\/iichen.cn\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/iichen.cn\/wp-content\/uploads\/2025\/01\/avatar.jpg\",\"contentUrl\":\"https:\/\/iichen.cn\/wp-content\/uploads\/2025\/01\/avatar.jpg\",\"width\":940,\"height\":940,\"caption\":\"iichen\"},\"logo\":{\"@id\":\"https:\/\/iichen.cn\/#\/schema\/person\/image\/\"},\"sameAs\":[\"https:\/\/www.iichen.cn\"],\"url\":\"https:\/\/iichen.cn\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Flutter-Tabbar\u6307\u793a\u5668 - IIchen","description":"Flutter-Tabbar\u6307\u793a\u5668","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/iichen.cn\/?p=337","og_locale":"zh_CN","og_type":"article","og_title":"Flutter-Tabbar\u6307\u793a\u5668 - IIchen","og_description":"Flutter-Tabbar\u6307\u793a\u5668","og_url":"https:\/\/iichen.cn\/?p=337","og_site_name":"IIchen","article_published_time":"2022-03-07T02:27:13+00:00","author":"iichen","twitter_card":"summary_large_image","twitter_misc":{"\u4f5c\u8005":"iichen","\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4":"25 \u5206"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/iichen.cn\/?p=337#article","isPartOf":{"@id":"https:\/\/iichen.cn\/?p=337"},"author":{"name":"iichen","@id":"https:\/\/iichen.cn\/#\/schema\/person\/4a47edf85ab49841df9e8f6aee40b77c"},"headline":"Flutter-Tabbar\u6307\u793a\u5668","datePublished":"2022-03-07T02:27:13+00:00","mainEntityOfPage":{"@id":"https:\/\/iichen.cn\/?p=337"},"wordCount":1,"commentCount":0,"publisher":{"@id":"https:\/\/iichen.cn\/#\/schema\/person\/4a47edf85ab49841df9e8f6aee40b77c"},"articleSection":["Flutter","\u7b14\u8bb0"],"inLanguage":"zh-Hans","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/iichen.cn\/?p=337#respond"]}]},{"@type":"WebPage","@id":"https:\/\/iichen.cn\/?p=337","url":"https:\/\/iichen.cn\/?p=337","name":"Flutter-Tabbar\u6307\u793a\u5668 - IIchen","isPartOf":{"@id":"https:\/\/iichen.cn\/#website"},"datePublished":"2022-03-07T02:27:13+00:00","description":"Flutter-Tabbar\u6307\u793a\u5668","breadcrumb":{"@id":"https:\/\/iichen.cn\/?p=337#breadcrumb"},"inLanguage":"zh-Hans","potentialAction":[{"@type":"ReadAction","target":["https:\/\/iichen.cn\/?p=337"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/iichen.cn\/?p=337#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"\u9996\u9875","item":"https:\/\/iichen.cn\/"},{"@type":"ListItem","position":2,"name":"Flutter-Tabbar\u6307\u793a\u5668"}]},{"@type":"WebSite","@id":"https:\/\/iichen.cn\/#website","url":"https:\/\/iichen.cn\/","name":"IIchen","description":"Just do it!","publisher":{"@id":"https:\/\/iichen.cn\/#\/schema\/person\/4a47edf85ab49841df9e8f6aee40b77c"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/iichen.cn\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"zh-Hans"},{"@type":["Person","Organization"],"@id":"https:\/\/iichen.cn\/#\/schema\/person\/4a47edf85ab49841df9e8f6aee40b77c","name":"iichen","image":{"@type":"ImageObject","inLanguage":"zh-Hans","@id":"https:\/\/iichen.cn\/#\/schema\/person\/image\/","url":"https:\/\/iichen.cn\/wp-content\/uploads\/2025\/01\/avatar.jpg","contentUrl":"https:\/\/iichen.cn\/wp-content\/uploads\/2025\/01\/avatar.jpg","width":940,"height":940,"caption":"iichen"},"logo":{"@id":"https:\/\/iichen.cn\/#\/schema\/person\/image\/"},"sameAs":["https:\/\/www.iichen.cn"],"url":"https:\/\/iichen.cn\/?author=1"}]}},"_links":{"self":[{"href":"https:\/\/iichen.cn\/index.php?rest_route=\/wp\/v2\/posts\/337","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/iichen.cn\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/iichen.cn\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/iichen.cn\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/iichen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=337"}],"version-history":[{"count":0,"href":"https:\/\/iichen.cn\/index.php?rest_route=\/wp\/v2\/posts\/337\/revisions"}],"wp:attachment":[{"href":"https:\/\/iichen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=337"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/iichen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=337"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/iichen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=337"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}