您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
1346 行
48 KiB
1346 行
48 KiB
using System.Collections.Generic;
|
|
using uiwidgets;
|
|
using Unity.UIWidgets.animation;
|
|
using Unity.UIWidgets.async2;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.gestures;
|
|
using Unity.UIWidgets.painting;
|
|
using Unity.UIWidgets.rendering;
|
|
using Unity.UIWidgets.ui;
|
|
using Unity.UIWidgets.widgets;
|
|
using TextStyle = Unity.UIWidgets.painting.TextStyle;
|
|
|
|
namespace Unity.UIWidgets.material {
|
|
static class TabsUtils {
|
|
public const float _kTabHeight = 46.0f;
|
|
public const float _kTextAndIconTabHeight = 72.0f;
|
|
|
|
public static float _indexChangeProgress(TabController controller) {
|
|
float controllerValue = controller.animation.value;
|
|
float previousIndex = controller.previousIndex;
|
|
float currentIndex = controller.index;
|
|
|
|
if (!controller.indexIsChanging) {
|
|
return (currentIndex - controllerValue).abs().clamp(0.0f, 1.0f);
|
|
}
|
|
|
|
return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs();
|
|
}
|
|
|
|
public static readonly PageScrollPhysics _kTabBarViewPhysics =
|
|
(PageScrollPhysics) new PageScrollPhysics().applyTo(new ClampingScrollPhysics());
|
|
}
|
|
|
|
public enum TabBarIndicatorSize {
|
|
tab,
|
|
label
|
|
}
|
|
|
|
public class Tab : StatelessWidget {
|
|
public Tab(
|
|
Key key = null,
|
|
string text = null,
|
|
Widget icon = null,
|
|
EdgeInsetsGeometry iconMargin = null,
|
|
Widget child = null
|
|
) : base(key: key) {
|
|
D.assert(text != null || child != null || icon != null);
|
|
D.assert(!(text != null && child != null));
|
|
this.text = text;
|
|
this.icon = icon;
|
|
this.child = child;
|
|
this.iconMargin = iconMargin ?? EdgeInsets.only(bottom: 10.0f);
|
|
}
|
|
|
|
public readonly string text;
|
|
|
|
public readonly Widget child;
|
|
|
|
public readonly Widget icon;
|
|
|
|
public readonly EdgeInsetsGeometry iconMargin;
|
|
|
|
Widget _buildLabelText() {
|
|
return child ?? new Text(text, softWrap: false, overflow: TextOverflow.fade);
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
D.assert(material_.debugCheckHasMaterial(context));
|
|
|
|
float height = 0f;
|
|
Widget label = null;
|
|
|
|
if (icon == null) {
|
|
height = TabsUtils._kTabHeight;
|
|
label = _buildLabelText();
|
|
}
|
|
else if (text == null && child == null) {
|
|
height = TabsUtils._kTabHeight;
|
|
label = icon;
|
|
}
|
|
else {
|
|
height = TabsUtils._kTextAndIconTabHeight;
|
|
label = new Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: new List<Widget> {
|
|
new Container(
|
|
child: icon,
|
|
margin: iconMargin
|
|
),
|
|
_buildLabelText()
|
|
}
|
|
);
|
|
}
|
|
|
|
return new SizedBox(
|
|
height: height,
|
|
child: new Center(
|
|
child: label,
|
|
widthFactor: 1.0f)
|
|
);
|
|
}
|
|
|
|
public override void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
base.debugFillProperties(properties);
|
|
properties.add(new StringProperty("text", text, defaultValue: foundation_.kNullDefaultValue));
|
|
properties.add(new DiagnosticsProperty<Widget>("icon", icon,
|
|
defaultValue: foundation_.kNullDefaultValue));
|
|
}
|
|
}
|
|
|
|
|
|
class _TabStyle : AnimatedWidget {
|
|
public _TabStyle(
|
|
Key key = null,
|
|
Animation<float> animation = null,
|
|
bool? selected = null,
|
|
Color labelColor = null,
|
|
Color unselectedLabelColor = null,
|
|
TextStyle labelStyle = null,
|
|
TextStyle unselectedLabelStyle = null,
|
|
Widget child = null
|
|
) : base(key: key, listenable: animation) {
|
|
D.assert(child != null);
|
|
D.assert(selected != null);
|
|
this.selected = selected.Value;
|
|
this.labelColor = labelColor;
|
|
this.unselectedLabelColor = unselectedLabelColor;
|
|
this.labelStyle = labelStyle;
|
|
this.unselectedLabelStyle = unselectedLabelStyle;
|
|
this.child = child;
|
|
}
|
|
|
|
public readonly TextStyle labelStyle;
|
|
|
|
public readonly TextStyle unselectedLabelStyle;
|
|
|
|
public readonly bool selected;
|
|
|
|
public readonly Color labelColor;
|
|
|
|
public readonly Color unselectedLabelColor;
|
|
|
|
public readonly Widget child;
|
|
|
|
protected internal override Widget build(BuildContext context) {
|
|
ThemeData themeData = Theme.of(context);
|
|
TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
|
Animation<float> animation = (Animation<float>) listenable;
|
|
|
|
TextStyle defaultStyle = (labelStyle
|
|
?? tabBarTheme.labelStyle
|
|
?? themeData.primaryTextTheme.bodyText1).copyWith(inherit: true);
|
|
TextStyle defaultUnselectedStyle = (unselectedLabelStyle
|
|
?? tabBarTheme.unselectedLabelStyle
|
|
?? labelStyle
|
|
?? themeData.primaryTextTheme.bodyText1).copyWith(inherit: true);
|
|
TextStyle textStyle = selected
|
|
? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)
|
|
: TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value);
|
|
|
|
Color selectedColor = labelColor ?? tabBarTheme.labelColor ?? themeData.primaryTextTheme.bodyText1.color;
|
|
Color unselectedColor = unselectedLabelColor ??
|
|
tabBarTheme.unselectedLabelColor ?? selectedColor.withAlpha(0xB2);
|
|
Color color = selected
|
|
? Color.lerp(selectedColor, unselectedColor, animation.value)
|
|
: Color.lerp(unselectedColor, selectedColor, animation.value);
|
|
|
|
return new DefaultTextStyle(
|
|
style: textStyle.copyWith(color: color),
|
|
child: IconTheme.merge(
|
|
data: new IconThemeData(
|
|
size: 24.0f,
|
|
color: color),
|
|
child: child
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
delegate void _LayoutCallback(List<float> xOffsets, float width);
|
|
|
|
|
|
class _TabLabelBarRenderer : RenderFlex {
|
|
public _TabLabelBarRenderer(
|
|
List<RenderBox> children = null,
|
|
Axis? direction = null,
|
|
MainAxisSize? mainAxisSize = null,
|
|
MainAxisAlignment? mainAxisAlignment = null,
|
|
CrossAxisAlignment? crossAxisAlignment = null,
|
|
VerticalDirection? verticalDirection = null,
|
|
_LayoutCallback onPerformLayout = null
|
|
) : base(
|
|
children: children,
|
|
direction: direction.Value,
|
|
mainAxisSize: mainAxisSize.Value,
|
|
mainAxisAlignment: mainAxisAlignment.Value,
|
|
crossAxisAlignment: crossAxisAlignment.Value,
|
|
verticalDirection: verticalDirection.Value
|
|
) {
|
|
D.assert(direction != null);
|
|
D.assert(mainAxisSize != null);
|
|
D.assert(mainAxisAlignment != null);
|
|
D.assert(crossAxisAlignment != null);
|
|
D.assert(verticalDirection != null);
|
|
|
|
D.assert(onPerformLayout != null);
|
|
this.onPerformLayout = onPerformLayout;
|
|
}
|
|
|
|
public _LayoutCallback onPerformLayout;
|
|
|
|
protected override void performLayout() {
|
|
base.performLayout();
|
|
|
|
RenderBox child = firstChild;
|
|
List<float> xOffsets = new List<float>();
|
|
|
|
while (child != null) {
|
|
FlexParentData childParentData = (FlexParentData) child.parentData;
|
|
xOffsets.Add(childParentData.offset.dx);
|
|
D.assert(child.parentData == childParentData);
|
|
child = childParentData.nextSibling;
|
|
}
|
|
|
|
xOffsets.Add(size.width);
|
|
onPerformLayout(xOffsets, size.width);
|
|
}
|
|
}
|
|
|
|
|
|
class _TabLabelBar : Flex {
|
|
public _TabLabelBar(
|
|
Key key = null,
|
|
List<Widget> children = null,
|
|
_LayoutCallback onPerformLayout = null
|
|
) : base(
|
|
key: key,
|
|
children: children ?? new List<Widget>(),
|
|
direction: Axis.horizontal,
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
verticalDirection: VerticalDirection.down
|
|
) {
|
|
this.onPerformLayout = onPerformLayout;
|
|
}
|
|
|
|
public readonly _LayoutCallback onPerformLayout;
|
|
|
|
public override RenderObject createRenderObject(BuildContext context) {
|
|
return new _TabLabelBarRenderer(
|
|
direction: direction,
|
|
mainAxisAlignment: mainAxisAlignment,
|
|
mainAxisSize: mainAxisSize,
|
|
crossAxisAlignment: crossAxisAlignment,
|
|
verticalDirection: verticalDirection,
|
|
onPerformLayout: onPerformLayout
|
|
);
|
|
}
|
|
|
|
public override void updateRenderObject(BuildContext context, RenderObject renderObject) {
|
|
base.updateRenderObject(context, renderObject);
|
|
_TabLabelBarRenderer _renderObject = (_TabLabelBarRenderer) renderObject;
|
|
_renderObject.onPerformLayout = onPerformLayout;
|
|
}
|
|
}
|
|
|
|
class _IndicatorPainter : AbstractCustomPainter {
|
|
public _IndicatorPainter(
|
|
TabController controller = null,
|
|
Decoration indicator = null,
|
|
TabBarIndicatorSize? indicatorSize = null,
|
|
List<GlobalKey> tabKeys = null,
|
|
_IndicatorPainter old = null
|
|
) : base(repaint: controller.animation) {
|
|
D.assert(controller != null);
|
|
D.assert(indicator != null);
|
|
this.controller = controller;
|
|
this.indicator = indicator;
|
|
this.indicatorSize = indicatorSize;
|
|
this.tabKeys = tabKeys;
|
|
if (old != null) {
|
|
saveTabOffsets(old._currentTabOffsets);
|
|
}
|
|
}
|
|
|
|
public readonly TabController controller;
|
|
|
|
public readonly Decoration indicator;
|
|
|
|
public readonly TabBarIndicatorSize? indicatorSize;
|
|
|
|
public readonly List<GlobalKey> tabKeys;
|
|
|
|
List<float> _currentTabOffsets;
|
|
Rect _currentRect;
|
|
BoxPainter _painter;
|
|
bool _needsPaint = false;
|
|
|
|
void markNeedsPaint() {
|
|
_needsPaint = true;
|
|
}
|
|
|
|
public void dispose() {
|
|
_painter?.Dispose();
|
|
}
|
|
|
|
public void saveTabOffsets(List<float> tabOffsets) {
|
|
_currentTabOffsets = tabOffsets;
|
|
}
|
|
|
|
public int maxTabIndex {
|
|
get { return _currentTabOffsets.Count - 2; }
|
|
}
|
|
|
|
public float centerOf(int tabIndex) {
|
|
D.assert(_currentTabOffsets != null);
|
|
D.assert(_currentTabOffsets.isNotEmpty());
|
|
D.assert(tabIndex >= 0);
|
|
D.assert(tabIndex <= maxTabIndex);
|
|
return (_currentTabOffsets[tabIndex] + _currentTabOffsets[tabIndex + 1]) / 2.0f;
|
|
}
|
|
|
|
public Rect indicatorRect(Size tabBarSize, int tabIndex) {
|
|
D.assert(_currentTabOffsets != null);
|
|
D.assert(_currentTabOffsets.isNotEmpty());
|
|
D.assert(tabIndex >= 0);
|
|
D.assert(tabIndex <= maxTabIndex);
|
|
float tabLeft = _currentTabOffsets[tabIndex];
|
|
float tabRight = _currentTabOffsets[tabIndex + 1];
|
|
|
|
if (indicatorSize == TabBarIndicatorSize.label) {
|
|
float tabWidth = tabKeys[tabIndex].currentContext.size.width;
|
|
float delta = ((tabRight - tabLeft) - tabWidth) / 2.0f;
|
|
tabLeft += delta;
|
|
tabRight -= delta;
|
|
}
|
|
|
|
return Rect.fromLTWH(tabLeft, 0.0f, tabRight - tabLeft, tabBarSize.height);
|
|
}
|
|
|
|
public override void paint(Canvas canvas, Size size) {
|
|
_needsPaint = false;
|
|
_painter = _painter ?? indicator.createBoxPainter(markNeedsPaint);
|
|
|
|
if (controller.indexIsChanging) {
|
|
Rect targetRect = indicatorRect(size, controller.index);
|
|
_currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect,
|
|
TabsUtils._indexChangeProgress(controller));
|
|
}
|
|
else {
|
|
int currentIndex = controller.index;
|
|
Rect previous = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
|
|
Rect middle = indicatorRect(size, currentIndex);
|
|
Rect next = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null;
|
|
float index = controller.index;
|
|
float value = controller.animation.value;
|
|
if (value == index - 1.0f) {
|
|
_currentRect = previous ?? middle;
|
|
}
|
|
else if (value == index + 1.0f) {
|
|
_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);
|
|
}
|
|
}
|
|
|
|
D.assert(_currentRect != null);
|
|
|
|
ImageConfiguration configuration = new ImageConfiguration(
|
|
size: _currentRect.size
|
|
);
|
|
|
|
_painter.paint(canvas, _currentRect.topLeft, configuration);
|
|
}
|
|
|
|
static bool _tabOffsetsEqual(List<float> a, List<float> b) {
|
|
if (a == null || b == null || a.Count != b.Count) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < a.Count; i++) {
|
|
if (a[i] != b[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public override bool shouldRepaint(CustomPainter old) {
|
|
_IndicatorPainter _old = (_IndicatorPainter) old;
|
|
return _needsPaint
|
|
|| controller != _old.controller
|
|
|| indicator != _old.indicator
|
|
|| tabKeys.Count != _old.tabKeys.Count
|
|
|| !_tabOffsetsEqual(_currentTabOffsets, _old._currentTabOffsets);
|
|
}
|
|
}
|
|
|
|
class _ChangeAnimation : AnimationWithParentMixin<float, float> {
|
|
public _ChangeAnimation(
|
|
TabController controller) {
|
|
this.controller = controller;
|
|
}
|
|
|
|
public readonly TabController controller;
|
|
|
|
public override Animation<float> parent {
|
|
get { return controller.animation; }
|
|
}
|
|
|
|
public override void removeStatusListener(AnimationStatusListener listener) {
|
|
if (parent != null) {
|
|
base.removeStatusListener(listener);
|
|
}
|
|
}
|
|
|
|
public override void removeListener(VoidCallback listener) {
|
|
if (parent != null) {
|
|
base.removeListener(listener);
|
|
}
|
|
}
|
|
|
|
public override float value {
|
|
get { return TabsUtils._indexChangeProgress(controller); }
|
|
}
|
|
}
|
|
|
|
|
|
class _DragAnimation : AnimationWithParentMixin<float, float> {
|
|
public _DragAnimation(
|
|
TabController controller,
|
|
int index) {
|
|
this.controller = controller;
|
|
this.index = index;
|
|
}
|
|
|
|
public readonly TabController controller;
|
|
|
|
public readonly int index;
|
|
|
|
public override Animation<float> parent {
|
|
get { return controller.animation; }
|
|
}
|
|
|
|
public override void removeStatusListener(AnimationStatusListener listener) {
|
|
if (parent != null) {
|
|
base.removeStatusListener(listener);
|
|
}
|
|
}
|
|
|
|
public override void removeListener(VoidCallback listener) {
|
|
if (parent != null) {
|
|
base.removeListener(listener);
|
|
}
|
|
}
|
|
|
|
public override float value {
|
|
get {
|
|
D.assert(!controller.indexIsChanging);
|
|
return (controller.animation.value - index).abs().clamp(0.0f, 1.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class _TabBarScrollPosition : ScrollPositionWithSingleContext {
|
|
public _TabBarScrollPosition(
|
|
ScrollPhysics physics = null,
|
|
ScrollContext context = null,
|
|
ScrollPosition oldPosition = null,
|
|
_TabBarState tabBar = null
|
|
) : base(
|
|
physics: physics,
|
|
context: context,
|
|
initialPixels: null,
|
|
oldPosition: oldPosition) {
|
|
this.tabBar = tabBar;
|
|
}
|
|
|
|
public readonly _TabBarState tabBar;
|
|
|
|
bool _initialViewportDimensionWasZero;
|
|
|
|
public override bool applyContentDimensions(float minScrollExtent, float maxScrollExtent) {
|
|
bool result = true;
|
|
if (_initialViewportDimensionWasZero != true) {
|
|
_initialViewportDimensionWasZero = viewportDimension != 0.0;
|
|
correctPixels(tabBar._initialScrollOffset(viewportDimension, minScrollExtent,
|
|
maxScrollExtent));
|
|
result = false;
|
|
}
|
|
|
|
return base.applyContentDimensions(minScrollExtent, maxScrollExtent) && result;
|
|
}
|
|
}
|
|
|
|
|
|
class _TabBarScrollController : ScrollController {
|
|
public _TabBarScrollController(_TabBarState tabBar) {
|
|
this.tabBar = tabBar;
|
|
}
|
|
|
|
public readonly _TabBarState tabBar;
|
|
|
|
public override ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context,
|
|
ScrollPosition oldPosition) {
|
|
return new _TabBarScrollPosition(
|
|
physics: physics,
|
|
context: context,
|
|
oldPosition: oldPosition,
|
|
tabBar: tabBar
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
public class TabBar : PreferredSizeWidget {
|
|
public TabBar(
|
|
Key key = null,
|
|
List<Widget> tabs = null,
|
|
TabController controller = null,
|
|
bool isScrollable = false,
|
|
Color indicatorColor = null,
|
|
float indicatorWeight = 2.0f,
|
|
EdgeInsetsGeometry indicatorPadding = null,
|
|
Decoration indicator = null,
|
|
TabBarIndicatorSize? indicatorSize = null,
|
|
Color labelColor = null,
|
|
TextStyle labelStyle = null,
|
|
EdgeInsetsGeometry labelPadding = null,
|
|
Color unselectedLabelColor = null,
|
|
TextStyle unselectedLabelStyle = null,
|
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
|
ValueChanged<int> onTap = null
|
|
) : base(key: key) {
|
|
indicatorPadding = indicatorPadding ?? EdgeInsets.zero;
|
|
D.assert(tabs != null);
|
|
D.assert(indicator != null || indicatorWeight > 0.0f);
|
|
D.assert(indicator != null || indicatorPadding != null);
|
|
this.tabs = tabs;
|
|
this.controller = controller;
|
|
this.isScrollable = isScrollable;
|
|
this.indicatorColor = indicatorColor;
|
|
this.indicatorWeight = indicatorWeight;
|
|
this.indicatorPadding = indicatorPadding;
|
|
this.indicator = indicator;
|
|
this.indicatorSize = indicatorSize;
|
|
this.labelColor = labelColor;
|
|
this.labelStyle = labelStyle;
|
|
this.labelPadding = labelPadding;
|
|
this.unselectedLabelColor = unselectedLabelColor;
|
|
this.unselectedLabelStyle = unselectedLabelStyle;
|
|
this.dragStartBehavior = dragStartBehavior;
|
|
this.onTap = onTap;
|
|
}
|
|
|
|
public readonly List<Widget> tabs;
|
|
|
|
public readonly TabController controller;
|
|
|
|
public readonly bool isScrollable;
|
|
|
|
public readonly Color indicatorColor;
|
|
|
|
public readonly float indicatorWeight;
|
|
|
|
public readonly EdgeInsetsGeometry indicatorPadding;
|
|
|
|
public readonly Decoration indicator;
|
|
|
|
public readonly TabBarIndicatorSize? indicatorSize;
|
|
|
|
public readonly Color labelColor;
|
|
|
|
public readonly Color unselectedLabelColor;
|
|
|
|
public readonly TextStyle labelStyle;
|
|
|
|
public readonly EdgeInsetsGeometry labelPadding;
|
|
|
|
public readonly TextStyle unselectedLabelStyle;
|
|
|
|
public readonly DragStartBehavior dragStartBehavior;
|
|
|
|
public readonly ValueChanged<int> onTap;
|
|
|
|
public override Size preferredSize {
|
|
get {
|
|
foreach (Widget item in tabs) {
|
|
if (item is Tab) {
|
|
Tab tab = (Tab) item;
|
|
if ((tab.text != null || tab.child != null) && tab.icon != null) {
|
|
return Size.fromHeight(TabsUtils._kTextAndIconTabHeight + indicatorWeight);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Size.fromHeight(TabsUtils._kTabHeight + indicatorWeight);
|
|
}
|
|
}
|
|
|
|
|
|
public override State createState() {
|
|
return new _TabBarState();
|
|
}
|
|
}
|
|
|
|
|
|
class _TabBarState : State<TabBar> {
|
|
ScrollController _scrollController;
|
|
TabController _controller;
|
|
_IndicatorPainter _indicatorPainter;
|
|
int _currentIndex;
|
|
List<GlobalKey> _tabKeys;
|
|
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
_tabKeys = new List<GlobalKey>();
|
|
foreach (Widget tab in widget.tabs) {
|
|
_tabKeys.Add(GlobalKey.key());
|
|
}
|
|
}
|
|
|
|
Decoration _indicator {
|
|
get {
|
|
if (widget.indicator != null) {
|
|
return widget.indicator;
|
|
}
|
|
|
|
TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
|
if (tabBarTheme.indicator != null) {
|
|
return tabBarTheme.indicator;
|
|
}
|
|
|
|
Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
|
|
if (color.value == Material.of(context).color?.value) {
|
|
color = Colors.white;
|
|
}
|
|
|
|
return new UnderlineTabIndicator(
|
|
insets: widget.indicatorPadding,
|
|
borderSide: new BorderSide(
|
|
width: widget.indicatorWeight,
|
|
color: color));
|
|
}
|
|
}
|
|
|
|
bool _controllerIsValid {
|
|
get { return _controller?.animation != null; }
|
|
}
|
|
|
|
void _updateTabController() {
|
|
TabController newController = widget.controller ?? DefaultTabController.of(context);
|
|
D.assert(() => {
|
|
if (newController == null) {
|
|
throw new UIWidgetsError(
|
|
"No TabController for " + widget.GetType() + ".\n" +
|
|
"When creating a " + widget.GetType() + ", you must either provide an explicit " +
|
|
"TabController using the \"controller\" property, or you must ensure that there " +
|
|
"is a DefaultTabController above the " + widget.GetType() + ".\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
|
|
: new _IndicatorPainter(
|
|
controller: _controller,
|
|
indicator: _indicator,
|
|
indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,
|
|
tabKeys: _tabKeys,
|
|
old: _indicatorPainter
|
|
);
|
|
}
|
|
|
|
public override void didChangeDependencies() {
|
|
base.didChangeDependencies();
|
|
D.assert(material_.debugCheckHasMaterial(context));
|
|
_updateTabController();
|
|
_initIndicatorPainter();
|
|
}
|
|
|
|
public override void didUpdateWidget(StatefulWidget oldWidget) {
|
|
base.didUpdateWidget(oldWidget);
|
|
TabBar _oldWidget = (TabBar) 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.Count > _oldWidget.tabs.Count) {
|
|
int delta = widget.tabs.Count - _oldWidget.tabs.Count;
|
|
for (int i = 0; i < delta; i++) {
|
|
_tabKeys.Add(GlobalKey.key());
|
|
}
|
|
}
|
|
else if (widget.tabs.Count < _oldWidget.tabs.Count) {
|
|
int delta = _oldWidget.tabs.Count - widget.tabs.Count;
|
|
_tabKeys.RemoveRange(widget.tabs.Count, delta);
|
|
}
|
|
}
|
|
|
|
public override void dispose() {
|
|
_indicatorPainter.dispose();
|
|
if (_controllerIsValid) {
|
|
_controller.animation.removeListener(_handleTabControllerAnimationTick);
|
|
_controller.removeListener(_handleTabControllerTick);
|
|
}
|
|
|
|
_controller = null;
|
|
base.dispose();
|
|
}
|
|
|
|
public int maxTabIndex {
|
|
get { return _indicatorPainter.maxTabIndex; }
|
|
}
|
|
|
|
float _tabScrollOffset(int index, float viewportWidth, float minExtent, float maxExtent) {
|
|
if (!widget.isScrollable) {
|
|
return 0.0f;
|
|
}
|
|
|
|
float tabCenter = _indicatorPainter.centerOf(index);
|
|
return (tabCenter - viewportWidth / 2.0f).clamp(minExtent, maxExtent);
|
|
}
|
|
|
|
float _tabCenteredScrollOffset(int index) {
|
|
ScrollPosition position = _scrollController.position;
|
|
return _tabScrollOffset(index, position.viewportDimension, position.minScrollExtent,
|
|
position.maxScrollExtent);
|
|
}
|
|
|
|
internal float _initialScrollOffset(float viewportWidth, float minExtent, float maxExtent) {
|
|
return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
|
|
}
|
|
|
|
void _scrollToCurrentIndex() {
|
|
float offset = _tabCenteredScrollOffset(_currentIndex);
|
|
_scrollController.animateTo(offset, duration: material_.kTabScrollDuration, curve: Curves.ease);
|
|
}
|
|
|
|
void _scrollToControllerValue() {
|
|
float? leadingPosition = _currentIndex > 0
|
|
? (float?) _tabCenteredScrollOffset(_currentIndex - 1)
|
|
: null;
|
|
float middlePosition = _tabCenteredScrollOffset(_currentIndex);
|
|
float? trailingPosition = _currentIndex < maxTabIndex
|
|
? (float?) _tabCenteredScrollOffset(_currentIndex + 1)
|
|
: null;
|
|
|
|
float index = _controller.index;
|
|
float value = _controller.animation.value;
|
|
float offset = 0.0f;
|
|
if (value == index - 1.0f) {
|
|
offset = leadingPosition ?? middlePosition;
|
|
}
|
|
else if (value == index + 1.0f) {
|
|
offset = trailingPosition ?? middlePosition;
|
|
}
|
|
else if (value == index) {
|
|
offset = middlePosition;
|
|
}
|
|
else if (value < index) {
|
|
offset = leadingPosition == null
|
|
? middlePosition
|
|
: MathUtils.lerpNullableFloat(middlePosition, leadingPosition, index - value).Value;
|
|
}
|
|
else {
|
|
offset = trailingPosition == null
|
|
? middlePosition
|
|
: MathUtils.lerpNullableFloat(middlePosition, trailingPosition, value - index).Value;
|
|
}
|
|
|
|
_scrollController.jumpTo(offset);
|
|
}
|
|
|
|
|
|
void _handleTabControllerAnimationTick() {
|
|
D.assert(mounted);
|
|
if (!_controller.indexIsChanging && widget.isScrollable) {
|
|
_currentIndex = _controller.index;
|
|
_scrollToControllerValue();
|
|
}
|
|
}
|
|
|
|
void _handleTabControllerTick() {
|
|
if (_controller.index != _currentIndex) {
|
|
_currentIndex = _controller.index;
|
|
if (widget.isScrollable) {
|
|
_scrollToCurrentIndex();
|
|
}
|
|
}
|
|
|
|
setState(() => { });
|
|
}
|
|
|
|
void _saveTabOffsets(List<float> tabOffsets, float width) {
|
|
_indicatorPainter?.saveTabOffsets(tabOffsets);
|
|
}
|
|
|
|
void _handleTap(int index) {
|
|
D.assert(index >= 0 && index < widget.tabs.Count);
|
|
_controller.animateTo(index);
|
|
if (widget.onTap != null) {
|
|
widget.onTap(index);
|
|
}
|
|
}
|
|
|
|
Widget _buildStyledTab(Widget child, bool selected, Animation<float> animation) {
|
|
return new _TabStyle(
|
|
animation: animation,
|
|
selected: selected,
|
|
labelColor: widget.labelColor,
|
|
unselectedLabelColor: widget.unselectedLabelColor,
|
|
labelStyle: widget.labelStyle,
|
|
unselectedLabelStyle: widget.unselectedLabelStyle,
|
|
child: child
|
|
);
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
D.assert(material_.debugCheckHasMaterialLocalizations(context));
|
|
D.assert(() => {
|
|
if (_controller.length != widget.tabs.Count) {
|
|
throw new UIWidgetsError(
|
|
$"Controller's length property {_controller.length} does not match the\n" +
|
|
$"number of tab elements {widget.tabs.Count} present in TabBar's tabs property."
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
if (_controller.length == 0) {
|
|
return new Container(
|
|
height: TabsUtils._kTabHeight + widget.indicatorWeight
|
|
);
|
|
}
|
|
|
|
TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
|
|
|
List<Widget> wrappedTabs = new List<Widget>();
|
|
for (int i = 0; i < widget.tabs.Count; i++) {
|
|
wrappedTabs.Add(new Center(
|
|
heightFactor: 1.0f,
|
|
child: new Padding(
|
|
padding: widget.labelPadding ?? tabBarTheme.labelPadding ?? material_.kTabLabelPadding,
|
|
child: new KeyedSubtree(
|
|
key: _tabKeys[i],
|
|
child: widget.tabs[i]
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
if (_controller != null) {
|
|
int previousIndex = _controller.previousIndex;
|
|
|
|
if (_controller.indexIsChanging) {
|
|
D.assert(_currentIndex != previousIndex);
|
|
Animation<float> animation = new _ChangeAnimation(_controller);
|
|
wrappedTabs[_currentIndex] =
|
|
_buildStyledTab(wrappedTabs[_currentIndex], true, animation);
|
|
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
|
|
}
|
|
else {
|
|
int tabIndex = _currentIndex;
|
|
Animation<float> centerAnimation = new _DragAnimation(_controller, tabIndex);
|
|
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
|
|
if (_currentIndex > 0) {
|
|
int previousTabIndex = _currentIndex - 1;
|
|
Animation<float> previousAnimation =
|
|
new ReverseAnimation(new _DragAnimation(_controller, previousTabIndex));
|
|
wrappedTabs[previousTabIndex] =
|
|
_buildStyledTab(wrappedTabs[previousTabIndex], false, previousAnimation);
|
|
}
|
|
|
|
if (_currentIndex < widget.tabs.Count - 1) {
|
|
int nextTabIndex = _currentIndex + 1;
|
|
Animation<float> nextAnimation =
|
|
new ReverseAnimation(new _DragAnimation(_controller, nextTabIndex));
|
|
wrappedTabs[nextTabIndex] =
|
|
_buildStyledTab(wrappedTabs[nextTabIndex], false, nextAnimation);
|
|
}
|
|
}
|
|
}
|
|
|
|
int tabCount = widget.tabs.Count;
|
|
for (int index = 0; index < tabCount; index++) {
|
|
int tabIndex = index;
|
|
wrappedTabs[index] = new InkWell(
|
|
onTap: () => { _handleTap(tabIndex); },
|
|
child: new Padding(
|
|
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
|
|
child: wrappedTabs[index]
|
|
)
|
|
);
|
|
if (!widget.isScrollable) {
|
|
wrappedTabs[index] = new Expanded(
|
|
child: wrappedTabs[index]);
|
|
}
|
|
}
|
|
|
|
Widget tabBar = new CustomPaint(
|
|
painter: _indicatorPainter,
|
|
child: new _TabStyle(
|
|
animation: Animations.kAlwaysDismissedAnimation,
|
|
selected: false,
|
|
labelColor: widget.labelColor,
|
|
unselectedLabelColor: widget.unselectedLabelColor,
|
|
labelStyle: widget.labelStyle,
|
|
unselectedLabelStyle: widget.unselectedLabelStyle,
|
|
child: new _TabLabelBar(
|
|
onPerformLayout: _saveTabOffsets,
|
|
children: wrappedTabs
|
|
)
|
|
)
|
|
);
|
|
|
|
if (widget.isScrollable) {
|
|
_scrollController = _scrollController ?? new _TabBarScrollController(this);
|
|
tabBar = new SingleChildScrollView(
|
|
dragStartBehavior: widget.dragStartBehavior,
|
|
scrollDirection: Axis.horizontal,
|
|
controller: _scrollController,
|
|
child: tabBar);
|
|
}
|
|
|
|
return tabBar;
|
|
}
|
|
}
|
|
|
|
|
|
public class TabBarView : StatefulWidget {
|
|
public TabBarView(
|
|
Key key = null,
|
|
List<Widget> children = null,
|
|
TabController controller = null,
|
|
ScrollPhysics physics = null,
|
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start
|
|
) : base(key: key) {
|
|
D.assert(children != null);
|
|
this.children = children;
|
|
this.controller = controller;
|
|
this.physics = physics;
|
|
this.dragStartBehavior = dragStartBehavior;
|
|
}
|
|
|
|
public readonly TabController controller;
|
|
|
|
public readonly List<Widget> children;
|
|
|
|
public readonly ScrollPhysics physics;
|
|
|
|
public readonly DragStartBehavior dragStartBehavior;
|
|
|
|
public override State createState() {
|
|
return new _TabBarViewState();
|
|
}
|
|
}
|
|
|
|
class _TabBarViewState : State<TabBarView> {
|
|
TabController _controller;
|
|
PageController _pageController;
|
|
List<Widget> _children;
|
|
List<Widget> _childrenWithKey;
|
|
int? _currentIndex = null;
|
|
int _warpUnderwayCount = 0;
|
|
|
|
bool _controllerIsValid {
|
|
get { return _controller?.animation != null; }
|
|
}
|
|
|
|
void _updateTabController() {
|
|
TabController newController = widget.controller ?? DefaultTabController.of(context);
|
|
D.assert(() => {
|
|
if (newController == null) {
|
|
throw new UIWidgetsError(
|
|
"No TabController for " + widget.GetType() + "\n" +
|
|
"When creating a " + widget.GetType() + ", you must either provide an explicit " +
|
|
"TabController using the \"controller\" property, or you must ensure that there " +
|
|
"is a DefaultTabController above the " + widget.GetType() + ".\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);
|
|
}
|
|
}
|
|
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
_updateChildren();
|
|
}
|
|
|
|
public override void didChangeDependencies() {
|
|
base.didChangeDependencies();
|
|
_updateTabController();
|
|
_currentIndex = _controller?.index;
|
|
_pageController = new PageController(initialPage: _currentIndex ?? 0);
|
|
}
|
|
|
|
public override void didUpdateWidget(StatefulWidget oldWidget) {
|
|
base.didUpdateWidget(oldWidget);
|
|
TabBarView _oldWidget = (TabBarView) oldWidget;
|
|
if (widget.controller != _oldWidget.controller) {
|
|
_updateTabController();
|
|
}
|
|
|
|
if (widget.children != _oldWidget.children && _warpUnderwayCount == 0) {
|
|
_updateChildren();
|
|
}
|
|
}
|
|
|
|
public override void dispose() {
|
|
if (_controllerIsValid) {
|
|
_controller.animation.removeListener(_handleTabControllerAnimationTick);
|
|
}
|
|
|
|
_controller = null;
|
|
base.dispose();
|
|
}
|
|
|
|
void _updateChildren() {
|
|
_children = widget.children;
|
|
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
|
|
}
|
|
|
|
void _handleTabControllerAnimationTick() {
|
|
if (_warpUnderwayCount > 0 || !_controller.indexIsChanging) {
|
|
return;
|
|
}
|
|
|
|
if (_controller.index != _currentIndex) {
|
|
_currentIndex = _controller.index;
|
|
_warpToCurrentIndex();
|
|
}
|
|
}
|
|
|
|
void _warpToCurrentIndex() {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
if (_pageController.page == _currentIndex) {
|
|
return;
|
|
}
|
|
|
|
int previousIndex = _controller.previousIndex;
|
|
if ((_currentIndex.Value - previousIndex).abs() == 1) {
|
|
_pageController.animateToPage(_currentIndex.Value, duration: material_.kTabScrollDuration,
|
|
curve: Curves.ease);
|
|
return;
|
|
}
|
|
|
|
D.assert((_currentIndex.Value - previousIndex).abs() > 1);
|
|
int initialPage = _currentIndex.Value > previousIndex
|
|
? _currentIndex.Value - 1
|
|
: _currentIndex.Value + 1;
|
|
List<Widget> originalChildren = _childrenWithKey;
|
|
|
|
setState(() => {
|
|
_warpUnderwayCount += 1;
|
|
|
|
_childrenWithKey = new List<Widget>(_childrenWithKey);
|
|
Widget temp = _childrenWithKey[initialPage];
|
|
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
|
|
_childrenWithKey[previousIndex] = temp;
|
|
});
|
|
|
|
_pageController.jumpToPage(initialPage);
|
|
_pageController.animateToPage(_currentIndex.Value, duration: material_.kTabScrollDuration,
|
|
curve: Curves.ease).then((value) => {
|
|
if (!mounted) {
|
|
return Future.value();
|
|
}
|
|
|
|
setState(() => {
|
|
_warpUnderwayCount -= 1;
|
|
if (widget.children != _children) {
|
|
_updateChildren();
|
|
}
|
|
else {
|
|
_childrenWithKey = originalChildren;
|
|
}
|
|
});
|
|
|
|
return Future.value();
|
|
});
|
|
}
|
|
|
|
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.0f, 1.0f);
|
|
}
|
|
else if (notification is ScrollEndNotification) {
|
|
_controller.index = _pageController.page.round();
|
|
_currentIndex = _controller.index;
|
|
}
|
|
|
|
_warpUnderwayCount -= 1;
|
|
|
|
return false;
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
D.assert(() => {
|
|
if (_controller.length != widget.children.Count) {
|
|
throw new UIWidgetsError(
|
|
$"Controller's length property {_controller.length} does not match the\n" +
|
|
$"number of tabs {widget.children.Count} present in TabBar's tabs property."
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
return new NotificationListener<ScrollNotification>(
|
|
onNotification: _handleScrollNotification,
|
|
child: new PageView(
|
|
dragStartBehavior: widget.dragStartBehavior,
|
|
controller: _pageController,
|
|
physics: widget.physics == null
|
|
? TabsUtils._kTabBarViewPhysics
|
|
: TabsUtils._kTabBarViewPhysics.applyTo(widget.physics),
|
|
children: _childrenWithKey
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
public class TabPageSelectorIndicator : StatelessWidget {
|
|
public TabPageSelectorIndicator(
|
|
Key key = null,
|
|
Color backgroundColor = null,
|
|
Color borderColor = null,
|
|
float? size = null
|
|
) : base(key: key) {
|
|
D.assert(backgroundColor != null);
|
|
D.assert(borderColor != null);
|
|
D.assert(size != null);
|
|
|
|
this.backgroundColor = backgroundColor;
|
|
this.borderColor = borderColor;
|
|
this.size = size.Value;
|
|
}
|
|
|
|
public readonly Color backgroundColor;
|
|
|
|
public readonly Color borderColor;
|
|
|
|
public readonly float size;
|
|
|
|
public override Widget build(BuildContext context) {
|
|
return new Container(
|
|
width: size,
|
|
height: size,
|
|
margin: EdgeInsets.all(4.0f),
|
|
decoration: new BoxDecoration(
|
|
color: backgroundColor,
|
|
border: Border.all(color: borderColor),
|
|
shape: BoxShape.circle
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
public class TabPageSelector : StatelessWidget {
|
|
public TabPageSelector(
|
|
Key key = null,
|
|
TabController controller = null,
|
|
float indicatorSize = 12.0f,
|
|
Color color = null,
|
|
Color selectedColor = null
|
|
) : base(key: key) {
|
|
D.assert(indicatorSize > 0.0f);
|
|
this.controller = controller;
|
|
this.indicatorSize = indicatorSize;
|
|
this.color = color;
|
|
this.selectedColor = selectedColor;
|
|
}
|
|
|
|
public readonly TabController controller;
|
|
|
|
public readonly float indicatorSize;
|
|
|
|
public readonly Color color;
|
|
|
|
public readonly Color selectedColor;
|
|
|
|
Widget _buildTabIndicator(
|
|
int tabIndex,
|
|
TabController tabController,
|
|
ColorTween selectedColorTween,
|
|
ColorTween previousColorTween) {
|
|
Color background = null;
|
|
if (tabController.indexIsChanging) {
|
|
float t = 1.0f - TabsUtils._indexChangeProgress(tabController);
|
|
if (tabController.index == tabIndex) {
|
|
background = selectedColorTween.lerp(t);
|
|
}
|
|
else if (tabController.previousIndex == tabIndex) {
|
|
background = previousColorTween.lerp(t);
|
|
}
|
|
else {
|
|
background = selectedColorTween.begin;
|
|
}
|
|
}
|
|
else {
|
|
float offset = tabController.offset;
|
|
if (tabController.index == tabIndex) {
|
|
background = selectedColorTween.lerp(1.0f - 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 new TabPageSelectorIndicator(
|
|
backgroundColor: background,
|
|
borderColor: selectedColorTween.end,
|
|
size: indicatorSize
|
|
);
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
Color fixColor = color ?? Colors.transparent;
|
|
Color fixSelectedColor = selectedColor ?? Theme.of(context).accentColor;
|
|
ColorTween selectedColorTween = new ColorTween(begin: fixColor, end: fixSelectedColor);
|
|
ColorTween previousColorTween = new ColorTween(begin: fixSelectedColor, end: fixColor);
|
|
TabController tabController = controller ?? DefaultTabController.of(context);
|
|
D.assert(() => {
|
|
if (tabController == null) {
|
|
throw new UIWidgetsError(
|
|
"No TabController for " + GetType() + ".\n" +
|
|
"When creating a " + GetType() + ", you must either provide an explicit TabController " +
|
|
"using the \"controller\" property, or you must ensure that there is a " +
|
|
"DefaultTabController above the " + GetType() + ".\n" +
|
|
"In this case, there was neither an explicit controller nor a default controller."
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
Animation<float> animation = new CurvedAnimation(
|
|
parent: tabController.animation,
|
|
curve: Curves.fastOutSlowIn
|
|
);
|
|
|
|
return new AnimatedBuilder(
|
|
animation: animation,
|
|
builder: (BuildContext subContext, Widget child) => {
|
|
List<Widget> children = new List<Widget>();
|
|
|
|
for (int tabIndex = 0; tabIndex < tabController.length; tabIndex++) {
|
|
children.Add(_buildTabIndicator(
|
|
tabIndex,
|
|
tabController,
|
|
selectedColorTween,
|
|
previousColorTween)
|
|
);
|
|
}
|
|
|
|
return new Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: children
|
|
);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|