Add animationDuration
property to TabController (#91987)
This commit is contained in:
parent
0d0b2dbae5
commit
a4b5123395
@ -101,11 +101,12 @@ class TabController extends ChangeNotifier {
|
|||||||
///
|
///
|
||||||
/// The `initialIndex` must be valid given [length] and must not be null. If
|
/// The `initialIndex` must be valid given [length] and must not be null. If
|
||||||
/// [length] is zero, then `initialIndex` must be 0 (the default).
|
/// [length] is zero, then `initialIndex` must be 0 (the default).
|
||||||
TabController({ int initialIndex = 0, required this.length, required TickerProvider vsync })
|
TabController({ int initialIndex = 0, Duration? animationDuration, required this.length, required TickerProvider vsync})
|
||||||
: assert(length != null && length >= 0),
|
: assert(length != null && length >= 0),
|
||||||
assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
|
assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
|
||||||
_index = initialIndex,
|
_index = initialIndex,
|
||||||
_previousIndex = initialIndex,
|
_previousIndex = initialIndex,
|
||||||
|
_animationDuration = animationDuration ?? kTabScrollDuration,
|
||||||
_animationController = AnimationController.unbounded(
|
_animationController = AnimationController.unbounded(
|
||||||
value: initialIndex.toDouble(),
|
value: initialIndex.toDouble(),
|
||||||
vsync: vsync,
|
vsync: vsync,
|
||||||
@ -117,14 +118,16 @@ class TabController extends ChangeNotifier {
|
|||||||
required int index,
|
required int index,
|
||||||
required int previousIndex,
|
required int previousIndex,
|
||||||
required AnimationController? animationController,
|
required AnimationController? animationController,
|
||||||
|
required Duration animationDuration,
|
||||||
required this.length,
|
required this.length,
|
||||||
}) : _index = index,
|
}) : _index = index,
|
||||||
_previousIndex = previousIndex,
|
_previousIndex = previousIndex,
|
||||||
_animationController = animationController;
|
_animationController = animationController,
|
||||||
|
_animationDuration = animationDuration;
|
||||||
|
|
||||||
|
|
||||||
/// Creates a new [TabController] with `index`, `previousIndex`, and `length`
|
/// Creates a new [TabController] with `index`, `previousIndex`, `length`, and
|
||||||
/// if they are non-null.
|
/// `animationDuration` if they are non-null.
|
||||||
///
|
///
|
||||||
/// This method is used by [DefaultTabController].
|
/// This method is used by [DefaultTabController].
|
||||||
///
|
///
|
||||||
@ -134,6 +137,7 @@ class TabController extends ChangeNotifier {
|
|||||||
required int? index,
|
required int? index,
|
||||||
required int? length,
|
required int? length,
|
||||||
required int? previousIndex,
|
required int? previousIndex,
|
||||||
|
required Duration? animationDuration,
|
||||||
}) {
|
}) {
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
_animationController!.value = index.toDouble();
|
_animationController!.value = index.toDouble();
|
||||||
@ -143,6 +147,7 @@ class TabController extends ChangeNotifier {
|
|||||||
length: length ?? this.length,
|
length: length ?? this.length,
|
||||||
animationController: _animationController,
|
animationController: _animationController,
|
||||||
previousIndex: previousIndex ?? _previousIndex,
|
previousIndex: previousIndex ?? _previousIndex,
|
||||||
|
animationDuration: animationDuration ?? _animationDuration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +164,12 @@ class TabController extends ChangeNotifier {
|
|||||||
Animation<double>? get animation => _animationController?.view;
|
Animation<double>? get animation => _animationController?.view;
|
||||||
AnimationController? _animationController;
|
AnimationController? _animationController;
|
||||||
|
|
||||||
|
/// Controls the duration of TabController and TabBarView animations.
|
||||||
|
///
|
||||||
|
/// Defaults to kTabScrollDuration.
|
||||||
|
Duration get animationDuration => _animationDuration;
|
||||||
|
final Duration _animationDuration;
|
||||||
|
|
||||||
/// The total number of tabs.
|
/// The total number of tabs.
|
||||||
///
|
///
|
||||||
/// Typically greater than one. Must match [TabBar.tabs]'s and
|
/// Typically greater than one. Must match [TabBar.tabs]'s and
|
||||||
@ -174,7 +185,7 @@ class TabController extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
_previousIndex = index;
|
_previousIndex = index;
|
||||||
_index = value;
|
_index = value;
|
||||||
if (duration != null) {
|
if (duration != null && duration > Duration.zero) {
|
||||||
_indexIsChangingCount += 1;
|
_indexIsChangingCount += 1;
|
||||||
notifyListeners(); // Because the value of indexIsChanging may have changed.
|
notifyListeners(); // Because the value of indexIsChanging may have changed.
|
||||||
_animationController!
|
_animationController!
|
||||||
@ -228,8 +239,8 @@ class TabController extends ChangeNotifier {
|
|||||||
///
|
///
|
||||||
/// While the animation is running [indexIsChanging] is true. When the
|
/// While the animation is running [indexIsChanging] is true. When the
|
||||||
/// animation completes [offset] will be 0.0.
|
/// animation completes [offset] will be 0.0.
|
||||||
void animateTo(int value, { Duration duration = kTabScrollDuration, Curve curve = Curves.ease }) {
|
void animateTo(int value, { Duration? duration, Curve curve = Curves.ease }) {
|
||||||
_changeIndex(value, duration: duration, curve: curve);
|
_changeIndex(value, duration: duration ?? _animationDuration, curve: curve);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The difference between the [animation]'s value and [index].
|
/// The difference between the [animation]'s value and [index].
|
||||||
@ -333,6 +344,7 @@ class DefaultTabController extends StatefulWidget {
|
|||||||
required this.length,
|
required this.length,
|
||||||
this.initialIndex = 0,
|
this.initialIndex = 0,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
this.animationDuration,
|
||||||
}) : assert(initialIndex != null),
|
}) : assert(initialIndex != null),
|
||||||
assert(length >= 0),
|
assert(length >= 0),
|
||||||
assert(length == 0 || (initialIndex >= 0 && initialIndex < length)),
|
assert(length == 0 || (initialIndex >= 0 && initialIndex < length)),
|
||||||
@ -349,6 +361,11 @@ class DefaultTabController extends StatefulWidget {
|
|||||||
/// Defaults to zero.
|
/// Defaults to zero.
|
||||||
final int initialIndex;
|
final int initialIndex;
|
||||||
|
|
||||||
|
/// Controls the duration of DefaultTabController and TabBarView animations.
|
||||||
|
///
|
||||||
|
/// Defaults to kTabScrollDuration.
|
||||||
|
final Duration? animationDuration;
|
||||||
|
|
||||||
/// The widget below this widget in the tree.
|
/// The widget below this widget in the tree.
|
||||||
///
|
///
|
||||||
/// Typically a [Scaffold] whose [AppBar] includes a [TabBar].
|
/// Typically a [Scaffold] whose [AppBar] includes a [TabBar].
|
||||||
@ -384,6 +401,7 @@ class _DefaultTabControllerState extends State<DefaultTabController> with Single
|
|||||||
vsync: this,
|
vsync: this,
|
||||||
length: widget.length,
|
length: widget.length,
|
||||||
initialIndex: widget.initialIndex,
|
initialIndex: widget.initialIndex,
|
||||||
|
animationDuration: widget.animationDuration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,9 +434,19 @@ class _DefaultTabControllerState extends State<DefaultTabController> with Single
|
|||||||
}
|
}
|
||||||
_controller = _controller._copyWith(
|
_controller = _controller._copyWith(
|
||||||
length: widget.length,
|
length: widget.length,
|
||||||
|
animationDuration: widget.animationDuration,
|
||||||
index: newIndex,
|
index: newIndex,
|
||||||
previousIndex: previousIndex,
|
previousIndex: previousIndex,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldWidget.animationDuration != widget.animationDuration) {
|
||||||
|
_controller = _controller._copyWith(
|
||||||
|
length: widget.length,
|
||||||
|
animationDuration: widget.animationDuration,
|
||||||
|
index: _controller.index,
|
||||||
|
previousIndex: _controller.previousIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1392,10 +1392,18 @@ class _TabBarViewState extends State<TabBarView> {
|
|||||||
if (_pageController.page == _currentIndex!.toDouble())
|
if (_pageController.page == _currentIndex!.toDouble())
|
||||||
return Future<void>.value();
|
return Future<void>.value();
|
||||||
|
|
||||||
|
final Duration duration = _controller!.animationDuration;
|
||||||
|
|
||||||
|
if (duration == Duration.zero) {
|
||||||
|
_pageController.jumpToPage(_currentIndex!);
|
||||||
|
return Future<void>.value();
|
||||||
|
}
|
||||||
|
|
||||||
final int previousIndex = _controller!.previousIndex;
|
final int previousIndex = _controller!.previousIndex;
|
||||||
|
|
||||||
if ((_currentIndex! - previousIndex).abs() == 1) {
|
if ((_currentIndex! - previousIndex).abs() == 1) {
|
||||||
_warpUnderwayCount += 1;
|
_warpUnderwayCount += 1;
|
||||||
await _pageController.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease);
|
await _pageController.animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
|
||||||
_warpUnderwayCount -= 1;
|
_warpUnderwayCount -= 1;
|
||||||
return Future<void>.value();
|
return Future<void>.value();
|
||||||
}
|
}
|
||||||
@ -1415,7 +1423,7 @@ class _TabBarViewState extends State<TabBarView> {
|
|||||||
});
|
});
|
||||||
_pageController.jumpToPage(initialPage);
|
_pageController.jumpToPage(initialPage);
|
||||||
|
|
||||||
await _pageController.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease);
|
await _pageController.animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
|
||||||
if (!mounted)
|
if (!mounted)
|
||||||
return Future<void>.value();
|
return Future<void>.value();
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -109,9 +109,11 @@ Widget buildFrame({
|
|||||||
required String value,
|
required String value,
|
||||||
bool isScrollable = false,
|
bool isScrollable = false,
|
||||||
Color? indicatorColor,
|
Color? indicatorColor,
|
||||||
|
Duration? animationDuration,
|
||||||
}) {
|
}) {
|
||||||
return boilerplate(
|
return boilerplate(
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
|
animationDuration: animationDuration,
|
||||||
initialIndex: tabs.indexOf(value),
|
initialIndex: tabs.indexOf(value),
|
||||||
length: tabs.length,
|
length: tabs.length,
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
@ -937,6 +939,262 @@ void main() {
|
|||||||
expect(find.text('Second'), findsNothing);
|
expect(find.text('Second'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('TabBar animationDuration sets indicator animation duration', (WidgetTester tester) async {
|
||||||
|
const Duration animationDuration = Duration(milliseconds: 100);
|
||||||
|
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: animationDuration));
|
||||||
|
final TabController controller = DefaultTabController.of(tester.element(find.text('A')))!;
|
||||||
|
|
||||||
|
await tester.tap(find.text('A'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(controller.indexIsChanging, true);
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pump(animationDuration);
|
||||||
|
expect(controller.index, 0);
|
||||||
|
expect(controller.previousIndex, 1);
|
||||||
|
expect(controller.indexIsChanging, false);
|
||||||
|
|
||||||
|
//Test when index diff is greater than 1
|
||||||
|
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: animationDuration));
|
||||||
|
await tester.tap(find.text('C'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(controller.indexIsChanging, true);
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pump(animationDuration);
|
||||||
|
expect(controller.index, 2);
|
||||||
|
expect(controller.previousIndex, 0);
|
||||||
|
expect(controller.indexIsChanging, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TabBarView controller sets animation duration', (WidgetTester tester) async {
|
||||||
|
const Duration animationDuration = Duration(milliseconds: 100);
|
||||||
|
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||||
|
|
||||||
|
final TabController tabController = TabController(
|
||||||
|
vsync: const TestVSync(),
|
||||||
|
initialIndex: 1,
|
||||||
|
length: tabs.length,
|
||||||
|
animationDuration: animationDuration,
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(boilerplate(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TabBar(
|
||||||
|
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||||
|
controller: tabController,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 400.0,
|
||||||
|
height: 400.0,
|
||||||
|
child: TabBarView(
|
||||||
|
controller: tabController,
|
||||||
|
children: const <Widget>[
|
||||||
|
Center(child: Text('0')),
|
||||||
|
Center(child: Text('1')),
|
||||||
|
Center(child: Text('2')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(tabController.index, 1);
|
||||||
|
|
||||||
|
final PageView pageView = tester.widget(find.byType(PageView));
|
||||||
|
final PageController pageController = pageView.controller;
|
||||||
|
final ScrollPosition position = pageController.position;
|
||||||
|
|
||||||
|
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
|
||||||
|
// page 1 is at 400.0, page 2 is at 800.0.
|
||||||
|
expect(position.pixels, 400);
|
||||||
|
await tester.tap(find.text('C'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(position.pixels, 400);
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pump(animationDuration);
|
||||||
|
expect(position.pixels, 800);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TabBar tap skips indicator animation when disabled in controller', (WidgetTester tester) async {
|
||||||
|
final List<String> tabs = <String>['A', 'B'];
|
||||||
|
|
||||||
|
const Color indicatorColor = Color(0xFFFF0000);
|
||||||
|
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'A', indicatorColor: indicatorColor, animationDuration: Duration.zero));
|
||||||
|
|
||||||
|
final RenderBox box = tester.renderObject(find.byType(TabBar));
|
||||||
|
final TabIndicatorRecordingCanvas canvas = TabIndicatorRecordingCanvas(indicatorColor);
|
||||||
|
final TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas);
|
||||||
|
|
||||||
|
box.paint(context, Offset.zero);
|
||||||
|
final Rect indicatorRect0 = canvas.indicatorRect;
|
||||||
|
expect(indicatorRect0.left, 0.0);
|
||||||
|
expect(indicatorRect0.width, 400.0);
|
||||||
|
expect(indicatorRect0.height, 2.0);
|
||||||
|
|
||||||
|
await tester.tap(find.text('B'));
|
||||||
|
await tester.pump();
|
||||||
|
box.paint(context, Offset.zero);
|
||||||
|
final Rect indicatorRect2 = canvas.indicatorRect;
|
||||||
|
expect(indicatorRect2.left, 400.0);
|
||||||
|
expect(indicatorRect2.width, 400.0);
|
||||||
|
expect(indicatorRect2.height, 2.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TabBar tap changes index instantly when animation is disabled in controller', (WidgetTester tester) async {
|
||||||
|
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: Duration.zero));
|
||||||
|
final TabController controller = DefaultTabController.of(tester.element(find.text('A')))!;
|
||||||
|
|
||||||
|
await tester.tap(find.text('A'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(controller.index, 0);
|
||||||
|
expect(controller.previousIndex, 1);
|
||||||
|
expect(controller.indexIsChanging, false);
|
||||||
|
|
||||||
|
//Test when index diff is greater than 1
|
||||||
|
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: Duration.zero));
|
||||||
|
await tester.tap(find.text('C'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(controller.index, 2);
|
||||||
|
expect(controller.previousIndex, 0);
|
||||||
|
expect(controller.indexIsChanging, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TabBarView skips animation when disabled in controller', (WidgetTester tester) async {
|
||||||
|
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||||
|
final TabController tabController = TabController(
|
||||||
|
vsync: const TestVSync(),
|
||||||
|
initialIndex: 1,
|
||||||
|
length: tabs.length,
|
||||||
|
animationDuration: Duration.zero,
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(boilerplate(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TabBar(
|
||||||
|
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||||
|
controller: tabController,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 400.0,
|
||||||
|
height: 400.0,
|
||||||
|
child: TabBarView(
|
||||||
|
controller: tabController,
|
||||||
|
children: const <Widget>[
|
||||||
|
Center(child: Text('0')),
|
||||||
|
Center(child: Text('1')),
|
||||||
|
Center(child: Text('2')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(tabController.index, 1);
|
||||||
|
|
||||||
|
final PageView pageView = tester.widget(find.byType(PageView));
|
||||||
|
final PageController pageController = pageView.controller;
|
||||||
|
final ScrollPosition position = pageController.position;
|
||||||
|
|
||||||
|
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
|
||||||
|
// page 1 is at 400.0, page 2 is at 800.0.
|
||||||
|
expect(position.pixels, 400);
|
||||||
|
await tester.tap(find.text('C'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(position.pixels, 800);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TabBarView skips animation when disabled in controller - skip tabs', (WidgetTester tester) async {
|
||||||
|
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||||
|
final TabController tabController = TabController(
|
||||||
|
vsync: const TestVSync(),
|
||||||
|
length: tabs.length,
|
||||||
|
animationDuration: Duration.zero,
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(boilerplate(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TabBar(
|
||||||
|
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||||
|
controller: tabController,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 400.0,
|
||||||
|
height: 400.0,
|
||||||
|
child: TabBarView(
|
||||||
|
controller: tabController,
|
||||||
|
children: const <Widget>[
|
||||||
|
Center(child: Text('0')),
|
||||||
|
Center(child: Text('1')),
|
||||||
|
Center(child: Text('2')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(tabController.index, 0);
|
||||||
|
|
||||||
|
final PageView pageView = tester.widget(find.byType(PageView));
|
||||||
|
final PageController pageController = pageView.controller;
|
||||||
|
final ScrollPosition position = pageController.position;
|
||||||
|
|
||||||
|
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
|
||||||
|
// page 1 is at 400.0, page 2 is at 800.0.
|
||||||
|
expect(position.pixels, 0);
|
||||||
|
await tester.tap(find.text('C'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(position.pixels, 800);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TabBarView skips animation when disabled in controller - two tabs', (WidgetTester tester) async {
|
||||||
|
final List<String> tabs = <String>['A', 'B'];
|
||||||
|
final TabController tabController = TabController(
|
||||||
|
vsync: const TestVSync(),
|
||||||
|
length: tabs.length,
|
||||||
|
animationDuration: Duration.zero,
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(boilerplate(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TabBar(
|
||||||
|
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||||
|
controller: tabController,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 400.0,
|
||||||
|
height: 400.0,
|
||||||
|
child: TabBarView(
|
||||||
|
controller: tabController,
|
||||||
|
children: const <Widget>[
|
||||||
|
Center(child: Text('0')),
|
||||||
|
Center(child: Text('1')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(tabController.index, 0);
|
||||||
|
|
||||||
|
final PageView pageView = tester.widget(find.byType(PageView));
|
||||||
|
final PageController pageController = pageView.controller;
|
||||||
|
final ScrollPosition position = pageController.position;
|
||||||
|
|
||||||
|
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
|
||||||
|
// page 1 is at 400.0, page 2 is at 800.0.
|
||||||
|
expect(position.pixels, 0);
|
||||||
|
await tester.tap(find.text('B'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(position.pixels, 400);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('TabBar tap animates the selection indicator', (WidgetTester tester) async {
|
testWidgets('TabBar tap animates the selection indicator', (WidgetTester tester) async {
|
||||||
// This is a regression test for https://github.com/flutter/flutter/issues/7479
|
// This is a regression test for https://github.com/flutter/flutter/issues/7479
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user