flutter-freaccess-hub/lib/flutter_flow/flutter_flow_button_tabbar....

855 lines
28 KiB
Dart

import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
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;
typedef _LayoutCallback = void Function(
List<double> xOffsets, TextDirection textDirection, double width);
class _TabLabelBarRenderer extends RenderFlex {
_TabLabelBarRenderer({
required super.direction,
required super.mainAxisSize,
required super.mainAxisAlignment,
required super.crossAxisAlignment,
required TextDirection super.textDirection,
required super.verticalDirection,
required this.onPerformLayout,
});
_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 {
const _TabLabelBar({
required super.children,
required this.onPerformLayout,
}) : super(
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;
}
}
class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({
required this.controller,
required this.tabKeys,
required _IndicatorPainter? old,
}) : super(repaint: controller.animation) {
if (old != null) {
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
}
}
final TabController controller;
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;
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;
}
@override
void paint(Canvas canvas, Size size) {
_needsPaint = false;
}
@override
bool shouldRepaint(_IndicatorPainter old) {
return _needsPaint ||
controller != old.controller ||
tabKeys.length != old.tabKeys.length ||
(!listEquals(_currentTabOffsets, old._currentTabOffsets)) ||
_currentTextDirection != old._currentTextDirection;
}
}
// 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({
required super.physics,
required super.context,
required super.oldPosition,
required this.tabBar,
}) : super(
initialPixels: null,
);
final _FlutterFlowButtonTabBarState tabBar;
bool _viewportDimensionWasNonZero = false;
// Position should be adjusted at least once.
bool _needsPixelsCorrection = true;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
bool result = true;
if (!_viewportDimensionWasNonZero) {
_viewportDimensionWasNonZero = viewportDimension != 0.0;
}
// 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.
if (!_viewportDimensionWasNonZero || _needsPixelsCorrection) {
_needsPixelsCorrection = false;
correctPixels(tabBar._initialScrollOffset(
viewportDimension, minScrollExtent, maxScrollExtent));
result = false;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent) &&
result;
}
void markNeedsPixelsCorrection() {
_needsPixelsCorrection = true;
}
}
// 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 _FlutterFlowButtonTabBarState tabBar;
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition? oldPosition) {
return _TabBarScrollPosition(
physics: physics,
context: context,
oldPosition: oldPosition,
tabBar: tabBar,
);
}
}
/// A Flutterflow Design widget that displays a horizontal row of tabs.
class FlutterFlowButtonTabBar extends StatefulWidget
implements PreferredSizeWidget {
/// 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.
///
const FlutterFlowButtonTabBar({
super.key,
required this.tabs,
this.controller,
this.isScrollable = false,
this.useToggleButtonStyle = false,
this.dragStartBehavior = DragStartBehavior.start,
this.onTap,
this.backgroundColor,
this.unselectedBackgroundColor,
this.decoration,
this.unselectedDecoration,
this.labelStyle,
this.unselectedLabelStyle,
this.labelColor,
this.unselectedLabelColor,
this.borderWidth = 0,
this.borderColor = Colors.transparent,
this.unselectedBorderColor = Colors.transparent,
this.physics = const BouncingScrollPhysics(),
this.labelPadding = const EdgeInsets.symmetric(horizontal: 4),
this.buttonMargin = const EdgeInsets.all(4),
this.padding = EdgeInsets.zero,
this.borderRadius = 8.0,
this.elevation = 0,
});
/// 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 [FlutterFlowButtonTabBar] is scrollable. Otherwise each tab gets an equal
/// share of the available space.
final bool isScrollable;
/// Whether the tab buttons should be styled as toggle buttons.
final bool useToggleButtonStyle;
/// The background [Color] of the button on its selected state.
final Color? backgroundColor;
/// The background [Color] of the button on its unselected state.
final Color? unselectedBackgroundColor;
/// The [BoxDecoration] of the button on its selected state.
///
/// If [BoxDecoration] is not provided, [backgroundColor] is used.
final BoxDecoration? decoration;
/// The [BoxDecoration] of the button on its unselected state.
///
/// If [BoxDecoration] is not provided, [unselectedBackgroundColor] is used.
final BoxDecoration? unselectedDecoration;
/// The [TextStyle] of the button's [Text] on its selected state. The color provided
/// on the TextStyle will be used for the [Icon]'s color.
final TextStyle? labelStyle;
/// The color of selected tab labels.
final Color? labelColor;
/// The color of unselected tab labels.
final Color? unselectedLabelColor;
/// The [TextStyle] of the button's [Text] on its unselected state. The color provided
/// on the TextStyle will be used for the [Icon]'s color.
final TextStyle? unselectedLabelStyle;
/// The with of solid [Border] for each button. If no value is provided, the border
/// is not drawn.
final double borderWidth;
/// The [Color] of solid [Border] for each button.
final Color? borderColor;
/// The [Color] of solid [Border] for each button. If no value is provided, the value of
/// [this.borderColor] is used.
final Color? unselectedBorderColor;
/// The [EdgeInsets] used for the [Padding] of the buttons' content.
///
/// The default value is [EdgeInsets.symmetric(horizontal: 4)].
final EdgeInsetsGeometry labelPadding;
/// The [EdgeInsets] used for the [Margin] of the buttons.
///
/// The default value is [EdgeInsets.all(4)].
final EdgeInsetsGeometry buttonMargin;
/// The amount of space by which to inset the tab bar.
final EdgeInsetsGeometry? padding;
/// The value of the [BorderRadius.circular] applied to each button.
final double borderRadius;
/// The value of the [elevation] applied to each button.
final double elevation;
final DragStartBehavior dragStartBehavior;
final ValueChanged<int>? onTap;
final ScrollPhysics? physics;
/// 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 + labelPadding.vertical + buttonMargin.vertical);
}
@override
State<FlutterFlowButtonTabBar> createState() =>
_FlutterFlowButtonTabBarState();
}
class _FlutterFlowButtonTabBarState extends State<FlutterFlowButtonTabBar>
with TickerProviderStateMixin {
ScrollController? _scrollController;
TabController? _controller;
_IndicatorPainter? _indicatorPainter;
late AnimationController _animationController;
int _currentIndex = 0;
int _prevIndex = -1;
late double _tabStripWidth;
late List<GlobalKey> _tabKeys;
final GlobalKey _tabsParentKey = GlobalKey();
bool _debugHasScheduledValidTabsCountCheck = false;
@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((tab) => GlobalKey()).toList();
/// The animation duration is 2/3 of the tab scroll animation duration in
/// Material design (kTabScrollDuration).
_animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200));
// so the buttons start in their "final" state (color)
_animationController
..value = 1.0
..addListener(() {
if (mounted) {
setState(() {});
}
});
}
// 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.maybeOf(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!,
tabKeys: _tabKeys,
old: _indicatorPainter,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
assert(debugCheckHasMaterial(context));
_updateTabController();
_initIndicatorPainter();
}
@override
void didUpdateWidget(FlutterFlowButtonTabBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_updateTabController();
_initIndicatorPainter();
// Adjust scroll position.
if (_scrollController != null) {
final ScrollPosition position = _scrollController!.position;
if (position is _TabBarScrollPosition) {
position.markNeedsPixelsCorrection();
}
}
}
if (widget.tabs.length > _tabKeys.length) {
final int delta = widget.tabs.length - _tabKeys.length;
_tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
} else if (widget.tabs.length < _tabKeys.length) {
_tabKeys.removeRange(widget.tabs.length, _tabKeys.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);
double paddingStart;
switch (Directionality.of(context)) {
case TextDirection.rtl:
paddingStart = widget.padding?.resolve(TextDirection.rtl).right ?? 0;
tabCenter = _tabStripWidth - tabCenter;
break;
case TextDirection.ltr:
paddingStart = widget.padding?.resolve(TextDirection.ltr).left ?? 0;
break;
}
return clampDouble(
tabCenter + paddingStart - viewportWidth / 2.0, minExtent, maxExtent);
}
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;
final 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) {
_prevIndex = _currentIndex;
_currentIndex = _controller!.index;
_triggerAnimation();
if (widget.isScrollable) {
_scrollToCurrentIndex();
}
}
setState(() {
// Rebuild the tabs after a (potentially animated) index change
// has completed.
});
}
void _triggerAnimation() {
// reset the animation so it's ready to go
_animationController
..reset()
..forward();
}
// 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);
widget.onTap?.call(index);
}
Widget _buildStyledTab(Widget child, int index) {
final TabBarTheme tabBarTheme = TabBarTheme.of(context) as TabBarTheme;
final double animationValue;
if (index == _currentIndex) {
animationValue = _animationController.value;
} else if (index == _prevIndex) {
animationValue = 1 - _animationController.value;
} else {
animationValue = 0;
}
final TextStyle? textStyle = TextStyle.lerp(
(widget.unselectedLabelStyle ??
tabBarTheme.labelStyle ??
DefaultTextStyle.of(context).style)
.copyWith(
color: widget.unselectedLabelColor,
),
(widget.labelStyle ??
tabBarTheme.labelStyle ??
DefaultTextStyle.of(context).style)
.copyWith(
color: widget.labelColor,
),
animationValue);
final Color? textColor = Color.lerp(
widget.unselectedLabelColor, widget.labelColor, animationValue);
final Color? borderColor = Color.lerp(
widget.unselectedBorderColor, widget.borderColor, animationValue);
BoxDecoration? boxDecoration = BoxDecoration.lerp(
BoxDecoration(
color: widget.unselectedDecoration?.color ??
widget.unselectedBackgroundColor ??
Colors.transparent,
boxShadow: widget.unselectedDecoration?.boxShadow,
gradient: widget.unselectedDecoration?.gradient,
borderRadius: widget.useToggleButtonStyle
? null
: BorderRadius.circular(widget.borderRadius),
),
BoxDecoration(
color: widget.decoration?.color ??
widget.backgroundColor ??
Colors.transparent,
boxShadow: widget.decoration?.boxShadow,
gradient: widget.decoration?.gradient,
borderRadius: widget.useToggleButtonStyle
? null
: BorderRadius.circular(widget.borderRadius),
),
animationValue);
if (widget.useToggleButtonStyle &&
widget.borderWidth > 0 &&
boxDecoration != null) {
if (index == 0) {
boxDecoration = boxDecoration.copyWith(
border: Border(
right: BorderSide(
color: widget.unselectedBorderColor ?? Colors.transparent,
width: widget.borderWidth / 2,
),
),
);
} else if (index == widget.tabs.length - 1) {
boxDecoration = boxDecoration.copyWith(
border: Border(
left: BorderSide(
color: widget.unselectedBorderColor ?? Colors.transparent,
width: widget.borderWidth / 2,
),
),
);
} else {
boxDecoration = boxDecoration.copyWith(
border: Border.symmetric(
vertical: BorderSide(
color: widget.unselectedBorderColor ?? Colors.transparent,
width: widget.borderWidth / 2,
),
),
);
}
}
return Padding(
key: _tabKeys[index],
// padding for the buttons
padding:
widget.useToggleButtonStyle ? EdgeInsets.zero : widget.buttonMargin,
child: TextButton(
onPressed: () => _handleTap(index),
style: ButtonStyle(
elevation: WidgetStateProperty.all(
widget.useToggleButtonStyle ? 0 : widget.elevation),
/// give a pretty small minimum size
minimumSize: WidgetStateProperty.all(const Size(10, 10)),
padding: WidgetStateProperty.all(EdgeInsets.zero),
textStyle: WidgetStateProperty.all(textStyle),
foregroundColor: WidgetStateProperty.all(textColor),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: WidgetStateProperty.all(
widget.useToggleButtonStyle
? const RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: BorderRadius.zero,
)
: RoundedRectangleBorder(
side: (widget.borderWidth == 0)
? BorderSide.none
: BorderSide(
color: borderColor ?? Colors.transparent,
width: widget.borderWidth,
),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
),
),
child: Ink(
decoration: boxDecoration,
child: Container(
padding: widget.labelPadding,
alignment: Alignment.center,
child: child,
),
),
),
);
}
bool _debugScheduleCheckHasValidTabsCount() {
if (_debugHasScheduledValidTabsCountCheck) {
return true;
}
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
_debugHasScheduledValidTabsCountCheck = false;
if (!mounted) {
return;
}
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;
}());
});
_debugHasScheduledValidTabsCountCheck = true;
return true;
}
@override
Widget build(BuildContext context) {
assert(_debugScheduleCheckHasValidTabsCount());
if (_controller!.length == 0) {
return Container(
height: _kTabHeight +
widget.labelPadding.vertical +
widget.buttonMargin.vertical,
);
}
final List<Widget> wrappedTabs =
List<Widget>.generate(widget.tabs.length, (int index) {
return _buildStyledTab(widget.tabs[index], index);
});
final int tabCount = widget.tabs.length;
// 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.
for (int index = 0; index < tabCount; index += 1) {
if (!widget.isScrollable) {
wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
}
}
Widget tabBar = AnimatedBuilder(
animation: _animationController,
key: _tabsParentKey,
builder: (context, child) {
Widget tabBarTemp = _TabLabelBar(
onPerformLayout: _saveTabOffsets,
children: wrappedTabs,
);
if (widget.useToggleButtonStyle) {
tabBarTemp = Material(
shape: widget.useToggleButtonStyle
? RoundedRectangleBorder(
side: (widget.borderWidth == 0)
? BorderSide.none
: BorderSide(
color: widget.borderColor ?? Colors.transparent,
width: widget.borderWidth,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(widget.borderRadius),
)
: null,
elevation: widget.useToggleButtonStyle ? widget.elevation : 0,
clipBehavior: Clip.antiAliasWithSaveLayer,
child: tabBarTemp,
);
}
return CustomPaint(
painter: _indicatorPainter,
child: tabBarTemp,
);
},
);
if (widget.isScrollable) {
_scrollController ??= _TabBarScrollController(this);
tabBar = SingleChildScrollView(
dragStartBehavior: widget.dragStartBehavior,
scrollDirection: Axis.horizontal,
controller: _scrollController,
padding: widget.padding,
physics: widget.physics,
child: tabBar,
);
} else if (widget.padding != null) {
tabBar = Padding(
padding: widget.padding!,
child: tabBar,
);
}
return tabBar;
}
}