Flutter TabBar 字體抖動臨時解決方案

// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:ui' show lerpDouble; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; const double _kTabHeight = 46.0; const double _kTextAndIconTabHeight = 72.0; /// Defines how the bounds of the selected tab indicator are computed. /// /// See also: /// /// * [TabBar], which displays a row of tabs. /// * [TabBarView], which displays a widget for the currently selected tab. /// * [TabBar.indicator], which defines the appearance of the selected tab /// indicator relative to the tab's bounds. enum TabBarIndicatorSize { /// The tab indicator's bounds are as wide as the space occupied by the tab /// in the tab bar: from the right edge of the previous tab to the left edge /// of the next tab. tab, /// The tab's bounds are only as wide as the (centered) tab widget itself. /// /// This value is used to align the tab's label, typically a [Tab] /// widget's text or icon, with the selected tab indicator. label, } /// A material design [TabBar] tab. /// /// If both [icon] and [text] are provided, the text is displayed below /// the icon. /// /// See also: /// /// * [TabBar], which displays a row of tabs. /// * [TabBarView], which displays a widget for the currently selected tab. /// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView]. /// * <https://material.io/design/components/tabs.html> class Tab extends StatelessWidget { /// Creates a material design [TabBar] tab. /// /// At least one of [text], [icon], and [child] must be non-null. The [text] /// and [child] arguments must not be used at the same time. The /// [iconMargin] is only useful when [icon] and either one of [text] or /// [child] is non-null. const Tab({ Key key, this.text, this.icon, this.iconMargin = const EdgeInsets.only(bottom: 10.0), this.child, }) : assert(text != null || child != null || icon != null), assert(!(text != null && null != child)), // TODO(goderbauer): https://github.com/dart-lang/sdk/issues/34180 super(key: key); /// The text to display as the tab's label. /// /// Must not be used in combination with [child]. final String text; /// The widget to be used as the tab's label. /// /// Usually a [Text] widget, possibly wrapped in a [Semantics] widget. /// /// Must not be used in combination with [text]. final Widget child; /// An icon to display as the tab's label. final Widget icon; /// The margin added around the tab's icon. /// /// Only useful when used in combination with [icon], and either one of /// [text] or [child] is non-null. final EdgeInsetsGeometry iconMargin; Widget _buildLabelText() { return child ?? Text(text, softWrap: false, overflow: TextOverflow.fade); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); double height; Widget label; if (icon == null) { height = _kTabHeight; label = _buildLabelText(); } else if (text == null && child == null) { height = _kTabHeight; label = icon; } else { height = _kTextAndIconTabHeight; label = Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Container( child: icon, margin: iconMargin, ), _buildLabelText(), ], ); } return SizedBox( height: height, child: Center( child: label, widthFactor: 1.0, ), ); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(StringProperty('text', text, defaultValue: null)); properties .add(DiagnosticsProperty<Widget>('icon', icon, defaultValue: null)); } } class _TabStyle extends AnimatedWidget { const _TabStyle({ Key key, Animation<double> animation, this.selected, this.labelColor, this.unselectedLabelColor, this.labelStyle, this.unselectedLabelStyle, @required this.child, }) : super(key: key, listenable: animation); final TextStyle labelStyle; final TextStyle unselectedLabelStyle; final bool selected; final Color labelColor; final Color unselectedLabelColor; final Widget child; @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); final TabBarTheme tabBarTheme = TabBarTheme.of(context); final Animation<double> animation = listenable as Animation<double>; // To enable TextStyle.lerp(style1, style2, value), both styles must have // the same value of inherit. Force that to be inherit=true here. final TextStyle defaultUnselectedStyle = (unselectedLabelStyle ?? tabBarTheme.unselectedLabelStyle ?? labelStyle ?? themeData.primaryTextTheme.bodyText1) .copyWith(inherit: true); final TextStyle defaultStyle = (labelStyle ?? tabBarTheme.labelStyle ?? themeData.primaryTextTheme.bodyText1) .copyWith(inherit: true).copyWith(fontSize:defaultUnselectedStyle.fontSize); final TextStyle textStyle = selected ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value) : TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value); final double multiple = labelStyle.fontSize / unselectedLabelStyle.fontSize; final double _scale = selected ? lerpDouble(multiple, 1, animation.value) : lerpDouble(1, multiple, animation.value); final Color selectedColor = labelColor ?? tabBarTheme.labelColor ?? themeData.primaryTextTheme.bodyText1.color; final Color unselectedColor = unselectedLabelColor ?? tabBarTheme.unselectedLabelColor ?? selectedColor.withAlpha(0xB2); // 70% alpha final Color color = selected ? Color.lerp(selectedColor, unselectedColor, animation.value) : Color.lerp(unselectedColor, selectedColor, animation.value); return DefaultTextStyle( style: textStyle.copyWith(color: color), // child: child, child: IconTheme.merge( data: IconThemeData( size: 24.0, color: color, ), child: Transform.scale(scale: _scale, child: child), ), ); } } typedef _LayoutCallback = void Function( List<double> xOffsets, TextDirection textDirection, double width); class _TabLabelBarRenderer extends RenderFlex { _TabLabelBarRenderer({ List<RenderBox> children, @required Axis direction, @required MainAxisSize mainAxisSize, @required MainAxisAlignment mainAxisAlignment, @required CrossAxisAlignment crossAxisAlignment, @required TextDirection textDirection, @required VerticalDirection verticalDirection, @required this.onPerformLayout, }) : assert(onPerformLayout != null), assert(textDirection != null), super( children: children, direction: direction, mainAxisSize: mainAxisSize, mainAxisAlignment: mainAxisAlignment, crossAxisAlignment: crossAxisAlignment, textDirection: textDirection, verticalDirection: verticalDirection, ); _LayoutCallback onPerformLayout; @override void performLayout() { super.performLayout(); // xOffsets will contain childCount+1 values, giving the offsets of the // leading edge of the first tab as the first value, of the leading edge of // the each subsequent tab as each subsequent value, and of the trailing // edge of the last tab as the last value. RenderBox child = firstChild; final List<double> xOffsets = <double>[]; while (child != null) { final FlexParentData childParentData = child.parentData as FlexParentData; xOffsets.add(childParentData.offset.dx); assert(child.parentData == childParentData); child = childParentData.nextSibling; } assert(textDirection != null); switch (textDirection) { case TextDirection.rtl: xOffsets.insert(0, size.width); break; case TextDirection.ltr: xOffsets.add(size.width); break; } onPerformLayout(xOffsets, textDirection, size.width); } } // This class and its renderer class only exist to report the widths of the tabs // upon layout. The tab widths are only used at paint time (see _IndicatorPainter) // or in response to input. class _TabLabelBar extends Flex { _TabLabelBar({ Key key, List<Widget> children = const <Widget>[], this.onPerformLayout, }) : super( key: key, children: children, direction: Axis.horizontal, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, verticalDirection: VerticalDirection.down, ); final _LayoutCallback onPerformLayout; @override RenderFlex createRenderObject(BuildContext context) { return _TabLabelBarRenderer( direction: direction, mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: crossAxisAlignment, textDirection: getEffectiveTextDirection(context), verticalDirection: verticalDirection, onPerformLayout: onPerformLayout, ); } @override void updateRenderObject( BuildContext context, _TabLabelBarRenderer renderObject) { super.updateRenderObject(context, renderObject); renderObject.onPerformLayout = onPerformLayout; } } double _indexChangeProgress(TabController controller) { final double controllerValue = controller.animation.value; final double previousIndex = controller.previousIndex.toDouble(); final double currentIndex = controller.index.toDouble(); // The controller's offset is changing because the user is dragging the // TabBarView's PageView to the left or right. if (!controller.indexIsChanging) return (currentIndex - controllerValue).abs().clamp(0.0, 1.0) as double; // The TabController animation's value is changing from previousIndex to currentIndex. return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs(); } class _IndicatorPainter extends CustomPainter { _IndicatorPainter({ @required this.controller, @required this.indicator, @required this.indicatorSize, @required this.tabKeys, _IndicatorPainter old, }) : assert(controller != null), assert(indicator != null), super(repaint: controller.animation) { if (old != null) saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); } final TabController controller; final Decoration indicator; final TabBarIndicatorSize indicatorSize; final List<GlobalKey> tabKeys; List<double> _currentTabOffsets; TextDirection _currentTextDirection; Rect _currentRect; BoxPainter _painter; bool _needsPaint = false; void markNeedsPaint() { _needsPaint = true; } void dispose() { _painter?.dispose(); } void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) { _currentTabOffsets = tabOffsets; _currentTextDirection = textDirection; } // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab. int get maxTabIndex => _currentTabOffsets.length - 2; double centerOf(int tabIndex) { assert(_currentTabOffsets != null); assert(_currentTabOffsets.isNotEmpty); assert(tabIndex >= 0); assert(tabIndex <= maxTabIndex); return (_currentTabOffsets[tabIndex] + _currentTabOffsets[tabIndex + 1]) / 2.0; } Rect indicatorRect(Size tabBarSize, int tabIndex) { assert(_currentTabOffsets != null); assert(_currentTextDirection != null); assert(_currentTabOffsets.isNotEmpty); assert(tabIndex >= 0); assert(tabIndex <= maxTabIndex); double tabLeft, tabRight; switch (_currentTextDirection) { case TextDirection.rtl: tabLeft = _currentTabOffsets[tabIndex + 1]; tabRight = _currentTabOffsets[tabIndex]; break; case TextDirection.ltr: tabLeft = _currentTabOffsets[tabIndex]; tabRight = _currentTabOffsets[tabIndex + 1]; break; } if (indicatorSize == TabBarIndicatorSize.label) { final double tabWidth = tabKeys[tabIndex].currentContext.size.width; final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0; tabLeft += delta; tabRight -= delta; } return Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height); } @override void paint(Canvas canvas, Size size) { _needsPaint = false; _painter ??= indicator.createBoxPainter(markNeedsPaint); if (controller.indexIsChanging) { // The user tapped on a tab, the tab controller's animation is running. final Rect targetRect = indicatorRect(size, controller.index); _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller)); } else { // The user is dragging the TabBarView's PageView left or right. final int currentIndex = controller.index; final Rect previous = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null; final Rect middle = indicatorRect(size, currentIndex); final Rect next = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null; final double index = controller.index.toDouble(); final double value = controller.animation.value; if (value == index - 1.0) _currentRect = previous ?? middle; else if (value == index + 1.0) _currentRect = next ?? middle; else if (value == index) _currentRect = middle; else if (value < index) _currentRect = previous == null ? middle : Rect.lerp(middle, previous, index - value); else _currentRect = next == null ? middle : Rect.lerp(middle, next, value - index); } assert(_currentRect != null); final ImageConfiguration configuration = ImageConfiguration( size: _currentRect.size, textDirection: _currentTextDirection, ); _painter.paint(canvas, _currentRect.topLeft, configuration); } static bool _tabOffsetsEqual(List<double> a, List<double> b) { // TODO(shihaohong): The following null check should be replaced when a fix // for https://github.com/flutter/flutter/issues/40014 is available. if (a == null || b == null || a.length != b.length) return false; for (int i = 0; i < a.length; i += 1) { if (a[i] != b[i]) return false; } return true; } @override bool shouldRepaint(_IndicatorPainter old) { return _needsPaint || controller != old.controller || indicator != old.indicator || tabKeys.length != old.tabKeys.length || (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets)) || _currentTextDirection != old._currentTextDirection; } } class _ChangeAnimation extends Animation<double> with AnimationWithParentMixin<double> { _ChangeAnimation(this.controller); final TabController controller; @override Animation<double> get parent => controller.animation; @override void removeStatusListener(AnimationStatusListener listener) { if (parent != null) super.removeStatusListener(listener); } @override void removeListener(VoidCallback listener) { if (parent != null) super.removeListener(listener); } @override double get value => _indexChangeProgress(controller); } class _DragAnimation extends Animation<double> with AnimationWithParentMixin<double> { _DragAnimation(this.controller, this.index); final TabController controller; final int index; @override Animation<double> get parent => controller.animation; @override void removeStatusListener(AnimationStatusListener listener) { if (parent != null) super.removeStatusListener(listener); } @override void removeListener(VoidCallback listener) { if (parent != null) super.removeListener(listener); } @override double get value { assert(!controller.indexIsChanging); return (controller.animation.value - index.toDouble()).abs().clamp(0.0, 1.0) as double; } } // This class, and TabBarScrollController, only exist to handle the case // where a scrollable TabBar has a non-zero initialIndex. In that case we can // only compute the scroll position's initial scroll offset (the "correct" // pixels value) after the TabBar viewport width and scroll limits are known. class _TabBarScrollPosition extends ScrollPositionWithSingleContext { _TabBarScrollPosition({ ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition, this.tabBar, }) : super( physics: physics, context: context, initialPixels: null, oldPosition: oldPosition, ); final _TabBarState tabBar; bool _initialViewportDimensionWasZero; @override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { bool result = true; if (_initialViewportDimensionWasZero != true) { // If the viewport never had a non-zero dimension, we just want to jump // to the initial scroll position to avoid strange scrolling effects in // release mode: In release mode, the viewport temporarily may have a // dimension of zero before the actual dimension is calculated. In that // scenario, setting the actual dimension would cause a strange scroll // effect without this guard because the super call below would starts a // ballistic scroll activity. assert(viewportDimension != null); _initialViewportDimensionWasZero = viewportDimension != 0.0; correctPixels(tabBar._initialScrollOffset( viewportDimension, minScrollExtent, maxScrollExtent)); result = false; } return super.applyContentDimensions(minScrollExtent, maxScrollExtent) && result; } } // This class, and TabBarScrollPosition, only exist to handle the case // where a scrollable TabBar has a non-zero initialIndex. class _TabBarScrollController extends ScrollController { _TabBarScrollController(this.tabBar); final _TabBarState tabBar; @override ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) { return _TabBarScrollPosition( physics: physics, context: context, oldPosition: oldPosition, tabBar: tabBar, ); } } /// A material design widget that displays a horizontal row of tabs. /// /// Typically created as the [AppBar.bottom] part of an [AppBar] and in /// conjunction with a [TabBarView]. /// /// If a [TabController] is not provided, then a [DefaultTabController] ancestor /// must be provided instead. The tab controller's [TabController.length] must /// equal the length of the [tabs] list and the length of the /// [TabBarView.children] list. /// /// Requires one of its ancestors to be a [Material] widget. /// /// Uses values from [TabBarTheme] if it is set in the current context. /// /// To see a sample implementation, visit the [TabController] documentation. /// /// See also: /// /// * [TabBarView], which displays page views that correspond to each tab. class TabBar extends StatefulWidget implements PreferredSizeWidget { /// Creates a material design tab bar. /// /// The [tabs] argument must not be null and its length must match the [controller]'s /// [TabController.length]. /// /// If a [TabController] is not provided, then there must be a /// [DefaultTabController] ancestor. /// /// The [indicatorWeight] parameter defaults to 2, and must not be null. /// /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null. /// /// If [indicator] is not null, then [indicatorWeight], [indicatorPadding], and /// [indicatorColor] are ignored. const TabBar({ Key key, @required this.tabs, this.controller, this.isScrollable = false, this.indicatorColor, this.indicatorWeight = 2.0, this.indicatorPadding = EdgeInsets.zero, this.indicator, this.indicatorSize, this.labelColor, this.labelStyle, this.labelPadding, this.unselectedLabelColor, this.unselectedLabelStyle, this.dragStartBehavior = DragStartBehavior.start, this.onTap, }) : assert(tabs != null), assert(isScrollable != null), assert(dragStartBehavior != null), assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)), assert(indicator != null || (indicatorPadding != null)), super(key: key); /// Typically a list of two or more [Tab] widgets. /// /// The length of this list must match the [controller]'s [TabController.length] /// and the length of the [TabBarView.children] list. final List<Widget> tabs; /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of [DefaultTabController.of] /// will be used. final TabController controller; /// Whether this tab bar can be scrolled horizontally. /// /// If [isScrollable] is true, then each tab is as wide as needed for its label /// and the entire [TabBar] is scrollable. Otherwise each tab gets an equal /// share of the available space. final bool isScrollable; /// The color of the line that appears below the selected tab. /// /// If this parameter is null, then the value of the Theme's indicatorColor /// property is used. /// /// If [indicator] is specified, this property is ignored. final Color indicatorColor; /// The thickness of the line that appears below the selected tab. /// /// The value of this parameter must be greater than zero and its default /// value is 2.0. /// /// If [indicator] is specified, this property is ignored. final double indicatorWeight; /// The horizontal padding for the line that appears below the selected tab. /// /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align /// the indicator with the tab's text for [Tab] widgets and all but the /// shortest [Tab.text] values. /// /// The [EdgeInsets.top] and [EdgeInsets.bottom] values of the /// [indicatorPadding] are ignored. /// /// The default value of [indicatorPadding] is [EdgeInsets.zero]. /// /// If [indicator] is specified, this property is ignored. final EdgeInsetsGeometry indicatorPadding; /// Defines the appearance of the selected tab indicator. /// /// If [indicator] is specified, the [indicatorColor], [indicatorWeight], /// and [indicatorPadding] properties are ignored. /// /// The default, underline-style, selected tab indicator can be defined with /// [UnderlineTabIndicator]. /// /// The indicator's size is based on the tab's bounds. If [indicatorSize] /// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space /// occupied by the tab in the tab bar. If [indicatorSize] is /// [TabBarIndicatorSize.label], then the tab's bounds are only as wide as /// the tab widget itself. final Decoration indicator; /// Defines how the selected tab indicator's size is computed. /// /// The size of the selected tab indicator is defined relative to the /// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab] /// (the default) or relative to the bounds of the tab's widget if /// [indicatorSize] is [TabBarIndicatorSize.label]. /// /// The selected tab's location appearance can be refined further with /// the [indicatorColor], [indicatorWeight], [indicatorPadding], and /// [indicator] properties. final TabBarIndicatorSize indicatorSize; /// The color of selected tab labels. /// /// Unselected tab labels are rendered with the same color rendered at 70% /// opacity unless [unselectedLabelColor] is non-null. /// /// If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s /// bodyText1 text color is used. final Color labelColor; /// The color of unselected tab labels. /// /// If this property is null, unselected tab labels are rendered with the /// [labelColor] with 70% opacity. final Color unselectedLabelColor; /// The text style of the selected tab labels. /// /// If [unselectedLabelStyle] is null, then this text style will be used for /// both selected and unselected label styles. /// /// If this property is null, then the text style of the /// [ThemeData.primaryTextTheme]'s bodyText1 definition is used. final TextStyle labelStyle; /// The padding added to each of the tab labels. /// /// If this property is null, then kTabLabelPadding is used. final EdgeInsetsGeometry labelPadding; /// The text style of the unselected tab labels /// /// If this property is null, then the [labelStyle] value is used. If [labelStyle] /// is null, then the text style of the [ThemeData.primaryTextTheme]'s /// bodyText1 definition is used. final TextStyle unselectedLabelStyle; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// An optional callback that's called when the [TabBar] is tapped. /// /// The callback is applied to the index of the tab where the tap occurred. /// /// This callback has no effect on the default handling of taps. It's for /// applications that want to do a little extra work when a tab is tapped, /// even if the tap doesn't change the TabController's index. TabBar [onTap] /// callbacks should not make changes to the TabController since that would /// interfere with the default tap handler. final ValueChanged<int> onTap; /// A size whose height depends on if the tabs have both icons and text. /// /// [AppBar] uses this size to compute its own preferred size. @override Size get preferredSize { for (final Widget item in tabs) { if (item is Tab) { final Tab tab = item; if ((tab.text != null || tab.child != null) && tab.icon != null) return Size.fromHeight(_kTextAndIconTabHeight + indicatorWeight); } } return Size.fromHeight(_kTabHeight + indicatorWeight); } @override _TabBarState createState() => _TabBarState(); } class _TabBarState extends State<TabBar> { ScrollController _scrollController; TabController _controller; _IndicatorPainter _indicatorPainter; int _currentIndex; double _tabStripWidth; List<GlobalKey> _tabKeys; @override void initState() { super.initState(); // If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find // the width of tab widget i. See _IndicatorPainter.indicatorRect(). _tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList(); } Decoration get _indicator { if (widget.indicator != null) return widget.indicator; final TabBarTheme tabBarTheme = TabBarTheme.of(context); if (tabBarTheme.indicator != null) return tabBarTheme.indicator; Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor; // ThemeData tries to avoid this by having indicatorColor avoid being the // primaryColor. However, it's possible that the tab bar is on a // Material that isn't the primaryColor. In that case, if the indicator // color ends up matching the material's color, then this overrides it. // When that happens, automatic transitions of the theme will likely look // ugly as the indicator color suddenly snaps to white at one end, but it's // not clear how to avoid that any further. // // The material's color might be null (if it's a transparency). In that case // there's no good way for us to find out what the color is so we don't. if (color.value == Material.of(context).color?.value) color = Colors.white; return UnderlineTabIndicator( insets: widget.indicatorPadding, borderSide: BorderSide( width: widget.indicatorWeight, color: color, ), ); } // If the TabBar is rebuilt with a new tab controller, the caller should // dispose the old one. In that case the old controller's animation will be // null and should not be accessed. bool get _controllerIsValid => _controller?.animation != null; void _updateTabController() { final TabController newController = widget.controller ?? DefaultTabController.of(context); assert(() { if (newController == null) { throw FlutterError('No TabController for ${widget.runtimeType}.\n' 'When creating a ${widget.runtimeType}, you must either provide an explicit ' 'TabController using the "controller" property, or you must ensure that there ' 'is a DefaultTabController above the ${widget.runtimeType}.\n' 'In this case, there was neither an explicit controller nor a default controller.'); } return true; }()); if (newController == _controller) return; if (_controllerIsValid) { _controller.animation.removeListener(_handleTabControllerAnimationTick); _controller.removeListener(_handleTabControllerTick); } _controller = newController; if (_controller != null) { _controller.animation.addListener(_handleTabControllerAnimationTick); _controller.addListener(_handleTabControllerTick); _currentIndex = _controller.index; } } void _initIndicatorPainter() { _indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter( controller: _controller, indicator: _indicator, indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize, tabKeys: _tabKeys, old: _indicatorPainter, ); } @override void didChangeDependencies() { super.didChangeDependencies(); assert(debugCheckHasMaterial(context)); _updateTabController(); _initIndicatorPainter(); } @override void didUpdateWidget(TabBar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { _updateTabController(); _initIndicatorPainter(); } else if (widget.indicatorColor != oldWidget.indicatorColor || widget.indicatorWeight != oldWidget.indicatorWeight || widget.indicatorSize != oldWidget.indicatorSize || widget.indicator != oldWidget.indicator) { _initIndicatorPainter(); } if (widget.tabs.length > oldWidget.tabs.length) { final int delta = widget.tabs.length - oldWidget.tabs.length; _tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey())); } else if (widget.tabs.length < oldWidget.tabs.length) { _tabKeys.removeRange(widget.tabs.length, oldWidget.tabs.length); } } @override void dispose() { _indicatorPainter.dispose(); if (_controllerIsValid) { _controller.animation.removeListener(_handleTabControllerAnimationTick); _controller.removeListener(_handleTabControllerTick); } _controller = null; // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } int get maxTabIndex => _indicatorPainter.maxTabIndex; double _tabScrollOffset( int index, double viewportWidth, double minExtent, double maxExtent) { if (!widget.isScrollable) return 0.0; double tabCenter = _indicatorPainter.centerOf(index); switch (Directionality.of(context)) { case TextDirection.rtl: tabCenter = _tabStripWidth - tabCenter; break; case TextDirection.ltr: break; } return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent) as double; } double _tabCenteredScrollOffset(int index) { final ScrollPosition position = _scrollController.position; return _tabScrollOffset(index, position.viewportDimension, position.minScrollExtent, position.maxScrollExtent); } double _initialScrollOffset( double viewportWidth, double minExtent, double maxExtent) { return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent); } void _scrollToCurrentIndex() { final double offset = _tabCenteredScrollOffset(_currentIndex); _scrollController.animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease); } void _scrollToControllerValue() { final double leadingPosition = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null; final double middlePosition = _tabCenteredScrollOffset(_currentIndex); final double trailingPosition = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null; final double index = _controller.index.toDouble(); final double value = _controller.animation.value; double offset; if (value == index - 1.0) offset = leadingPosition ?? middlePosition; else if (value == index + 1.0) offset = trailingPosition ?? middlePosition; else if (value == index) offset = middlePosition; else if (value < index) offset = leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value); else offset = trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index); _scrollController.jumpTo(offset); } void _handleTabControllerAnimationTick() { assert(mounted); if (!_controller.indexIsChanging && widget.isScrollable) { // Sync the TabBar's scroll position with the TabBarView's PageView. _currentIndex = _controller.index; _scrollToControllerValue(); } } void _handleTabControllerTick() { if (_controller.index != _currentIndex) { _currentIndex = _controller.index; if (widget.isScrollable) _scrollToCurrentIndex(); } setState(() { // Rebuild the tabs after a (potentially animated) index change // has completed. }); } // Called each time layout completes. void _saveTabOffsets( List<double> tabOffsets, TextDirection textDirection, double width) { _tabStripWidth = width; _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection); } void _handleTap(int index) { assert(index >= 0 && index < widget.tabs.length); _controller.animateTo(index); if (widget.onTap != null) { widget.onTap(index); } } Widget _buildStyledTab( Widget child, bool selected, Animation<double> animation) { return _TabStyle( animation: animation, selected: selected, labelColor: widget.labelColor, unselectedLabelColor: widget.unselectedLabelColor, labelStyle: widget.labelStyle, unselectedLabelStyle: widget.unselectedLabelStyle, child: child, ); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); assert(() { if (_controller.length != widget.tabs.length) { throw FlutterError( "Controller's length property (${_controller.length}) does not match the " "number of tabs (${widget.tabs.length}) present in TabBar's tabs property."); } return true; }()); final MaterialLocalizations localizations = MaterialLocalizations.of(context); if (_controller.length == 0) { return Container( height: _kTabHeight + widget.indicatorWeight, ); } final TabBarTheme tabBarTheme = TabBarTheme.of(context); final List<Widget> wrappedTabs = List<Widget>(widget.tabs.length); for (int i = 0; i < widget.tabs.length; i += 1) { wrappedTabs[i] = Center( heightFactor: 1.0, child: Padding( padding: widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding, child: KeyedSubtree( key: _tabKeys[i], child: widget.tabs[i], ), ), ); } // If the controller was provided by DefaultTabController and we're part // of a Hero (typically the AppBar), then we will not be able to find the // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213. if (_controller != null) { final int previousIndex = _controller.previousIndex; if (_controller.indexIsChanging) { // The user tapped on a tab, the tab controller's animation is running. assert(_currentIndex != previousIndex); final Animation<double> animation = _ChangeAnimation(_controller); wrappedTabs[_currentIndex] = _buildStyledTab(wrappedTabs[_currentIndex], true, animation); wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation); } else { // The user is dragging the TabBarView's PageView left or right. final int tabIndex = _currentIndex; final Animation<double> centerAnimation = _DragAnimation(_controller, tabIndex); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation); if (_currentIndex > 0) { final int tabIndex = _currentIndex - 1; final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex)); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation); } if (_currentIndex < widget.tabs.length - 1) { final int tabIndex = _currentIndex + 1; final Animation<double> nextAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex)); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation); } } } // Add the tap handler to each tab. If the tab bar is not scrollable, // then give all of the tabs equal flexibility so that they each occupy // the same share of the tab bar's overall width. final int tabCount = widget.tabs.length; for (int index = 0; index < tabCount; index += 1) { wrappedTabs[index] = InkWell( onTap: () { _handleTap(index); }, child: Padding( padding: EdgeInsets.only(bottom: widget.indicatorWeight), child: Stack( children: <Widget>[ wrappedTabs[index], Semantics( selected: index == _currentIndex, label: localizations.tabLabel( tabIndex: index + 1, tabCount: tabCount), ), ], ), ), ); if (!widget.isScrollable) wrappedTabs[index] = Expanded(child: wrappedTabs[index]); } Widget tabBar = CustomPaint( painter: _indicatorPainter, child: _TabStyle( animation: kAlwaysDismissedAnimation, selected: false, labelColor: widget.labelColor, unselectedLabelColor: widget.unselectedLabelColor, labelStyle: widget.labelStyle, unselectedLabelStyle: widget.unselectedLabelStyle, child: _TabLabelBar( onPerformLayout: _saveTabOffsets, children: wrappedTabs, ), ), ); if (widget.isScrollable) { _scrollController ??= _TabBarScrollController(this); tabBar = SingleChildScrollView( dragStartBehavior: widget.dragStartBehavior, scrollDirection: Axis.horizontal, controller: _scrollController, child: tabBar, ); } return tabBar; } } /// A page view that displays the widget which corresponds to the currently /// selected tab. /// /// This widget is typically used in conjunction with a [TabBar]. /// /// If a [TabController] is not provided, then there must be a [DefaultTabController] /// ancestor. /// /// The tab controller's [TabController.length] must equal the length of the /// [children] list and the length of the [TabBar.tabs] list. /// /// To see a sample implementation, visit the [TabController] documentation. class TabBarView extends StatefulWidget { /// Creates a page view with one child per tab. /// /// The length of [children] must be the same as the [controller]'s length. const TabBarView({ Key key, @required this.children, this.controller, this.physics, this.dragStartBehavior = DragStartBehavior.start, }) : assert(children != null), assert(dragStartBehavior != null), super(key: key); /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of [DefaultTabController.of] /// will be used. final TabController controller; /// One widget per tab. /// /// Its length must match the length of the [TabBar.tabs] /// list, as well as the [controller]'s [TabController.length]. final List<Widget> children; /// How the page view should respond to user input. /// /// For example, determines how the page view continues to animate after the /// user stops dragging the page view. /// /// The physics are modified to snap to page boundaries using /// [PageScrollPhysics] prior to being used. /// /// Defaults to matching platform conventions. final ScrollPhysics physics; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; @override _TabBarViewState createState() => _TabBarViewState(); } final PageScrollPhysics _kTabBarViewPhysics = const PageScrollPhysics().applyTo(const ClampingScrollPhysics()); class _TabBarViewState extends State<TabBarView> { TabController _controller; PageController _pageController; List<Widget> _children; List<Widget> _childrenWithKey; int _currentIndex; int _warpUnderwayCount = 0; // If the TabBarView is rebuilt with a new tab controller, the caller should // dispose the old one. In that case the old controller's animation will be // null and should not be accessed. bool get _controllerIsValid => _controller?.animation != null; void _updateTabController() { final TabController newController = widget.controller ?? DefaultTabController.of(context); assert(() { if (newController == null) { throw FlutterError('No TabController for ${widget.runtimeType}.\n' 'When creating a ${widget.runtimeType}, you must either provide an explicit ' 'TabController using the "controller" property, or you must ensure that there ' 'is a DefaultTabController above the ${widget.runtimeType}.\n' 'In this case, there was neither an explicit controller nor a default controller.'); } return true; }()); if (newController == _controller) return; if (_controllerIsValid) _controller.animation.removeListener(_handleTabControllerAnimationTick); _controller = newController; if (_controller != null) _controller.animation.addListener(_handleTabControllerAnimationTick); } @override void initState() { super.initState(); _updateChildren(); } @override void didChangeDependencies() { super.didChangeDependencies(); _updateTabController(); _currentIndex = _controller?.index; _pageController = PageController(initialPage: _currentIndex ?? 0); } @override void didUpdateWidget(TabBarView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) _updateTabController(); if (widget.children != oldWidget.children && _warpUnderwayCount == 0) _updateChildren(); } @override void dispose() { if (_controllerIsValid) _controller.animation.removeListener(_handleTabControllerAnimationTick); _controller = null; // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } void _updateChildren() { _children = widget.children; _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children); } void _handleTabControllerAnimationTick() { if (_warpUnderwayCount > 0 || !_controller.indexIsChanging) return; // This widget is driving the controller's animation. if (_controller.index != _currentIndex) { _currentIndex = _controller.index; _warpToCurrentIndex(); } } Future<void> _warpToCurrentIndex() async { if (!mounted) return Future<void>.value(); if (_pageController.page == _currentIndex.toDouble()) return Future<void>.value(); final int previousIndex = _controller.previousIndex; if ((_currentIndex - previousIndex).abs() == 1) return _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease); assert((_currentIndex - previousIndex).abs() > 1); final int initialPage = _currentIndex > previousIndex ? _currentIndex - 1 : _currentIndex + 1; final List<Widget> originalChildren = _childrenWithKey; setState(() { _warpUnderwayCount += 1; _childrenWithKey = List<Widget>.from(_childrenWithKey, growable: false); final Widget temp = _childrenWithKey[initialPage]; _childrenWithKey[initialPage] = _childrenWithKey[previousIndex]; _childrenWithKey[previousIndex] = temp; }); _pageController.jumpToPage(initialPage); await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease); if (!mounted) return Future<void>.value(); setState(() { _warpUnderwayCount -= 1; if (widget.children != _children) { _updateChildren(); } else { _childrenWithKey = originalChildren; } }); } // Called when the PageView scrolls bool _handleScrollNotification(ScrollNotification notification) { if (_warpUnderwayCount > 0) return false; if (notification.depth != 0) return false; _warpUnderwayCount += 1; if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) { if ((_pageController.page - _controller.index).abs() > 1.0) { _controller.index = _pageController.page.floor(); _currentIndex = _controller.index; } _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0) as double; } else if (notification is ScrollEndNotification) { _controller.index = _pageController.page.round(); _currentIndex = _controller.index; } _warpUnderwayCount -= 1; return false; } @override Widget build(BuildContext context) { assert(() { if (_controller.length != widget.children.length) { throw FlutterError( "Controller's length property (${_controller.length}) does not match the " "number of tabs (${widget.children.length}) present in TabBar's tabs property."); } return true; }()); return NotificationListener<ScrollNotification>( onNotification: _handleScrollNotification, child: PageView( dragStartBehavior: widget.dragStartBehavior, controller: _pageController, physics: widget.physics == null ? _kTabBarViewPhysics : _kTabBarViewPhysics.applyTo(widget.physics), children: _childrenWithKey, ), ); } } /// Displays a single circle with the specified border and background colors. /// /// Used by [TabPageSelector] to indicate the selected page. class TabPageSelectorIndicator extends StatelessWidget { /// Creates an indicator used by [TabPageSelector]. /// /// The [backgroundColor], [borderColor], and [size] parameters must not be null. const TabPageSelectorIndicator({ Key key, @required this.backgroundColor, @required this.borderColor, @required this.size, }) : assert(backgroundColor != null), assert(borderColor != null), assert(size != null), super(key: key); /// The indicator circle's background color. final Color backgroundColor; /// The indicator circle's border color. final Color borderColor; /// The indicator circle's diameter. final double size; @override Widget build(BuildContext context) { return Container( width: size, height: size, margin: const EdgeInsets.all(4.0), decoration: BoxDecoration( color: backgroundColor, border: Border.all(color: borderColor), shape: BoxShape.circle, ), ); } } /// Displays a row of small circular indicators, one per tab. /// /// The selected tab's indicator is highlighted. Often used in conjunction with /// a [TabBarView]. /// /// If a [TabController] is not provided, then there must be a /// [DefaultTabController] ancestor. class TabPageSelector extends StatelessWidget { /// Creates a compact widget that indicates which tab has been selected. const TabPageSelector({ Key key, this.controller, this.indicatorSize = 12.0, this.color, this.selectedColor, }) : assert(indicatorSize != null && indicatorSize > 0.0), super(key: key); /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of /// [DefaultTabController.of] will be used. final TabController controller; /// The indicator circle's diameter (the default value is 12.0). final double indicatorSize; /// The indicator circle's fill color for unselected pages. /// /// If this parameter is null, then the indicator is filled with [Colors.transparent]. final Color color; /// The indicator circle's fill color for selected pages and border color /// for all indicator circles. /// /// If this parameter is null, then the indicator is filled with the theme's /// accent color, [ThemeData.accentColor]. final Color selectedColor; Widget _buildTabIndicator( int tabIndex, TabController tabController, ColorTween selectedColorTween, ColorTween previousColorTween, ) { Color background; if (tabController.indexIsChanging) { // The selection's animation is animating from previousValue to value. final double t = 1.0 - _indexChangeProgress(tabController); if (tabController.index == tabIndex) background = selectedColorTween.lerp(t); else if (tabController.previousIndex == tabIndex) background = previousColorTween.lerp(t); else background = selectedColorTween.begin; } else { // The selection's offset reflects how far the TabBarView has / been dragged // to the previous page (-1.0 to 0.0) or the next page (0.0 to 1.0). final double offset = tabController.offset; if (tabController.index == tabIndex) { background = selectedColorTween.lerp(1.0 - offset.abs()); } else if (tabController.index == tabIndex - 1 && offset > 0.0) { background = selectedColorTween.lerp(offset); } else if (tabController.index == tabIndex + 1 && offset < 0.0) { background = selectedColorTween.lerp(-offset); } else { background = selectedColorTween.begin; } } return TabPageSelectorIndicator( backgroundColor: background, borderColor: selectedColorTween.end, size: indicatorSize, ); } @override Widget build(BuildContext context) { final Color fixColor = color ?? Colors.transparent; final Color fixSelectedColor = selectedColor ?? Theme.of(context).accentColor; final ColorTween selectedColorTween = ColorTween(begin: fixColor, end: fixSelectedColor); final ColorTween previousColorTween = ColorTween(begin: fixSelectedColor, end: fixColor); final TabController tabController = controller ?? DefaultTabController.of(context); assert(() { if (tabController == null) { throw FlutterError('No TabController for $runtimeType.\n' 'When creating a $runtimeType, you must either provide an explicit TabController ' 'using the "controller" property, or you must ensure that there is a ' 'DefaultTabController above the $runtimeType.\n' 'In this case, there was neither an explicit controller nor a default controller.'); } return true; }()); final Animation<double> animation = CurvedAnimation( parent: tabController.animation, curve: Curves.fastOutSlowIn, ); return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget child) { return Semantics( label: 'Page ${tabController.index + 1} of ${tabController.length}', child: Row( mainAxisSize: MainAxisSize.min, children: List<Widget>.generate(tabController.length, (int tabIndex) { return _buildTabIndicator(tabIndex, tabController, selectedColorTween, previousColorTween); }).toList(), ), ); }, ); } }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章