flutter_flow_button_tabbar.dart 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. import 'dart:math' as math;
  2. import 'dart:ui' show lerpDouble;
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter/gestures.dart' show DragStartBehavior;
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/rendering.dart';
  7. const double _kTabHeight = 46.0;
  8. typedef _LayoutCallback = void Function(
  9. List<double> xOffsets, TextDirection textDirection, double width);
  10. class _TabLabelBarRenderer extends RenderFlex {
  11. _TabLabelBarRenderer({
  12. required Axis direction,
  13. required MainAxisSize mainAxisSize,
  14. required MainAxisAlignment mainAxisAlignment,
  15. required CrossAxisAlignment crossAxisAlignment,
  16. required TextDirection textDirection,
  17. required VerticalDirection verticalDirection,
  18. required this.onPerformLayout,
  19. }) : super(
  20. direction: direction,
  21. mainAxisSize: mainAxisSize,
  22. mainAxisAlignment: mainAxisAlignment,
  23. crossAxisAlignment: crossAxisAlignment,
  24. textDirection: textDirection,
  25. verticalDirection: verticalDirection,
  26. );
  27. _LayoutCallback onPerformLayout;
  28. @override
  29. void performLayout() {
  30. super.performLayout();
  31. // xOffsets will contain childCount+1 values, giving the offsets of the
  32. // leading edge of the first tab as the first value, of the leading edge of
  33. // the each subsequent tab as each subsequent value, and of the trailing
  34. // edge of the last tab as the last value.
  35. RenderBox? child = firstChild;
  36. final List<double> xOffsets = <double>[];
  37. while (child != null) {
  38. final FlexParentData childParentData =
  39. child.parentData! as FlexParentData;
  40. xOffsets.add(childParentData.offset.dx);
  41. assert(child.parentData == childParentData);
  42. child = childParentData.nextSibling;
  43. }
  44. assert(textDirection != null);
  45. switch (textDirection!) {
  46. case TextDirection.rtl:
  47. xOffsets.insert(0, size.width);
  48. break;
  49. case TextDirection.ltr:
  50. xOffsets.add(size.width);
  51. break;
  52. }
  53. onPerformLayout(xOffsets, textDirection!, size.width);
  54. }
  55. }
  56. // This class and its renderer class only exist to report the widths of the tabs
  57. // upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
  58. // or in response to input.
  59. class _TabLabelBar extends Flex {
  60. _TabLabelBar({
  61. required List<Widget> children,
  62. required this.onPerformLayout,
  63. }) : super(
  64. children: children,
  65. direction: Axis.horizontal,
  66. mainAxisSize: MainAxisSize.max,
  67. mainAxisAlignment: MainAxisAlignment.start,
  68. crossAxisAlignment: CrossAxisAlignment.center,
  69. verticalDirection: VerticalDirection.down,
  70. );
  71. final _LayoutCallback onPerformLayout;
  72. @override
  73. RenderFlex createRenderObject(BuildContext context) {
  74. return _TabLabelBarRenderer(
  75. direction: direction,
  76. mainAxisAlignment: mainAxisAlignment,
  77. mainAxisSize: mainAxisSize,
  78. crossAxisAlignment: crossAxisAlignment,
  79. textDirection: getEffectiveTextDirection(context)!,
  80. verticalDirection: verticalDirection,
  81. onPerformLayout: onPerformLayout,
  82. );
  83. }
  84. @override
  85. void updateRenderObject(
  86. BuildContext context, _TabLabelBarRenderer renderObject) {
  87. super.updateRenderObject(context, renderObject);
  88. renderObject.onPerformLayout = onPerformLayout;
  89. }
  90. }
  91. class _IndicatorPainter extends CustomPainter {
  92. _IndicatorPainter({
  93. required this.controller,
  94. required this.tabKeys,
  95. required _IndicatorPainter? old,
  96. }) : super(repaint: controller.animation) {
  97. if (old != null) {
  98. saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
  99. }
  100. }
  101. final TabController controller;
  102. final List<GlobalKey> tabKeys;
  103. // _currentTabOffsets and _currentTextDirection are set each time TabBar
  104. // layout is completed. These values can be null when TabBar contains no
  105. // tabs, since there are nothing to lay out.
  106. List<double>? _currentTabOffsets;
  107. TextDirection? _currentTextDirection;
  108. BoxPainter? _painter;
  109. bool _needsPaint = false;
  110. void markNeedsPaint() {
  111. _needsPaint = true;
  112. }
  113. void dispose() {
  114. _painter?.dispose();
  115. }
  116. void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
  117. _currentTabOffsets = tabOffsets;
  118. _currentTextDirection = textDirection;
  119. }
  120. // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
  121. // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
  122. int get maxTabIndex => _currentTabOffsets!.length - 2;
  123. double centerOf(int tabIndex) {
  124. assert(_currentTabOffsets != null);
  125. assert(_currentTabOffsets!.isNotEmpty);
  126. assert(tabIndex >= 0);
  127. assert(tabIndex <= maxTabIndex);
  128. return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) /
  129. 2.0;
  130. }
  131. @override
  132. void paint(Canvas canvas, Size size) {
  133. _needsPaint = false;
  134. }
  135. @override
  136. bool shouldRepaint(_IndicatorPainter old) {
  137. return _needsPaint ||
  138. controller != old.controller ||
  139. tabKeys.length != old.tabKeys.length ||
  140. (!listEquals(_currentTabOffsets, old._currentTabOffsets)) ||
  141. _currentTextDirection != old._currentTextDirection;
  142. }
  143. }
  144. // This class, and TabBarScrollController, only exist to handle the case
  145. // where a scrollable TabBar has a non-zero initialIndex. In that case we can
  146. // only compute the scroll position's initial scroll offset (the "correct"
  147. // pixels value) after the TabBar viewport width and scroll limits are known.
  148. class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
  149. _TabBarScrollPosition({
  150. required ScrollPhysics physics,
  151. required ScrollContext context,
  152. required ScrollPosition? oldPosition,
  153. required this.tabBar,
  154. }) : super(
  155. initialPixels: null,
  156. physics: physics,
  157. context: context,
  158. oldPosition: oldPosition,
  159. );
  160. final _FlutterFlowButtonTabBarState tabBar;
  161. bool _viewportDimensionWasNonZero = false;
  162. // Position should be adjusted at least once.
  163. bool _needsPixelsCorrection = true;
  164. @override
  165. bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
  166. bool result = true;
  167. if (!_viewportDimensionWasNonZero) {
  168. _viewportDimensionWasNonZero = viewportDimension != 0.0;
  169. }
  170. // If the viewport never had a non-zero dimension, we just want to jump
  171. // to the initial scroll position to avoid strange scrolling effects in
  172. // release mode: In release mode, the viewport temporarily may have a
  173. // dimension of zero before the actual dimension is calculated. In that
  174. // scenario, setting the actual dimension would cause a strange scroll
  175. // effect without this guard because the super call below would starts a
  176. // ballistic scroll activity.
  177. if (!_viewportDimensionWasNonZero || _needsPixelsCorrection) {
  178. _needsPixelsCorrection = false;
  179. correctPixels(tabBar._initialScrollOffset(
  180. viewportDimension, minScrollExtent, maxScrollExtent));
  181. result = false;
  182. }
  183. return super.applyContentDimensions(minScrollExtent, maxScrollExtent) &&
  184. result;
  185. }
  186. void markNeedsPixelsCorrection() {
  187. _needsPixelsCorrection = true;
  188. }
  189. }
  190. // This class, and TabBarScrollPosition, only exist to handle the case
  191. // where a scrollable TabBar has a non-zero initialIndex.
  192. class _TabBarScrollController extends ScrollController {
  193. _TabBarScrollController(this.tabBar);
  194. final _FlutterFlowButtonTabBarState tabBar;
  195. @override
  196. ScrollPosition createScrollPosition(ScrollPhysics physics,
  197. ScrollContext context, ScrollPosition? oldPosition) {
  198. return _TabBarScrollPosition(
  199. physics: physics,
  200. context: context,
  201. oldPosition: oldPosition,
  202. tabBar: tabBar,
  203. );
  204. }
  205. }
  206. /// A Flutterflow Design widget that displays a horizontal row of tabs.
  207. class FlutterFlowButtonTabBar extends StatefulWidget
  208. implements PreferredSizeWidget {
  209. /// The [tabs] argument must not be null and its length must match the [controller]'s
  210. /// [TabController.length].
  211. ///
  212. /// If a [TabController] is not provided, then there must be a
  213. /// [DefaultTabController] ancestor.
  214. ///
  215. const FlutterFlowButtonTabBar({
  216. Key? key,
  217. required this.tabs,
  218. this.controller,
  219. this.isScrollable = false,
  220. this.useToggleButtonStyle = false,
  221. this.dragStartBehavior = DragStartBehavior.start,
  222. this.onTap,
  223. this.backgroundColor,
  224. this.unselectedBackgroundColor,
  225. this.decoration,
  226. this.unselectedDecoration,
  227. this.labelStyle,
  228. this.unselectedLabelStyle,
  229. this.labelColor,
  230. this.unselectedLabelColor,
  231. this.borderWidth = 0,
  232. this.borderColor = Colors.transparent,
  233. this.unselectedBorderColor = Colors.transparent,
  234. this.physics = const BouncingScrollPhysics(),
  235. this.labelPadding = const EdgeInsets.symmetric(horizontal: 4),
  236. this.buttonMargin = const EdgeInsets.all(4),
  237. this.padding = EdgeInsets.zero,
  238. this.borderRadius = 8.0,
  239. this.elevation = 0,
  240. }) : super(key: key);
  241. /// Typically a list of two or more [Tab] widgets.
  242. ///
  243. /// The length of this list must match the [controller]'s [TabController.length]
  244. /// and the length of the [TabBarView.children] list.
  245. final List<Widget> tabs;
  246. /// This widget's selection and animation state.
  247. ///
  248. /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  249. /// will be used.
  250. final TabController? controller;
  251. /// Whether this tab bar can be scrolled horizontally.
  252. ///
  253. /// If [isScrollable] is true, then each tab is as wide as needed for its label
  254. /// and the entire [FlutterFlowButtonTabBar] is scrollable. Otherwise each tab gets an equal
  255. /// share of the available space.
  256. final bool isScrollable;
  257. /// Whether the tab buttons should be styled as toggle buttons.
  258. final bool useToggleButtonStyle;
  259. /// The background [Color] of the button on its selected state.
  260. final Color? backgroundColor;
  261. /// The background [Color] of the button on its unselected state.
  262. final Color? unselectedBackgroundColor;
  263. /// The [BoxDecoration] of the button on its selected state.
  264. ///
  265. /// If [BoxDecoration] is not provided, [backgroundColor] is used.
  266. final BoxDecoration? decoration;
  267. /// The [BoxDecoration] of the button on its unselected state.
  268. ///
  269. /// If [BoxDecoration] is not provided, [unselectedBackgroundColor] is used.
  270. final BoxDecoration? unselectedDecoration;
  271. /// The [TextStyle] of the button's [Text] on its selected state. The color provided
  272. /// on the TextStyle will be used for the [Icon]'s color.
  273. final TextStyle? labelStyle;
  274. /// The color of selected tab labels.
  275. final Color? labelColor;
  276. /// The color of unselected tab labels.
  277. final Color? unselectedLabelColor;
  278. /// The [TextStyle] of the button's [Text] on its unselected state. The color provided
  279. /// on the TextStyle will be used for the [Icon]'s color.
  280. final TextStyle? unselectedLabelStyle;
  281. /// The with of solid [Border] for each button. If no value is provided, the border
  282. /// is not drawn.
  283. final double borderWidth;
  284. /// The [Color] of solid [Border] for each button.
  285. final Color? borderColor;
  286. /// The [Color] of solid [Border] for each button. If no value is provided, the value of
  287. /// [this.borderColor] is used.
  288. final Color? unselectedBorderColor;
  289. /// The [EdgeInsets] used for the [Padding] of the buttons' content.
  290. ///
  291. /// The default value is [EdgeInsets.symmetric(horizontal: 4)].
  292. final EdgeInsetsGeometry labelPadding;
  293. /// The [EdgeInsets] used for the [Margin] of the buttons.
  294. ///
  295. /// The default value is [EdgeInsets.all(4)].
  296. final EdgeInsetsGeometry buttonMargin;
  297. /// The amount of space by which to inset the tab bar.
  298. final EdgeInsetsGeometry? padding;
  299. /// The value of the [BorderRadius.circular] applied to each button.
  300. final double borderRadius;
  301. /// The value of the [elevation] applied to each button.
  302. final double elevation;
  303. final DragStartBehavior dragStartBehavior;
  304. final ValueChanged<int>? onTap;
  305. final ScrollPhysics? physics;
  306. /// A size whose height depends on if the tabs have both icons and text.
  307. ///
  308. /// [AppBar] uses this size to compute its own preferred size.
  309. @override
  310. Size get preferredSize {
  311. double maxHeight = _kTabHeight;
  312. for (final Widget item in tabs) {
  313. if (item is PreferredSizeWidget) {
  314. final double itemHeight = item.preferredSize.height;
  315. maxHeight = math.max(itemHeight, maxHeight);
  316. }
  317. }
  318. return Size.fromHeight(
  319. maxHeight + labelPadding.vertical + buttonMargin.vertical);
  320. }
  321. @override
  322. State<FlutterFlowButtonTabBar> createState() =>
  323. _FlutterFlowButtonTabBarState();
  324. }
  325. class _FlutterFlowButtonTabBarState extends State<FlutterFlowButtonTabBar>
  326. with TickerProviderStateMixin {
  327. ScrollController? _scrollController;
  328. TabController? _controller;
  329. _IndicatorPainter? _indicatorPainter;
  330. late AnimationController _animationController;
  331. int _currentIndex = 0;
  332. int _prevIndex = -1;
  333. late double _tabStripWidth;
  334. late List<GlobalKey> _tabKeys;
  335. final GlobalKey _tabsParentKey = GlobalKey();
  336. bool _debugHasScheduledValidTabsCountCheck = false;
  337. @override
  338. void initState() {
  339. super.initState();
  340. // If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
  341. // the width of tab widget i. See _IndicatorPainter.indicatorRect().
  342. _tabKeys = widget.tabs.map((tab) => GlobalKey()).toList();
  343. /// The animation duration is 2/3 of the tab scroll animation duration in
  344. /// Material design (kTabScrollDuration).
  345. _animationController = AnimationController(
  346. vsync: this, duration: const Duration(milliseconds: 200));
  347. // so the buttons start in their "final" state (color)
  348. _animationController
  349. ..value = 1.0
  350. ..addListener(() {
  351. if (mounted) {
  352. setState(() {});
  353. }
  354. });
  355. }
  356. // If the TabBar is rebuilt with a new tab controller, the caller should
  357. // dispose the old one. In that case the old controller's animation will be
  358. // null and should not be accessed.
  359. bool get _controllerIsValid => _controller?.animation != null;
  360. void _updateTabController() {
  361. final TabController? newController =
  362. widget.controller ?? DefaultTabController.maybeOf(context);
  363. assert(() {
  364. if (newController == null) {
  365. throw FlutterError(
  366. 'No TabController for ${widget.runtimeType}.\n'
  367. 'When creating a ${widget.runtimeType}, you must either provide an explicit '
  368. 'TabController using the "controller" property, or you must ensure that there '
  369. 'is a DefaultTabController above the ${widget.runtimeType}.\n'
  370. 'In this case, there was neither an explicit controller nor a default controller.',
  371. );
  372. }
  373. return true;
  374. }());
  375. if (newController == _controller) {
  376. return;
  377. }
  378. if (_controllerIsValid) {
  379. _controller!.animation!.removeListener(_handleTabControllerAnimationTick);
  380. _controller!.removeListener(_handleTabControllerTick);
  381. }
  382. _controller = newController;
  383. if (_controller != null) {
  384. _controller!.animation!.addListener(_handleTabControllerAnimationTick);
  385. _controller!.addListener(_handleTabControllerTick);
  386. _currentIndex = _controller!.index;
  387. }
  388. }
  389. void _initIndicatorPainter() {
  390. _indicatorPainter = !_controllerIsValid
  391. ? null
  392. : _IndicatorPainter(
  393. controller: _controller!,
  394. tabKeys: _tabKeys,
  395. old: _indicatorPainter,
  396. );
  397. }
  398. @override
  399. void didChangeDependencies() {
  400. super.didChangeDependencies();
  401. assert(debugCheckHasMaterial(context));
  402. _updateTabController();
  403. _initIndicatorPainter();
  404. }
  405. @override
  406. void didUpdateWidget(FlutterFlowButtonTabBar oldWidget) {
  407. super.didUpdateWidget(oldWidget);
  408. if (widget.controller != oldWidget.controller) {
  409. _updateTabController();
  410. _initIndicatorPainter();
  411. // Adjust scroll position.
  412. if (_scrollController != null) {
  413. final ScrollPosition position = _scrollController!.position;
  414. if (position is _TabBarScrollPosition) {
  415. position.markNeedsPixelsCorrection();
  416. }
  417. }
  418. }
  419. if (widget.tabs.length > _tabKeys.length) {
  420. final int delta = widget.tabs.length - _tabKeys.length;
  421. _tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
  422. } else if (widget.tabs.length < _tabKeys.length) {
  423. _tabKeys.removeRange(widget.tabs.length, _tabKeys.length);
  424. }
  425. }
  426. @override
  427. void dispose() {
  428. _indicatorPainter!.dispose();
  429. if (_controllerIsValid) {
  430. _controller!.animation!.removeListener(_handleTabControllerAnimationTick);
  431. _controller!.removeListener(_handleTabControllerTick);
  432. }
  433. _controller = null;
  434. // We don't own the _controller Animation, so it's not disposed here.
  435. super.dispose();
  436. }
  437. int get maxTabIndex => _indicatorPainter!.maxTabIndex;
  438. double _tabScrollOffset(
  439. int index, double viewportWidth, double minExtent, double maxExtent) {
  440. if (!widget.isScrollable) {
  441. return 0.0;
  442. }
  443. double tabCenter = _indicatorPainter!.centerOf(index);
  444. double paddingStart;
  445. switch (Directionality.of(context)) {
  446. case TextDirection.rtl:
  447. paddingStart = widget.padding?.resolve(TextDirection.rtl).right ?? 0;
  448. tabCenter = _tabStripWidth - tabCenter;
  449. break;
  450. case TextDirection.ltr:
  451. paddingStart = widget.padding?.resolve(TextDirection.ltr).left ?? 0;
  452. break;
  453. }
  454. return clampDouble(
  455. tabCenter + paddingStart - viewportWidth / 2.0, minExtent, maxExtent);
  456. }
  457. double _tabCenteredScrollOffset(int index) {
  458. final ScrollPosition position = _scrollController!.position;
  459. return _tabScrollOffset(index, position.viewportDimension,
  460. position.minScrollExtent, position.maxScrollExtent);
  461. }
  462. double _initialScrollOffset(
  463. double viewportWidth, double minExtent, double maxExtent) {
  464. return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
  465. }
  466. void _scrollToCurrentIndex() {
  467. final double offset = _tabCenteredScrollOffset(_currentIndex);
  468. _scrollController!
  469. .animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
  470. }
  471. void _scrollToControllerValue() {
  472. final double? leadingPosition =
  473. _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
  474. final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
  475. final double? trailingPosition = _currentIndex < maxTabIndex
  476. ? _tabCenteredScrollOffset(_currentIndex + 1)
  477. : null;
  478. final double index = _controller!.index.toDouble();
  479. final double value = _controller!.animation!.value;
  480. final double offset;
  481. if (value == index - 1.0) {
  482. offset = leadingPosition ?? middlePosition;
  483. } else if (value == index + 1.0) {
  484. offset = trailingPosition ?? middlePosition;
  485. } else if (value == index) {
  486. offset = middlePosition;
  487. } else if (value < index) {
  488. offset = leadingPosition == null
  489. ? middlePosition
  490. : lerpDouble(middlePosition, leadingPosition, index - value)!;
  491. } else {
  492. offset = trailingPosition == null
  493. ? middlePosition
  494. : lerpDouble(middlePosition, trailingPosition, value - index)!;
  495. }
  496. _scrollController!.jumpTo(offset);
  497. }
  498. void _handleTabControllerAnimationTick() {
  499. assert(mounted);
  500. if (!_controller!.indexIsChanging && widget.isScrollable) {
  501. // Sync the TabBar's scroll position with the TabBarView's PageView.
  502. _currentIndex = _controller!.index;
  503. _scrollToControllerValue();
  504. }
  505. }
  506. void _handleTabControllerTick() {
  507. if (_controller!.index != _currentIndex) {
  508. _prevIndex = _currentIndex;
  509. _currentIndex = _controller!.index;
  510. _triggerAnimation();
  511. if (widget.isScrollable) {
  512. _scrollToCurrentIndex();
  513. }
  514. }
  515. setState(() {
  516. // Rebuild the tabs after a (potentially animated) index change
  517. // has completed.
  518. });
  519. }
  520. void _triggerAnimation() {
  521. // reset the animation so it's ready to go
  522. _animationController
  523. ..reset()
  524. ..forward();
  525. }
  526. // Called each time layout completes.
  527. void _saveTabOffsets(
  528. List<double> tabOffsets, TextDirection textDirection, double width) {
  529. _tabStripWidth = width;
  530. _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
  531. }
  532. void _handleTap(int index) {
  533. assert(index >= 0 && index < widget.tabs.length);
  534. _controller?.animateTo(index);
  535. widget.onTap?.call(index);
  536. }
  537. Widget _buildStyledTab(Widget child, int index) {
  538. final tabBarTheme = TabBarTheme.of(context);
  539. final double animationValue;
  540. if (index == _currentIndex) {
  541. animationValue = _animationController.value;
  542. } else if (index == _prevIndex) {
  543. animationValue = 1 - _animationController.value;
  544. } else {
  545. animationValue = 0;
  546. }
  547. final TextStyle? textStyle = TextStyle.lerp(
  548. (widget.unselectedLabelStyle ??
  549. tabBarTheme.labelStyle ??
  550. DefaultTextStyle.of(context).style)
  551. .copyWith(
  552. color: widget.unselectedLabelColor,
  553. ),
  554. (widget.labelStyle ??
  555. tabBarTheme.labelStyle ??
  556. DefaultTextStyle.of(context).style)
  557. .copyWith(
  558. color: widget.labelColor,
  559. ),
  560. animationValue);
  561. final Color? textColor = Color.lerp(
  562. widget.unselectedLabelColor, widget.labelColor, animationValue);
  563. final Color? borderColor = Color.lerp(
  564. widget.unselectedBorderColor, widget.borderColor, animationValue);
  565. BoxDecoration? boxDecoration = BoxDecoration.lerp(
  566. BoxDecoration(
  567. color: widget.unselectedDecoration?.color ??
  568. widget.unselectedBackgroundColor ??
  569. Colors.transparent,
  570. boxShadow: widget.unselectedDecoration?.boxShadow,
  571. gradient: widget.unselectedDecoration?.gradient,
  572. borderRadius: widget.useToggleButtonStyle
  573. ? null
  574. : BorderRadius.circular(widget.borderRadius),
  575. ),
  576. BoxDecoration(
  577. color: widget.decoration?.color ??
  578. widget.backgroundColor ??
  579. Colors.transparent,
  580. boxShadow: widget.decoration?.boxShadow,
  581. gradient: widget.decoration?.gradient,
  582. borderRadius: widget.useToggleButtonStyle
  583. ? null
  584. : BorderRadius.circular(widget.borderRadius),
  585. ),
  586. animationValue);
  587. if (widget.useToggleButtonStyle &&
  588. widget.borderWidth > 0 &&
  589. boxDecoration != null) {
  590. if (index == 0) {
  591. boxDecoration = boxDecoration.copyWith(
  592. border: Border(
  593. right: BorderSide(
  594. color: widget.unselectedBorderColor ?? Colors.transparent,
  595. width: widget.borderWidth / 2,
  596. ),
  597. ),
  598. );
  599. } else if (index == widget.tabs.length - 1) {
  600. boxDecoration = boxDecoration.copyWith(
  601. border: Border(
  602. left: BorderSide(
  603. color: widget.unselectedBorderColor ?? Colors.transparent,
  604. width: widget.borderWidth / 2,
  605. ),
  606. ),
  607. );
  608. } else {
  609. boxDecoration = boxDecoration.copyWith(
  610. border: Border.symmetric(
  611. vertical: BorderSide(
  612. color: widget.unselectedBorderColor ?? Colors.transparent,
  613. width: widget.borderWidth / 2,
  614. ),
  615. ),
  616. );
  617. }
  618. }
  619. return Padding(
  620. key: _tabKeys[index],
  621. // padding for the buttons
  622. padding:
  623. widget.useToggleButtonStyle ? EdgeInsets.zero : widget.buttonMargin,
  624. child: TextButton(
  625. onPressed: () => _handleTap(index),
  626. style: ButtonStyle(
  627. elevation: MaterialStateProperty.all(
  628. widget.useToggleButtonStyle ? 0 : widget.elevation),
  629. /// give a pretty small minimum size
  630. minimumSize: MaterialStateProperty.all(const Size(10, 10)),
  631. padding: MaterialStateProperty.all(EdgeInsets.zero),
  632. textStyle: MaterialStateProperty.all(textStyle),
  633. foregroundColor: MaterialStateProperty.all(textColor),
  634. tapTargetSize: MaterialTapTargetSize.shrinkWrap,
  635. shape: MaterialStateProperty.all(
  636. widget.useToggleButtonStyle
  637. ? const RoundedRectangleBorder(
  638. side: BorderSide.none,
  639. borderRadius: BorderRadius.zero,
  640. )
  641. : RoundedRectangleBorder(
  642. side: (widget.borderWidth == 0)
  643. ? BorderSide.none
  644. : BorderSide(
  645. color: borderColor ?? Colors.transparent,
  646. width: widget.borderWidth,
  647. ),
  648. borderRadius: BorderRadius.circular(widget.borderRadius),
  649. ),
  650. ),
  651. ),
  652. child: Ink(
  653. decoration: boxDecoration,
  654. child: Container(
  655. padding: widget.labelPadding,
  656. alignment: Alignment.center,
  657. child: child,
  658. ),
  659. ),
  660. ),
  661. );
  662. }
  663. bool _debugScheduleCheckHasValidTabsCount() {
  664. if (_debugHasScheduledValidTabsCountCheck) {
  665. return true;
  666. }
  667. WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
  668. _debugHasScheduledValidTabsCountCheck = false;
  669. if (!mounted) {
  670. return;
  671. }
  672. assert(() {
  673. if (_controller!.length != widget.tabs.length) {
  674. throw FlutterError(
  675. "Controller's length property (${_controller!.length}) does not match the "
  676. "number of tabs (${widget.tabs.length}) present in TabBar's tabs property.",
  677. );
  678. }
  679. return true;
  680. }());
  681. });
  682. _debugHasScheduledValidTabsCountCheck = true;
  683. return true;
  684. }
  685. @override
  686. Widget build(BuildContext context) {
  687. assert(_debugScheduleCheckHasValidTabsCount());
  688. if (_controller!.length == 0) {
  689. return Container(
  690. height: _kTabHeight +
  691. widget.labelPadding.vertical +
  692. widget.buttonMargin.vertical,
  693. );
  694. }
  695. final List<Widget> wrappedTabs =
  696. List<Widget>.generate(widget.tabs.length, (int index) {
  697. return _buildStyledTab(widget.tabs[index], index);
  698. });
  699. final int tabCount = widget.tabs.length;
  700. // Add the tap handler to each tab. If the tab bar is not scrollable,
  701. // then give all of the tabs equal flexibility so that they each occupy
  702. // the same share of the tab bar's overall width.
  703. for (int index = 0; index < tabCount; index += 1) {
  704. if (!widget.isScrollable) {
  705. wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
  706. }
  707. }
  708. Widget tabBar = AnimatedBuilder(
  709. animation: _animationController,
  710. key: _tabsParentKey,
  711. builder: (context, child) {
  712. Widget tabBarTemp = _TabLabelBar(
  713. onPerformLayout: _saveTabOffsets,
  714. children: wrappedTabs,
  715. );
  716. if (widget.useToggleButtonStyle) {
  717. tabBarTemp = Material(
  718. shape: widget.useToggleButtonStyle
  719. ? RoundedRectangleBorder(
  720. side: (widget.borderWidth == 0)
  721. ? BorderSide.none
  722. : BorderSide(
  723. color: widget.borderColor ?? Colors.transparent,
  724. width: widget.borderWidth,
  725. style: BorderStyle.solid,
  726. ),
  727. borderRadius: BorderRadius.circular(widget.borderRadius),
  728. )
  729. : null,
  730. elevation: widget.useToggleButtonStyle ? widget.elevation : 0,
  731. clipBehavior: Clip.antiAliasWithSaveLayer,
  732. child: tabBarTemp,
  733. );
  734. }
  735. return CustomPaint(
  736. painter: _indicatorPainter,
  737. child: tabBarTemp,
  738. );
  739. },
  740. );
  741. if (widget.isScrollable) {
  742. _scrollController ??= _TabBarScrollController(this);
  743. tabBar = SingleChildScrollView(
  744. dragStartBehavior: widget.dragStartBehavior,
  745. scrollDirection: Axis.horizontal,
  746. controller: _scrollController,
  747. padding: widget.padding,
  748. physics: widget.physics,
  749. child: tabBar,
  750. );
  751. } else if (widget.padding != null) {
  752. tabBar = Padding(
  753. padding: widget.padding!,
  754. child: tabBar,
  755. );
  756. }
  757. return tabBar;
  758. }
  759. }