Skip to content

Instantly share code, notes, and snippets.

@mondoktamas
Created January 13, 2022 09:40
Show Gist options
  • Save mondoktamas/2302f1bc633b6f2ad0bf03161c1e5900 to your computer and use it in GitHub Desktop.
Save mondoktamas/2302f1bc633b6f2ad0bf03161c1e5900 to your computer and use it in GitHub Desktop.
// 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