Created
January 13, 2022 09:40
-
-
Save mondoktamas/2302f1bc633b6f2ad0bf03161c1e5900 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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:math' as math; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/gestures.dart' show DragStartBehavior; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
const double _kTabHeight = 46.0; | |
const double _kTextAndIconTabHeight = 72.0; | |
class _TabStyle extends AnimatedWidget { | |
const _TabStyle({ | |
Key? key, | |
required Animation<double> animation, | |
required this.selected, | |
required this.labelColor, | |
required this.unselectedLabelColor, | |
required this.labelStyle, | |
required 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 defaultStyle = | |
(labelStyle ?? tabBarTheme.labelStyle ?? themeData.primaryTextTheme.bodyText1!).copyWith(inherit: true); | |
final TextStyle defaultUnselectedStyle = (unselectedLabelStyle ?? | |
tabBarTheme.unselectedLabelStyle ?? | |
labelStyle ?? | |
themeData.primaryTextTheme.bodyText1!) | |
.copyWith(inherit: true); | |
final TextStyle textStyle = selected | |
? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)! | |
: TextStyle.lerp(defaultUnselectedStyle, defaultStyle, 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: IconTheme.merge( | |
data: IconThemeData( | |
size: 24.0, | |
color: color, | |
), | |
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, | |
}) : 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>[], | |
required 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) => _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); | |
} | |
// 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, | |
required _IndicatorPainter? old, | |
required this.indicatorPadding, | |
}) : super(repaint: controller.animation) { | |
if (old != null) { | |
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); | |
} | |
} | |
final TabController controller; | |
final Decoration indicator; | |
final TabBarIndicatorSize? indicatorSize; | |
final EdgeInsetsGeometry indicatorPadding; | |
final List<GlobalKey> tabKeys; | |
// _currentTabOffsets and _currentTextDirection are set each time TabBar | |
// layout is completed. These values can be null when TabBar contains no | |
// tabs, since there are nothing to lay out. | |
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; | |
} | |
final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection); | |
final Rect rect = Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height); | |
if (!(rect.size >= insets.collapsedSize)) { | |
throw FlutterError( | |
'indicatorPadding insets should be less than Tab Size\n' | |
'Rect Size : ${rect.size}, Insets: ${insets.toString()}', | |
); | |
} | |
return insets.deflateRect(rect); | |
} | |
@override | |
void paint(Canvas canvas, Size size) { | |
_needsPaint = false; | |
_painter ??= indicator.createBoxPainter(markNeedsPaint); | |
final double index = controller.index.toDouble(); | |
final double value = controller.animation!.value; | |
final bool ltr = index > value; | |
final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex); | |
final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex); | |
final Rect fromRect = indicatorRect(size, from); | |
final Rect toRect = indicatorRect(size, to); | |
_currentRect = Rect.lerp(fromRect, toRect, (value - from).abs()); | |
assert(_currentRect != null); | |
final ImageConfiguration configuration = ImageConfiguration( | |
size: _currentRect!.size, | |
textDirection: _currentTextDirection, | |
); | |
_painter!.paint(canvas, _currentRect!.topLeft, configuration); | |
} | |
@override | |
bool shouldRepaint(_IndicatorPainter old) => | |
_needsPaint || | |
controller != old.controller || | |
indicator != old.indicator || | |
tabKeys.length != old.tabKeys.length || | |
(!listEquals(_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 (controller.animation != null) { | |
super.removeStatusListener(listener); | |
} | |
} | |
@override | |
void removeListener(VoidCallback listener) { | |
if (controller.animation != 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 (controller.animation != null) { | |
super.removeStatusListener(listener); | |
} | |
} | |
@override | |
void removeListener(VoidCallback listener) { | |
if (controller.animation != null) { | |
super.removeListener(listener); | |
} | |
} | |
@override | |
double get value { | |
assert(!controller.indexIsChanging); | |
final double controllerMaxValue = (controller.length - 1).toDouble(); | |
final double controllerValue = controller.animation!.value.clamp(0.0, controllerMaxValue); | |
return (controllerValue - index.toDouble()).abs().clamp(0.0, 1.0); | |
} | |
} | |
/// 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]. | |
/// | |
/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40} | |
/// | |
/// 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. | |
/// | |
/// {@tool dartpad} | |
/// This sample shows the implementation of [TabBar] and [TabBarView] using a [DefaultTabController]. | |
/// Each [Tab] corresponds to a child of the [TabBarView] in the order they are written. | |
/// | |
/// ** See code in examples/api/lib/material/tabs/tab_bar.0.dart ** | |
/// {@end-tool} | |
/// | |
/// {@tool dartpad} | |
/// [TabBar] can also be implemented by using a [TabController] which provides more options | |
/// to control the behavior of the [TabBar] and [TabBarView]. This can be used instead of | |
/// a [DefaultTabController], demonstrated below. | |
/// | |
/// ** See code in examples/api/lib/material/tabs/tab_bar.1.dart ** | |
/// {@end-tool} | |
/// | |
/// See also: | |
/// | |
/// * [TabBarView], which displays page views that correspond to each tab. | |
/// * [TabBar], which is used to display the [Tab] that corresponds to each page of the [TabBarView]. | |
class CustomTabBar 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 or provided from [TabBarTheme], | |
/// then [indicatorWeight], [indicatorPadding], and [indicatorColor] are ignored. | |
const CustomTabBar({ | |
Key? key, | |
required this.tabs, | |
this.controller, | |
this.padding, | |
this.margin, | |
this.indicatorColor, | |
this.automaticIndicatorColorAdjustment = true, | |
this.indicatorWeight = 2.0, | |
this.indicatorPadding = EdgeInsets.zero, | |
this.indicator, | |
this.tabBarDecoration, | |
this.indicatorSize, | |
this.labelColor, | |
this.labelStyle, | |
this.labelPadding, | |
this.unselectedLabelColor, | |
this.unselectedLabelStyle, | |
this.dragStartBehavior = DragStartBehavior.start, | |
this.overlayColor, | |
this.mouseCursor, | |
this.enableFeedback, | |
this.onTap, | |
}) : assert(indicator != 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; | |
/// The amount of space by which to inset the tab bar. | |
final EdgeInsetsGeometry? padding; | |
/// The margin of the tab bar. | |
final EdgeInsetsGeometry? margin; | |
/// 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 or provided from [TabBarTheme], | |
/// 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 or provided from [TabBarTheme], | |
/// this property is ignored. | |
final double indicatorWeight; | |
/// Padding for indicator. | |
/// This property will now no longer be ignored even if indicator is declared | |
/// or provided by [TabBarTheme] | |
/// | |
/// 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 default value of [indicatorPadding] is [EdgeInsets.zero]. | |
final EdgeInsetsGeometry indicatorPadding; | |
/// Defines the appearance of the selected tab indicator. | |
/// | |
/// If [indicator] is specified or provided from [TabBarTheme], | |
/// the [indicatorColor], and [indicatorWeight] 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; | |
/// Decorates the tab bar container itself | |
final Decoration? tabBarDecoration; | |
/// Whether this tab bar should automatically adjust the [indicatorColor]. | |
/// | |
/// If [automaticIndicatorColorAdjustment] is true, | |
/// then the [indicatorColor] will be automatically adjusted to [Colors.white] | |
/// when the [indicatorColor] is same as [Material.color] of the [Material] parent widget. | |
final bool automaticIndicatorColorAdjustment; | |
/// 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 there are few tabs with both icon and text and few | |
/// tabs with only icon or text, this padding is vertically | |
/// adjusted to provide uniform padding to all tabs. | |
/// | |
/// 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; | |
/// Defines the ink response focus, hover, and splash colors. | |
/// | |
/// If non-null, it is resolved against one of [MaterialState.focused], | |
/// [MaterialState.hovered], and [MaterialState.pressed]. | |
/// | |
/// [MaterialState.pressed] triggers a ripple (an ink splash), per | |
/// the current Material Design spec. The [overlayColor] doesn't map | |
/// a state to [InkResponse.highlightColor] because a separate highlight | |
/// is not used by the current design guidelines. See | |
/// https://material.io/design/interaction/states.html#pressed | |
/// | |
/// If the overlay color is null or resolves to null, then the default values | |
/// for [InkResponse.focusColor], [InkResponse.hoverColor], [InkResponse.splashColor] | |
/// will be used instead. | |
final MaterialStateProperty<Color?>? overlayColor; | |
/// {@macro flutter.widgets.scrollable.dragStartBehavior} | |
final DragStartBehavior dragStartBehavior; | |
/// The cursor for a mouse pointer when it enters or is hovering over the | |
/// individual tab widgets. | |
/// | |
/// If this property is null, [SystemMouseCursors.click] will be used. | |
final MouseCursor? mouseCursor; | |
/// Whether detected gestures should provide acoustic and/or haptic feedback. | |
/// | |
/// For example, on Android a tap will produce a clicking sound and a long-press | |
/// will produce a short vibration, when feedback is enabled. | |
/// | |
/// Defaults to true. | |
final bool? enableFeedback; | |
/// 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 { | |
double maxHeight = _kTabHeight; | |
for (final Widget item in tabs) { | |
if (item is PreferredSizeWidget) { | |
final double itemHeight = item.preferredSize.height; | |
maxHeight = math.max(itemHeight, maxHeight); | |
} | |
} | |
return Size.fromHeight(maxHeight + indicatorWeight); | |
} | |
/// Returns whether the [TabBar] contains a tab with both text and icon. | |
/// | |
/// [TabBar] uses this to give uniform padding to all tabs in cases where | |
/// there are some tabs with both text and icon and some which contain only | |
/// text or icon. | |
bool get tabHasTextAndIcon { | |
for (final Widget item in tabs) { | |
if (item is PreferredSizeWidget) { | |
if (item.preferredSize.height == _kTextAndIconTabHeight) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
@override | |
State<CustomTabBar> createState() => _CustomTabBarState(); | |
} | |
class _CustomTabBarState extends State<CustomTabBar> { | |
TabController? _controller; | |
_IndicatorPainter? _indicatorPainter; | |
int? _currentIndex; | |
late 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. | |
// | |
// TODO(xu-baolin): Remove automatic adjustment to white color indicator | |
// with a better long-term solution. | |
// https://github.com/flutter/flutter/pull/68171#pullrequestreview-517753917 | |
if (widget.automaticIndicatorColorAdjustment && color.value == Material.of(context)?.color?.value) { | |
color = Colors.white; | |
} | |
return UnderlineTabIndicator( | |
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!.removeListener(_handleTabControllerTick); | |
} | |
_controller = newController; | |
if (_controller != null) { | |
_controller!.addListener(_handleTabControllerTick); | |
_currentIndex = _controller!.index; | |
} | |
} | |
void _initIndicatorPainter() { | |
_indicatorPainter = !_controllerIsValid | |
? null | |
: _IndicatorPainter( | |
controller: _controller!, | |
indicator: _indicator, | |
indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize, | |
indicatorPadding: widget.indicatorPadding, | |
tabKeys: _tabKeys, | |
old: _indicatorPainter, | |
); | |
} | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
assert(debugCheckHasMaterial(context)); | |
_updateTabController(); | |
_initIndicatorPainter(); | |
} | |
@override | |
void didUpdateWidget(CustomTabBar 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!.removeListener(_handleTabControllerTick); | |
} | |
_controller = null; | |
// We don't own the _controller Animation, so it's not disposed here. | |
super.dispose(); | |
} | |
int get maxTabIndex => _indicatorPainter!.maxTabIndex; | |
void _handleTabControllerTick() { | |
if (_controller!.index != _currentIndex) { | |
_currentIndex = _controller!.index; | |
} | |
setState(() {}); | |
} | |
// Called each time layout completes. | |
void _saveTabOffsets(List<double> tabOffsets, TextDirection textDirection, double width) { | |
_indicatorPainter?.saveTabOffsets(tabOffsets, textDirection); | |
} | |
void _handleTap(int index) { | |
assert(index >= 0 && index < widget.tabs.length); | |
_controller!.animateTo(index); | |
widget.onTap?.call(index); | |
} | |
Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) => _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>.generate(widget.tabs.length, (int index) { | |
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight) / 2.0; | |
EdgeInsetsGeometry? adjustedPadding; | |
if (widget.tabs[index] is PreferredSizeWidget) { | |
final PreferredSizeWidget tab = widget.tabs[index] as PreferredSizeWidget; | |
if (widget.tabHasTextAndIcon && tab.preferredSize.height == _kTabHeight) { | |
if (widget.labelPadding != null || tabBarTheme.labelPadding != null) { | |
adjustedPadding = (widget.labelPadding ?? tabBarTheme.labelPadding!) | |
.add(const EdgeInsets.symmetric(vertical: verticalAdjustment)); | |
} else { | |
adjustedPadding = const EdgeInsets.symmetric(vertical: verticalAdjustment, horizontal: 16.0); | |
} | |
} | |
} | |
return Center( | |
heightFactor: 1.0, | |
child: Padding( | |
padding: adjustedPadding ?? widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding, | |
child: KeyedSubtree( | |
key: _tabKeys[index], | |
child: widget.tabs[index], | |
), | |
), | |
); | |
}); | |
// 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( | |
mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click, | |
onTap: () { | |
_handleTap(index); | |
}, | |
enableFeedback: widget.enableFeedback ?? true, | |
overlayColor: widget.overlayColor, | |
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), | |
), | |
], | |
), | |
), | |
); | |
wrappedTabs[index] = Expanded(child: wrappedTabs[index]); | |
} | |
Widget tabBar = Container( | |
margin: widget.margin, | |
decoration: widget.tabBarDecoration, | |
padding: widget.padding, | |
child: 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, | |
), | |
), | |
), | |
); | |
return tabBar; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment