Hibok
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

915 строки
32 KiB

  1. // Copyright 2016 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 'dart:collection' show Queue;
  5. import 'dart:math' as math;
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/widgets.dart';
  8. /// Defines the layout and behavior of a [BottomNavigationBar].
  9. ///
  10. /// See also:
  11. ///
  12. /// * [BottomNavigationBar]
  13. /// * [BottomNavigationBarItem]
  14. /// * <https://material.io/design/components/bottom-navigation.html#specs>
  15. enum BottomNavigationBarType {
  16. /// The [BottomNavigationBar]'s [BottomNavigationBarItem]s have fixed width.
  17. fixed,
  18. /// The location and size of the [BottomNavigationBar] [BottomNavigationBarItem]s
  19. /// animate and labels fade in when they are tapped.
  20. shifting,
  21. }
  22. /// A material widget that's displayed at the bottom of an app for selecting
  23. /// among a small number of views, typically between three and five.
  24. ///
  25. /// The bottom navigation bar consists of multiple items in the form of
  26. /// text labels, icons, or both, laid out on top of a piece of material. It
  27. /// provides quick navigation between the top-level views of an app. For larger
  28. /// screens, side navigation may be a better fit.
  29. ///
  30. /// A bottom navigation bar is usually used in conjunction with a [Scaffold],
  31. /// where it is provided as the [Scaffold.bottomNavigationBar] argument.
  32. ///
  33. /// The bottom navigation bar's [type] changes how its [items] are displayed.
  34. /// If not specified, then it's automatically set to
  35. /// [BottomNavigationBarType.fixed] when there are less than four items, and
  36. /// [BottomNavigationBarType.shifting] otherwise.
  37. ///
  38. /// * [BottomNavigationBarType.fixed], the default when there are less than
  39. /// four [items]. The selected item is rendered with the
  40. /// [selectedItemColor] if it's non-null, otherwise the theme's
  41. /// [ThemeData.primaryColor] is used. If [backgroundColor] is null, The
  42. /// navigation bar's background color defaults to the [Material] background
  43. /// color, [ThemeData.canvasColor] (essentially opaque white).
  44. /// * [BottomNavigationBarType.shifting], the default when there are four
  45. /// or more [items]. If [selectedItemColor] is null, all items are rendered
  46. /// in white. The navigation bar's background color is the same as the
  47. /// [BottomNavigationBarItem.backgroundColor] of the selected item. In this
  48. /// case it's assumed that each item will have a different background color
  49. /// and that background color will contrast well with white.
  50. ///
  51. /// {@tool snippet --template=stateful_widget_material}
  52. /// This example shows a [BottomNavigationBar] as it is used within a [Scaffold]
  53. /// widget. The [BottomNavigationBar] has three [BottomNavigationBarItem]
  54. /// widgets and the [currentIndex] is set to index 0. The selected item is
  55. /// amber. The `_onItemTapped` function changes the selected item's index
  56. /// and displays a corresponding message in the center of the [Scaffold].
  57. ///
  58. /// ![A scaffold with a bottom navigation bar containing three bottom navigation
  59. /// bar items. The first one is selected.](https://flutter.github.io/assets-for-api-docs/assets/material/bottom_navigation_bar.png)
  60. ///
  61. /// ```dart
  62. /// int _selectedIndex = 0;
  63. /// static const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
  64. /// static const List<Widget> _widgetOptions = <Widget>[
  65. /// Text(
  66. /// 'Index 0: Home',
  67. /// style: optionStyle,
  68. /// ),
  69. /// Text(
  70. /// 'Index 1: Business',
  71. /// style: optionStyle,
  72. /// ),
  73. /// Text(
  74. /// 'Index 2: School',
  75. /// style: optionStyle,
  76. /// ),
  77. /// ];
  78. ///
  79. /// void _onItemTapped(int index) {
  80. /// setState(() {
  81. /// _selectedIndex = index;
  82. /// });
  83. /// }
  84. ///
  85. /// @override
  86. /// Widget build(BuildContext context) {
  87. /// return Scaffold(
  88. /// appBar: AppBar(
  89. /// title: const Text('BottomNavigationBar Sample'),
  90. /// ),
  91. /// body: Center(
  92. /// child: _widgetOptions.elementAt(_selectedIndex),
  93. /// ),
  94. /// bottomNavigationBar: BottomNavigationBar(
  95. /// items: const <BottomNavigationBarItem>[
  96. /// BottomNavigationBarItem(
  97. /// icon: Icon(Icons.home),
  98. /// title: Text('Home'),
  99. /// ),
  100. /// BottomNavigationBarItem(
  101. /// icon: Icon(Icons.business),
  102. /// title: Text('Business'),
  103. /// ),
  104. /// BottomNavigationBarItem(
  105. /// icon: Icon(Icons.school),
  106. /// title: Text('School'),
  107. /// ),
  108. /// ],
  109. /// currentIndex: _selectedIndex,
  110. /// selectedItemColor: Colors.amber[800],
  111. /// onTap: _onItemTapped,
  112. /// ),
  113. /// );
  114. /// }
  115. /// ```
  116. /// {@end-tool}
  117. ///
  118. /// See also:
  119. ///
  120. /// * [BottomNavigationBarItem]
  121. /// * [Scaffold]
  122. /// * <https://material.io/design/components/bottom-navigation.html>
  123. class BottomNavigationBar extends StatefulWidget {
  124. /// Creates a bottom navigation bar which is typically used as a
  125. /// [Scaffold]'s [Scaffold.bottomNavigationBar] argument.
  126. ///
  127. /// The length of [items] must be at least two and each item's icon and title
  128. /// must not be null.
  129. ///
  130. /// If [type] is null then [BottomNavigationBarType.fixed] is used when there
  131. /// are two or three [items], [BottomNavigationBarType.shifting] otherwise.
  132. ///
  133. /// The [iconSize], [selectedFontSize], [unselectedFontSize], and [elevation]
  134. /// arguments must be non-null and non-negative.
  135. ///
  136. /// If [selectedLabelStyle.color] and [unselectedLabelStyle.color] values
  137. /// are non-null, they will be used instead of [selectedItemColor] and
  138. /// [unselectedItemColor].
  139. ///
  140. /// If custom [IconThemData]s are used, you must provide both
  141. /// [selectedIconTheme] and [unselectedIconTheme], and both
  142. /// [IconThemeData.color] and [IconThemeData.size] must be set.
  143. ///
  144. /// If both [selectedLabelStyle.fontSize] and [selectedFontSize] are set,
  145. /// [selectedLabelStyle.fontSize] will be used.
  146. ///
  147. /// Only one of [selectedItemColor] and [fixedColor] can be specified. The
  148. /// former is preferred, [fixedColor] only exists for the sake of
  149. /// backwards compatibility.
  150. ///
  151. /// The [showSelectedLabels] argument must not be non-null.
  152. ///
  153. /// The [showUnselectedLabels] argument defaults to `true` if [type] is
  154. /// [BottomNavigationBarType.fixed] and `false` if [type] is
  155. /// [BottomNavigationBarType.shifting].
  156. BottomNavigationBar({
  157. Key key,
  158. @required this.items,
  159. this.onTap,
  160. this.currentIndex = 0,
  161. this.elevation = 8.0,
  162. BottomNavigationBarType type,
  163. Color fixedColor,
  164. this.backgroundColor,
  165. this.iconSize = 24.0,
  166. Color selectedItemColor,
  167. this.unselectedItemColor,
  168. this.selectedIconTheme = const IconThemeData(),
  169. this.unselectedIconTheme = const IconThemeData(),
  170. this.selectedFontSize = 14.0,
  171. this.unselectedFontSize = 12.0,
  172. this.selectedLabelStyle,
  173. this.unselectedLabelStyle,
  174. this.showSelectedLabels = true,
  175. bool showUnselectedLabels,
  176. }) : assert(items != null),
  177. assert(items.length >= 2),
  178. assert(
  179. items.every((BottomNavigationBarItem item) => item.title != null) ==
  180. true,
  181. 'Every item must have a non-null title',
  182. ),
  183. assert(0 <= currentIndex && currentIndex < items.length),
  184. assert(elevation != null && elevation >= 0.0),
  185. assert(iconSize != null && iconSize >= 0.0),
  186. assert(selectedItemColor == null || fixedColor == null,
  187. 'Either selectedItemColor or fixedColor can be specified, but not both'),
  188. assert(selectedFontSize != null && selectedFontSize >= 0.0),
  189. assert(unselectedFontSize != null && unselectedFontSize >= 0.0),
  190. assert(showSelectedLabels != null),
  191. type = _type(type, items),
  192. selectedItemColor = selectedItemColor ?? fixedColor,
  193. showUnselectedLabels =
  194. showUnselectedLabels ?? _defaultShowUnselected(_type(type, items)),
  195. super(key: key);
  196. /// Defines the appearance of the button items that are arrayed within the
  197. /// bottom navigation bar.
  198. final List<BottomNavigationBarItem> items;
  199. /// Called when one of the [items] is tapped.
  200. ///
  201. /// The stateful widget that creates the bottom navigation bar needs to keep
  202. /// track of the index of the selected [BottomNavigationBarItem] and call
  203. /// `setState` to rebuild the bottom navigation bar with the new [currentIndex].
  204. final ValueChanged<int> onTap;
  205. /// The index into [items] for the current active [BottomNavigationBarItem].
  206. final int currentIndex;
  207. /// The z-coordinate of this [BottomNavigationBar].
  208. ///
  209. /// If null, defaults to `8.0`.
  210. ///
  211. /// {@macro flutter.material.material.elevation}
  212. final double elevation;
  213. /// Defines the layout and behavior of a [BottomNavigationBar].
  214. ///
  215. /// See documentation for [BottomNavigationBarType] for information on the
  216. /// meaning of different types.
  217. final BottomNavigationBarType type;
  218. /// The value of [selectedItemColor].
  219. ///
  220. /// This getter only exists for backwards compatibility, the
  221. /// [selectedItemColor] property is preferred.
  222. Color get fixedColor => selectedItemColor;
  223. /// The color of the [BottomNavigationBar] itself.
  224. ///
  225. /// If [type] is [BottomNavigationBarType.shifting] and the
  226. /// [items]s, have [BottomNavigationBarItem.backgroundColor] set, the [item]'s
  227. /// backgroundColor will splash and overwrite this color.
  228. final Color backgroundColor;
  229. /// The size of all of the [BottomNavigationBarItem] icons.
  230. ///
  231. /// See [BottomNavigationBarItem.icon] for more information.
  232. final double iconSize;
  233. /// The color of the selected [BottomNavigationBarItem.icon] and
  234. /// [BottomNavigationBarItem.label].
  235. ///
  236. /// If null then the [ThemeData.primaryColor] is used.
  237. final Color selectedItemColor;
  238. /// The color of the unselected [BottomNavigationBarItem.icon] and
  239. /// [BottomNavigationBarItem.label]s.
  240. ///
  241. /// If null then the [TextTheme.caption]'s color is used.
  242. final Color unselectedItemColor;
  243. /// The size, opacity, and color of the icon in the currently selected
  244. /// [BottomNavigationBarItem.icon].
  245. ///
  246. /// If this is not provided, the size will default to [iconSize], the color
  247. /// will default to [selectedItemColor].
  248. ///
  249. /// It this field is provided, it must contain non-null [IconThemeData.size]
  250. /// and [IconThemeData.color] properties. Also, if this field is supplied,
  251. /// [unselectedIconTheme] must be provided.
  252. final IconThemeData selectedIconTheme;
  253. /// The size, opacity, and color of the icon in the currently unselected
  254. /// [BottomNavigationBarItem.icon]s
  255. ///
  256. /// If this is not provided, the size will default to [iconSize], the color
  257. /// will default to [unselectedItemColor].
  258. ///
  259. /// It this field is provided, it must contain non-null [IconThemeData.size]
  260. /// and [IconThemeData.color] properties. Also, if this field is supplied,
  261. /// [unselectedIconTheme] must be provided.
  262. final IconThemeData unselectedIconTheme;
  263. /// The [TextStyle] of the [BottomNavigationBarItem] labels when they are
  264. /// selected.
  265. final TextStyle selectedLabelStyle;
  266. /// The [TextStyle] of the [BottomNavigationBarItem] labels when they are not
  267. /// selected.
  268. final TextStyle unselectedLabelStyle;
  269. /// The font size of the [BottomNavigationBarItem] labels when they are selected.
  270. ///
  271. /// If [selectedLabelStyle.fontSize] is non-null, it will be used instead of this.
  272. ///
  273. /// Defaults to `14.0`.
  274. final double selectedFontSize;
  275. /// The font size of the [BottomNavigationBarItem] labels when they are not
  276. /// selected.
  277. ///
  278. /// If [unselectedLabelStyle.fontSize] is non-null, it will be used instead of this.
  279. ///
  280. /// Defaults to `12.0`.
  281. final double unselectedFontSize;
  282. /// Whether the labels are shown for the selected [BottomNavigationBarItem].
  283. final bool showUnselectedLabels;
  284. /// Whether the labels are shown for the unselected [BottomNavigationBarItem]s.
  285. final bool showSelectedLabels;
  286. // Used by the [BottomNavigationBar] constructor to set the [type] parameter.
  287. //
  288. // If type is provided, it is returned. Otherwise,
  289. // [BottomNavigationBarType.fixed] is used for 3 or fewer items, and
  290. // [BottomNavigationBarType.shifting] is used for 4+ items.
  291. static BottomNavigationBarType _type(
  292. BottomNavigationBarType type,
  293. List<BottomNavigationBarItem> items,
  294. ) {
  295. if (type != null) {
  296. return type;
  297. }
  298. return items.length <= 3
  299. ? BottomNavigationBarType.fixed
  300. : BottomNavigationBarType.shifting;
  301. }
  302. // Used by the [BottomNavigationBar] constructor to set the [showUnselected]
  303. // parameter.
  304. //
  305. // Unselected labels are shown by default for [BottomNavigationBarType.fixed],
  306. // and hidden by default for [BottomNavigationBarType.shifting].
  307. static bool _defaultShowUnselected(BottomNavigationBarType type) {
  308. switch (type) {
  309. case BottomNavigationBarType.shifting:
  310. return false;
  311. case BottomNavigationBarType.fixed:
  312. return true;
  313. }
  314. assert(false);
  315. return false;
  316. }
  317. @override
  318. _BottomNavigationBarState createState() => _BottomNavigationBarState();
  319. }
  320. // This represents a single tile in the bottom navigation bar. It is intended
  321. // to go into a flex container.
  322. class _BottomNavigationTile extends StatelessWidget {
  323. const _BottomNavigationTile(
  324. this.type,
  325. this.item,
  326. this.animation,
  327. this.iconSize, {
  328. this.onTap,
  329. this.colorTween,
  330. this.flex,
  331. this.selected = false,
  332. @required this.selectedLabelStyle,
  333. @required this.unselectedLabelStyle,
  334. @required this.selectedIconTheme,
  335. @required this.unselectedIconTheme,
  336. this.showSelectedLabels,
  337. this.showUnselectedLabels,
  338. this.indexLabel,
  339. }) : assert(type != null),
  340. assert(item != null),
  341. assert(animation != null),
  342. assert(selected != null),
  343. assert(selectedLabelStyle != null),
  344. assert(unselectedLabelStyle != null);
  345. final BottomNavigationBarType type;
  346. final BottomNavigationBarItem item;
  347. final Animation<double> animation;
  348. final double iconSize;
  349. final VoidCallback onTap;
  350. final ColorTween colorTween;
  351. final double flex;
  352. final bool selected;
  353. final IconThemeData selectedIconTheme;
  354. final IconThemeData unselectedIconTheme;
  355. final TextStyle selectedLabelStyle;
  356. final TextStyle unselectedLabelStyle;
  357. final String indexLabel;
  358. final bool showSelectedLabels;
  359. final bool showUnselectedLabels;
  360. @override
  361. Widget build(BuildContext context) {
  362. // In order to use the flex container to grow the tile during animation, we
  363. // need to divide the changes in flex allotment into smaller pieces to
  364. // produce smooth animation. We do this by multiplying the flex value
  365. // (which is an integer) by a large number.
  366. int size;
  367. final double selectedFontSize = selectedLabelStyle.fontSize;
  368. final double selectedIconSize = selectedIconTheme?.size ?? iconSize;
  369. final double unselectedIconSize = unselectedIconTheme?.size ?? iconSize;
  370. // The amount that the selected icon is bigger than the unselected icons,
  371. // (or zero if the selected icon is not bigger than the unselected icons).
  372. final double selectedIconDiff =
  373. math.max(selectedIconSize - unselectedIconSize, 0);
  374. // The amount that the unselected icons are bigger than the selected icon,
  375. // (or zero if the unselected icons are not any bigger than the selected icon).
  376. final double unselectedIconDiff =
  377. math.max(unselectedIconSize - selectedIconSize, 0);
  378. // Defines the padding for the animating icons + labels.
  379. //
  380. // The animations go from "Unselected":
  381. // =======
  382. // | <-- Padding equal to the text height + 1/2 selectedIconDiff.
  383. // | ☆
  384. // | text <-- Invisible text + padding equal to 1/2 selectedIconDiff.
  385. // =======
  386. //
  387. // To "Selected":
  388. //
  389. // =======
  390. // | <-- Padding equal to 1/2 text height + 1/2 unselectedIconDiff.
  391. // | ☆
  392. // | text
  393. // | <-- Padding equal to 1/2 text height + 1/2 unselectedIconDiff.
  394. // =======
  395. double bottomPadding;
  396. //double topPadding;
  397. if (showSelectedLabels && !showUnselectedLabels) {
  398. bottomPadding = Tween<double>(
  399. begin: selectedIconDiff / 2.0,
  400. end: selectedFontSize / 2.0 - unselectedIconDiff / 2.0,
  401. ).evaluate(animation);
  402. // topPadding = Tween<double>(
  403. // begin: selectedFontSize + selectedIconDiff / 2.0,
  404. // end: selectedFontSize / 2.0 - unselectedIconDiff / 2.0,
  405. // ).evaluate(animation);
  406. } else if (!showSelectedLabels && !showUnselectedLabels) {
  407. bottomPadding = Tween<double>(
  408. begin: selectedIconDiff / 2.0,
  409. end: unselectedIconDiff / 2.0,
  410. ).evaluate(animation);
  411. // topPadding = Tween<double>(
  412. // begin: selectedFontSize + selectedIconDiff / 2.0,
  413. // end: selectedFontSize + unselectedIconDiff / 2.0,
  414. // ).evaluate(animation);
  415. } else {
  416. bottomPadding = Tween<double>(
  417. begin: selectedFontSize / 2.0 + selectedIconDiff / 2.0,
  418. end: selectedFontSize / 2.0 + unselectedIconDiff / 2.0,
  419. ).evaluate(animation);
  420. // topPadding = Tween<double>(
  421. // begin: selectedFontSize / 2.0 + selectedIconDiff / 2.0,
  422. // end: selectedFontSize / 2.0 + unselectedIconDiff / 2.0,
  423. // ).evaluate(animation);
  424. }
  425. switch (type) {
  426. case BottomNavigationBarType.fixed:
  427. size = 1;
  428. break;
  429. case BottomNavigationBarType.shifting:
  430. size = (flex * 1000.0).round();
  431. break;
  432. }
  433. return Expanded(
  434. flex: size,
  435. child: Semantics(
  436. container: true,
  437. selected: selected,
  438. child: Focus(
  439. child: Stack(
  440. children: <Widget>[
  441. InkResponse(
  442. onTap: onTap,
  443. child: Padding(
  444. padding:
  445. EdgeInsets.only(top: 8, bottom: bottomPadding),
  446. child: _TileIcon(
  447. colorTween: colorTween,
  448. animation: animation,
  449. iconSize: iconSize,
  450. selected: selected,
  451. item: item,
  452. selectedIconTheme: selectedIconTheme,
  453. unselectedIconTheme: unselectedIconTheme,
  454. )),
  455. ),
  456. Semantics(
  457. label: indexLabel,
  458. ),
  459. ],
  460. ),
  461. ),
  462. ),
  463. );
  464. }
  465. }
  466. class _TileIcon extends StatelessWidget {
  467. const _TileIcon({
  468. Key key,
  469. @required this.colorTween,
  470. @required this.animation,
  471. @required this.iconSize,
  472. @required this.selected,
  473. @required this.item,
  474. @required this.selectedIconTheme,
  475. @required this.unselectedIconTheme,
  476. }) : assert(selected != null),
  477. assert(item != null),
  478. super(key: key);
  479. final ColorTween colorTween;
  480. final Animation<double> animation;
  481. final double iconSize;
  482. final bool selected;
  483. final BottomNavigationBarItem item;
  484. final IconThemeData selectedIconTheme;
  485. final IconThemeData unselectedIconTheme;
  486. @override
  487. Widget build(BuildContext context) {
  488. final Color iconColor = colorTween.evaluate(animation);
  489. final IconThemeData defaultIconTheme = IconThemeData(
  490. color: iconColor,
  491. size: iconSize,
  492. );
  493. final IconThemeData iconThemeData = IconThemeData.lerp(
  494. defaultIconTheme.merge(unselectedIconTheme),
  495. defaultIconTheme.merge(selectedIconTheme),
  496. animation.value,
  497. );
  498. return Align(
  499. alignment: Alignment.topCenter,
  500. heightFactor: 1.0,
  501. child: Container(
  502. child: IconTheme(
  503. data: iconThemeData,
  504. child: selected ? item.activeIcon : item.icon,
  505. ),
  506. ),
  507. );
  508. }
  509. }
  510. class _BottomNavigationBarState extends State<BottomNavigationBar>
  511. with TickerProviderStateMixin {
  512. List<AnimationController> _controllers = <AnimationController>[];
  513. List<CurvedAnimation> _animations;
  514. // A queue of color splashes currently being animated.
  515. final Queue<_Circle> _circles = Queue<_Circle>();
  516. // Last splash circle's color, and the final color of the control after
  517. // animation is complete.
  518. Color _backgroundColor;
  519. static final Animatable<double> _flexTween =
  520. Tween<double>(begin: 1.0, end: 1.5);
  521. void _resetState() {
  522. for (AnimationController controller in _controllers) controller.dispose();
  523. for (_Circle circle in _circles) circle.dispose();
  524. _circles.clear();
  525. _controllers =
  526. List<AnimationController>.generate(widget.items.length, (int index) {
  527. return AnimationController(
  528. duration: kThemeAnimationDuration,
  529. vsync: this,
  530. )..addListener(_rebuild);
  531. });
  532. _animations =
  533. List<CurvedAnimation>.generate(widget.items.length, (int index) {
  534. return CurvedAnimation(
  535. parent: _controllers[index],
  536. curve: Curves.fastOutSlowIn,
  537. reverseCurve: Curves.fastOutSlowIn.flipped,
  538. );
  539. });
  540. _controllers[widget.currentIndex].value = 1.0;
  541. _backgroundColor = widget.items[widget.currentIndex].backgroundColor;
  542. }
  543. @override
  544. void initState() {
  545. super.initState();
  546. _resetState();
  547. }
  548. void _rebuild() {
  549. setState(() {
  550. // Rebuilding when any of the controllers tick, i.e. when the items are
  551. // animated.
  552. });
  553. }
  554. @override
  555. void dispose() {
  556. for (AnimationController controller in _controllers) controller.dispose();
  557. for (_Circle circle in _circles) circle.dispose();
  558. super.dispose();
  559. }
  560. double _evaluateFlex(Animation<double> animation) =>
  561. _flexTween.evaluate(animation);
  562. void _pushCircle(int index) {
  563. if (widget.items[index].backgroundColor != null) {
  564. _circles.add(
  565. _Circle(
  566. state: this,
  567. index: index,
  568. color: widget.items[index].backgroundColor,
  569. vsync: this,
  570. )..controller.addStatusListener(
  571. (AnimationStatus status) {
  572. switch (status) {
  573. case AnimationStatus.completed:
  574. setState(() {
  575. final _Circle circle = _circles.removeFirst();
  576. _backgroundColor = circle.color;
  577. circle.dispose();
  578. });
  579. break;
  580. case AnimationStatus.dismissed:
  581. case AnimationStatus.forward:
  582. case AnimationStatus.reverse:
  583. break;
  584. }
  585. },
  586. ),
  587. );
  588. }
  589. }
  590. @override
  591. void didUpdateWidget(BottomNavigationBar oldWidget) {
  592. super.didUpdateWidget(oldWidget);
  593. // No animated segue if the length of the items list changes.
  594. if (widget.items.length != oldWidget.items.length) {
  595. _resetState();
  596. return;
  597. }
  598. if (widget.currentIndex != oldWidget.currentIndex) {
  599. switch (widget.type) {
  600. case BottomNavigationBarType.fixed:
  601. break;
  602. case BottomNavigationBarType.shifting:
  603. _pushCircle(widget.currentIndex);
  604. break;
  605. }
  606. _controllers[oldWidget.currentIndex].reverse();
  607. _controllers[widget.currentIndex].forward();
  608. } else {
  609. if (_backgroundColor != widget.items[widget.currentIndex].backgroundColor)
  610. _backgroundColor = widget.items[widget.currentIndex].backgroundColor;
  611. }
  612. }
  613. // If the given [TextStyle] has a non-null `fontSize`, it should be used.
  614. // Otherwise, the [selectedFontSize] parameter should be used.
  615. static TextStyle _effectiveTextStyle(TextStyle textStyle, double fontSize) {
  616. textStyle ??= const TextStyle();
  617. // Prefer the font size on textStyle if present.
  618. return textStyle.fontSize == null
  619. ? textStyle.copyWith(fontSize: fontSize)
  620. : textStyle;
  621. }
  622. List<Widget> _createTiles() {
  623. final MaterialLocalizations localizations =
  624. MaterialLocalizations.of(context);
  625. assert(localizations != null);
  626. final ThemeData themeData = Theme.of(context);
  627. final TextStyle effectiveSelectedLabelStyle =
  628. _effectiveTextStyle(widget.selectedLabelStyle, widget.selectedFontSize);
  629. final TextStyle effectiveUnselectedLabelStyle = _effectiveTextStyle(
  630. widget.unselectedLabelStyle, widget.unselectedFontSize);
  631. Color themeColor;
  632. switch (themeData.brightness) {
  633. case Brightness.light:
  634. themeColor = themeData.primaryColor;
  635. break;
  636. case Brightness.dark:
  637. themeColor = themeData.accentColor;
  638. break;
  639. }
  640. ColorTween colorTween;
  641. switch (widget.type) {
  642. case BottomNavigationBarType.fixed:
  643. colorTween = ColorTween(
  644. begin:
  645. widget.unselectedItemColor ?? themeData.textTheme.caption.color,
  646. end: widget.selectedItemColor ?? widget.fixedColor ?? themeColor,
  647. );
  648. break;
  649. case BottomNavigationBarType.shifting:
  650. colorTween = ColorTween(
  651. begin: widget.unselectedItemColor ?? Colors.white,
  652. end: widget.selectedItemColor ?? Colors.white,
  653. );
  654. break;
  655. }
  656. final List<Widget> tiles = <Widget>[];
  657. for (int i = 0; i < widget.items.length; i++) {
  658. tiles.add(_BottomNavigationTile(
  659. widget.type,
  660. widget.items[i],
  661. _animations[i],
  662. widget.iconSize,
  663. selectedIconTheme: widget.selectedIconTheme,
  664. unselectedIconTheme: widget.unselectedIconTheme,
  665. selectedLabelStyle: effectiveSelectedLabelStyle,
  666. unselectedLabelStyle: effectiveUnselectedLabelStyle,
  667. onTap: () {
  668. if (widget.onTap != null) widget.onTap(i);
  669. },
  670. colorTween: colorTween,
  671. flex: _evaluateFlex(_animations[i]),
  672. selected: i == widget.currentIndex,
  673. showSelectedLabels: widget.showSelectedLabels,
  674. showUnselectedLabels: widget.showUnselectedLabels,
  675. indexLabel: localizations.tabLabel(
  676. tabIndex: i + 1, tabCount: widget.items.length),
  677. ));
  678. }
  679. return tiles;
  680. }
  681. Widget _createContainer(List<Widget> tiles) {
  682. return DefaultTextStyle.merge(
  683. overflow: TextOverflow.ellipsis,
  684. child: Row(
  685. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  686. children: tiles,
  687. ),
  688. );
  689. }
  690. @override
  691. Widget build(BuildContext context) {
  692. assert(debugCheckHasDirectionality(context));
  693. assert(debugCheckHasMaterialLocalizations(context));
  694. assert(debugCheckHasMediaQuery(context));
  695. // Labels apply up to _bottomMargin padding. Remainder is media padding.
  696. final double additionalBottomPadding = math.max(
  697. MediaQuery.of(context).padding.bottom - widget.selectedFontSize / 2.0,
  698. 0.0);
  699. Color backgroundColor;
  700. switch (widget.type) {
  701. case BottomNavigationBarType.fixed:
  702. backgroundColor = widget.backgroundColor;
  703. break;
  704. case BottomNavigationBarType.shifting:
  705. backgroundColor = _backgroundColor;
  706. break;
  707. }
  708. return Semantics(
  709. explicitChildNodes: true,
  710. child: Material(
  711. elevation: widget.elevation,
  712. color: backgroundColor,
  713. child: ConstrainedBox(
  714. constraints: BoxConstraints(
  715. minHeight: kBottomNavigationBarHeight + additionalBottomPadding),
  716. child: CustomPaint(
  717. painter: _RadialPainter(
  718. circles: _circles.toList(),
  719. textDirection: Directionality.of(context),
  720. ),
  721. child: Material(
  722. // Splashes.
  723. type: MaterialType.transparency,
  724. child: Padding(
  725. padding: EdgeInsets.only(bottom: additionalBottomPadding),
  726. child: MediaQuery.removePadding(
  727. context: context,
  728. removeBottom: true,
  729. child: _createContainer(_createTiles()),
  730. ),
  731. ),
  732. ),
  733. ),
  734. ),
  735. ),
  736. );
  737. }
  738. }
  739. // Describes an animating color splash circle.
  740. class _Circle {
  741. _Circle({
  742. @required this.state,
  743. @required this.index,
  744. @required this.color,
  745. @required TickerProvider vsync,
  746. }) : assert(state != null),
  747. assert(index != null),
  748. assert(color != null) {
  749. controller = AnimationController(
  750. duration: kThemeAnimationDuration,
  751. vsync: vsync,
  752. );
  753. animation = CurvedAnimation(
  754. parent: controller,
  755. curve: Curves.fastOutSlowIn,
  756. );
  757. controller.forward();
  758. }
  759. final _BottomNavigationBarState state;
  760. final int index;
  761. final Color color;
  762. AnimationController controller;
  763. CurvedAnimation animation;
  764. double get horizontalLeadingOffset {
  765. double weightSum(Iterable<Animation<double>> animations) {
  766. // We're adding flex values instead of animation values to produce correct
  767. // ratios.
  768. return animations
  769. .map<double>(state._evaluateFlex)
  770. .fold<double>(0.0, (double sum, double value) => sum + value);
  771. }
  772. final double allWeights = weightSum(state._animations);
  773. // These weights sum to the start edge of the indexed item.
  774. final double leadingWeights =
  775. weightSum(state._animations.sublist(0, index));
  776. // Add half of its flex value in order to get to the center.
  777. return (leadingWeights +
  778. state._evaluateFlex(state._animations[index]) / 2.0) /
  779. allWeights;
  780. }
  781. void dispose() {
  782. controller.dispose();
  783. }
  784. }
  785. // Paints the animating color splash circles.
  786. class _RadialPainter extends CustomPainter {
  787. _RadialPainter({
  788. @required this.circles,
  789. @required this.textDirection,
  790. }) : assert(circles != null),
  791. assert(textDirection != null);
  792. final List<_Circle> circles;
  793. final TextDirection textDirection;
  794. // Computes the maximum radius attainable such that at least one of the
  795. // bounding rectangle's corners touches the edge of the circle. Drawing a
  796. // circle larger than this radius is not needed, since there is no perceivable
  797. // difference within the cropped rectangle.
  798. static double _maxRadius(Offset center, Size size) {
  799. final double maxX = math.max(center.dx, size.width - center.dx);
  800. final double maxY = math.max(center.dy, size.height - center.dy);
  801. return math.sqrt(maxX * maxX + maxY * maxY);
  802. }
  803. @override
  804. bool shouldRepaint(_RadialPainter oldPainter) {
  805. if (textDirection != oldPainter.textDirection) return true;
  806. if (circles == oldPainter.circles) return false;
  807. if (circles.length != oldPainter.circles.length) return true;
  808. for (int i = 0; i < circles.length; i += 1)
  809. if (circles[i] != oldPainter.circles[i]) return true;
  810. return false;
  811. }
  812. @override
  813. void paint(Canvas canvas, Size size) {
  814. for (_Circle circle in circles) {
  815. final Paint paint = Paint()..color = circle.color;
  816. final Rect rect = Rect.fromLTWH(0.0, 0.0, size.width, size.height);
  817. canvas.clipRect(rect);
  818. double leftFraction;
  819. switch (textDirection) {
  820. case TextDirection.rtl:
  821. leftFraction = 1.0 - circle.horizontalLeadingOffset;
  822. break;
  823. case TextDirection.ltr:
  824. leftFraction = circle.horizontalLeadingOffset;
  825. break;
  826. }
  827. final Offset center =
  828. Offset(leftFraction * size.width, size.height / 2.0);
  829. final Tween<double> radiusTween = Tween<double>(
  830. begin: 0.0,
  831. end: _maxRadius(center, size),
  832. );
  833. canvas.drawCircle(
  834. center,
  835. radiusTween.transform(circle.animation.value),
  836. paint,
  837. );
  838. }
  839. }
  840. }