Commit 8a365886 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Fixed scrollable TabBar flashing (#8741)

parent 71aaa5db
...@@ -99,6 +99,7 @@ class TabController extends ChangeNotifier { ...@@ -99,6 +99,7 @@ class TabController extends ChangeNotifier {
_index = value; _index = value;
if (duration != null) { if (duration != null) {
_indexIsChangingCount += 1; _indexIsChangingCount += 1;
notifyListeners(); // Because the value of indexIsChanging may have changed.
_animationController _animationController
..animateTo(_index.toDouble(), duration: duration, curve: curve).whenComplete(() { ..animateTo(_index.toDouble(), duration: duration, curve: curve).whenComplete(() {
_indexIsChangingCount -= 1; _indexIsChangingCount -= 1;
......
...@@ -133,8 +133,8 @@ class _TabStyle extends AnimatedWidget { ...@@ -133,8 +133,8 @@ class _TabStyle extends AnimatedWidget {
final Color unselectedColor = unselectedLabelColor ?? selectedColor.withAlpha(0xB2); // 70% alpha final Color unselectedColor = unselectedLabelColor ?? selectedColor.withAlpha(0xB2); // 70% alpha
final Animation<double> animation = listenable; final Animation<double> animation = listenable;
final Color color = selected final Color color = selected
? Color.lerp(unselectedColor, selectedColor, animation.value) ? Color.lerp(selectedColor, unselectedColor, animation.value)
: Color.lerp(selectedColor, unselectedColor, animation.value); : Color.lerp(unselectedColor, selectedColor, animation.value);
return new DefaultTextStyle( return new DefaultTextStyle(
style: textStyle.copyWith(color: color), style: textStyle.copyWith(color: color),
...@@ -229,18 +229,17 @@ class _TabLabelBar extends Flex { ...@@ -229,18 +229,17 @@ class _TabLabelBar extends Flex {
} }
double _indexChangeProgress(TabController controller) { double _indexChangeProgress(TabController controller) {
if (!controller.indexIsChanging)
return 1.0;
final double controllerValue = controller.animation.value; final double controllerValue = controller.animation.value;
final double previousIndex = controller.previousIndex.toDouble(); final double previousIndex = controller.previousIndex.toDouble();
final double currentIndex = controller.index.toDouble(); final double currentIndex = controller.index.toDouble();
if (controllerValue == previousIndex)
return 0.0; // The controller's offset is changing because the user is dragging the
else if (controllerValue == currentIndex) // TabBarView's PageView to the left or right.
return 1.0; if (!controller.indexIsChanging)
else return (currentIndex - controllerValue).abs().clamp(0.0, 1.0);
return (controllerValue - previousIndex).abs() / (currentIndex - previousIndex).abs();
// The TabController animation's value is changing from previousIndex to currentIndex.
return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs();
} }
class _IndicatorPainter extends CustomPainter { class _IndicatorPainter extends CustomPainter {
...@@ -321,6 +320,22 @@ class _ChangeAnimation extends Animation<double> with AnimationWithParentMixin<d ...@@ -321,6 +320,22 @@ class _ChangeAnimation extends Animation<double> with AnimationWithParentMixin<d
double get value => _indexChangeProgress(controller); 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
double get value {
assert(!controller.indexIsChanging);
return (controller.animation.value - index.toDouble()).abs().clamp(0.0, 1.0);
}
}
/// A material design widget that displays a horizontal row of tabs. /// A material design widget that displays a horizontal row of tabs.
/// ///
/// Typically created as part of an [AppBar] and in conjuction with a /// Typically created as part of an [AppBar] and in conjuction with a
...@@ -426,7 +441,6 @@ class _TabBarState extends State<TabBar> { ...@@ -426,7 +441,6 @@ class _TabBarState extends State<TabBar> {
final ScrollController _scrollController = new ScrollController(); final ScrollController _scrollController = new ScrollController();
TabController _controller; TabController _controller;
_ChangeAnimation _changeAnimation;
_IndicatorPainter _indicatorPainter; _IndicatorPainter _indicatorPainter;
int _currentIndex; int _currentIndex;
...@@ -435,12 +449,14 @@ class _TabBarState extends State<TabBar> { ...@@ -435,12 +449,14 @@ class _TabBarState extends State<TabBar> {
if (newController == _controller) if (newController == _controller)
return; return;
if (_controller != null) if (_controller != null) {
_controller.animation.removeListener(_handleTick); _controller.animation.removeListener(_handleTabControllerAnimationTick);
_controller.removeListener(_handleTabControllerTick);
}
_controller = newController; _controller = newController;
if (_controller != null) { if (_controller != null) {
_controller.animation.addListener(_handleTick); _controller.animation.addListener(_handleTabControllerAnimationTick);
_changeAnimation = new _ChangeAnimation(_controller); _controller.addListener(_handleTabControllerTick);
_currentIndex = _controller.index; _currentIndex = _controller.index;
final List<double> offsets = _indicatorPainter?.tabOffsets; final List<double> offsets = _indicatorPainter?.tabOffsets;
_indicatorPainter = new _IndicatorPainter(_controller)..tabOffsets = offsets; _indicatorPainter = new _IndicatorPainter(_controller)..tabOffsets = offsets;
...@@ -463,7 +479,7 @@ class _TabBarState extends State<TabBar> { ...@@ -463,7 +479,7 @@ class _TabBarState extends State<TabBar> {
@override @override
void dispose() { void dispose() {
if (_controller != null) if (_controller != null)
_controller.animation.removeListener(_handleTick); _controller.animation.removeListener(_handleTabControllerAnimationTick);
// We don't own the _controller Animation, so it's not disposed here. // We don't own the _controller Animation, so it's not disposed here.
super.dispose(); super.dispose();
} }
...@@ -509,18 +525,20 @@ class _TabBarState extends State<TabBar> { ...@@ -509,18 +525,20 @@ class _TabBarState extends State<TabBar> {
_scrollController.jumpTo(offset); _scrollController.jumpTo(offset);
} }
void _handleTick() { void _handleTabControllerAnimationTick() {
assert(mounted); assert(mounted);
if (!_controller.indexIsChanging && config.isScrollable) {
// Sync the TabBar's scroll position with the TabBarView's PageView.
_currentIndex = _controller.index;
_scrollToControllerValue();
}
}
if (_controller.indexIsChanging) { void _handleTabControllerTick() {
setState(() { setState(() {
// Rebuild so that the tab label colors reflect the selected tab index. // Rebuild the tabs after a (potentially animated) index change
// The first build for a new _controller.index value will also trigger // has completed.
// a scroll to center the selected tab.
}); });
} else if (config.isScrollable) {
_scrollToControllerValue();
}
} }
void _saveTabOffsets(List<double> tabOffsets) { void _saveTabOffsets(List<double> tabOffsets) {
...@@ -532,6 +550,18 @@ class _TabBarState extends State<TabBar> { ...@@ -532,6 +550,18 @@ class _TabBarState extends State<TabBar> {
_controller.animateTo(index); _controller.animateTo(index);
} }
Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) {
return new _TabStyle(
animation: animation,
selected: selected,
labelColor: config.labelColor,
unselectedLabelColor: config.unselectedLabelColor,
labelStyle: config.labelStyle,
unselectedLabelStyle: config.unselectedLabelStyle,
child: child,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> wrappedTabs = new List<Widget>.from(config.tabs, growable: false); final List<Widget> wrappedTabs = new List<Widget>.from(config.tabs, growable: false);
...@@ -561,35 +591,26 @@ class _TabBarState extends State<TabBar> { ...@@ -561,35 +591,26 @@ class _TabBarState extends State<TabBar> {
final int previousIndex = _controller.previousIndex; final int previousIndex = _controller.previousIndex;
if (_controller.indexIsChanging) { if (_controller.indexIsChanging) {
// The user tapped on a tab, the tab controller's animation is running.
assert(_currentIndex != previousIndex); assert(_currentIndex != previousIndex);
wrappedTabs[_currentIndex] = new _TabStyle( final Animation<double> animation = new _ChangeAnimation(_controller);
animation: _changeAnimation, wrappedTabs[_currentIndex] = _buildStyledTab(wrappedTabs[_currentIndex], true, animation);
selected: true, wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
labelColor: config.labelColor,
unselectedLabelColor: config.unselectedLabelColor,
labelStyle: config.labelStyle,
unselectedLabelStyle: config.unselectedLabelStyle,
child: wrappedTabs[_currentIndex],
);
wrappedTabs[previousIndex] = new _TabStyle(
animation: _changeAnimation,
selected: false,
labelColor: config.labelColor,
unselectedLabelColor: config.unselectedLabelColor,
labelStyle: config.labelStyle,
unselectedLabelStyle: config.unselectedLabelStyle,
child: wrappedTabs[previousIndex],
);
} else { } else {
wrappedTabs[_currentIndex] = new _TabStyle( // The user is dragging the TabBarView's PageView left or right.
animation: kAlwaysCompleteAnimation, final int tabIndex = _currentIndex;
selected: true, final Animation<double> centerAnimation = new _DragAnimation(_controller, tabIndex);
labelColor: config.labelColor, wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
unselectedLabelColor: config.unselectedLabelColor, if (_currentIndex > 0) {
labelStyle: config.labelStyle, final int tabIndex = _currentIndex - 1;
unselectedLabelStyle: config.unselectedLabelStyle, final Animation<double> leftAnimation = new _DragAnimation(_controller, tabIndex);
child: wrappedTabs[_currentIndex], wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, leftAnimation);
); }
if (_currentIndex < config.tabs.length - 1) {
final int tabIndex = _currentIndex + 1;
final Animation<double> rightAnimation = new _DragAnimation(_controller, tabIndex);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, rightAnimation);
}
} }
} }
...@@ -610,7 +631,7 @@ class _TabBarState extends State<TabBar> { ...@@ -610,7 +631,7 @@ class _TabBarState extends State<TabBar> {
child: new Padding( child: new Padding(
padding: const EdgeInsets.only(bottom: _kTabIndicatorHeight), padding: const EdgeInsets.only(bottom: _kTabIndicatorHeight),
child: new _TabStyle( child: new _TabStyle(
animation: kAlwaysCompleteAnimation, animation: kAlwaysDismissedAnimation,
selected: false, selected: false,
labelColor: config.labelColor, labelColor: config.labelColor,
unselectedLabelColor: config.unselectedLabelColor, unselectedLabelColor: config.unselectedLabelColor,
...@@ -681,10 +702,10 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -681,10 +702,10 @@ class _TabBarViewState extends State<TabBarView> {
return; return;
if (_controller != null) if (_controller != null)
_controller.animation.removeListener(_handleTick); _controller.animation.removeListener(_handleTabControllerAnimationTick);
_controller = newController; _controller = newController;
if (_controller != null) if (_controller != null)
_controller.animation.addListener(_handleTick); _controller.animation.addListener(_handleTabControllerAnimationTick);
} }
@override @override
...@@ -713,13 +734,13 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -713,13 +734,13 @@ class _TabBarViewState extends State<TabBarView> {
@override @override
void dispose() { void dispose() {
if (_controller != null) if (_controller != null)
_controller.animation.removeListener(_handleTick); _controller.animation.removeListener(_handleTabControllerAnimationTick);
// We don't own the _controller Animation, so it's not disposed here. // We don't own the _controller Animation, so it's not disposed here.
super.dispose(); super.dispose();
} }
void _handleTick() { void _handleTabControllerAnimationTick() {
if (!_controller.indexIsChanging) if (_warpUnderwayCount > 0 || !_controller.indexIsChanging)
return; // This widget is driving the controller's animation. return; // This widget is driving the controller's animation.
if (_controller.index != _currentIndex) { if (_controller.index != _currentIndex) {
...@@ -773,12 +794,18 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -773,12 +794,18 @@ class _TabBarViewState extends State<TabBarView> {
if (notification.depth != 0) if (notification.depth != 0)
return false; return false;
_warpUnderwayCount += 1;
if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) { if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) {
_currentIndex = _pageController.page.round(); if ((_pageController.page - _controller.index).abs() > 1.0) {
if (_currentIndex != _controller.index) _controller.index = _pageController.page.floor();
_controller.index = _currentIndex; _currentIndex=_controller.index;
}
_controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0); _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0);
} else if (notification is ScrollEndNotification) {
_controller.index = _pageController.page.floor();
_currentIndex = _controller.index;
} }
_warpUnderwayCount -= 1;
return false; return false;
} }
......
...@@ -618,7 +618,7 @@ void main() { ...@@ -618,7 +618,7 @@ void main() {
expect(secondColor, equals(Colors.blue[500])); expect(secondColor, equals(Colors.blue[500]));
}); });
testWidgets('TabBar unselectedLabelColor control test', (WidgetTester tester) async { testWidgets('TabBarView page left and right test', (WidgetTester tester) async {
final TabController controller = new TabController( final TabController controller = new TabController(
vsync: const TestVSync(), vsync: const TestVSync(),
length: 2, length: 2,
...@@ -635,29 +635,46 @@ void main() { ...@@ -635,29 +635,46 @@ void main() {
expect(controller.index, equals(0)); expect(controller.index, equals(0));
final TestGesture gesture = await tester.startGesture(const Point(100.0, 100.0)); TestGesture gesture = await tester.startGesture(const Point(100.0, 100.0));
expect(controller.index, equals(0)); expect(controller.index, equals(0));
await gesture.moveBy(const Offset(-380.0, 0.0)); // Drag to the left and right, by less than the TabBarView's width.
// The selected index (controller.index) should not change.
await gesture.moveBy(const Offset(-100.0, 0.0));
await gesture.moveBy(const Offset(100.0, 0.0));
expect(controller.index, equals(0)); expect(controller.index, equals(0));
expect(find.text('First'), findsOneWidget);
expect(find.text('Second'), findsNothing);
await gesture.moveBy(const Offset(-40.0, 0.0)); // Drag more than the TabBarView's width to the right. This forces
// the selected index to change to 1.
await gesture.moveBy(const Offset(-500.0, 0.0));
await gesture.up();
await tester.pump(); // start the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, equals(1)); expect(controller.index, equals(1));
expect(find.text('First'), findsNothing);
expect(find.text('Second'), findsOneWidget);
await gesture.moveBy(const Offset(-40.0, 0.0)); gesture = await tester.startGesture(const Point(100.0, 100.0));
await tester.pump();
expect(controller.index, equals(1)); expect(controller.index, equals(1));
await gesture.up(); // Drag to the left and right, by less than the TabBarView's width.
await tester.pumpUntilNoTransientCallbacks(); // The selected index (controller.index) should not change.
await gesture.moveBy(const Offset(-100.0, 0.0));
await gesture.moveBy(const Offset(100.0, 0.0));
expect(controller.index, equals(1)); expect(controller.index, equals(1));
expect(find.text('First'), findsNothing); expect(find.text('First'), findsNothing);
expect(find.text('Second'), findsOneWidget); expect(find.text('Second'), findsOneWidget);
});
// Drag more than the TabBarView's width to the left. This forces
// the selected index to change back to 0.
await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up();
await tester.pump(); // start the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, equals(0));
expect(find.text('First'), findsOneWidget);
expect(find.text('Second'), findsNothing);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment