BottomTabs.dart 49 KB

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