Skip to content

Commit

Permalink
Merge pull request #969 from HansMuller/fix_tab_swipe_animation
Browse files Browse the repository at this point in the history
Correct the TabBarView swipe selection change animation

The TabBarSelection change animation needs to start where the fling's drag gesture ended rather than from zero. The intial vlaue of progress for the TabBarSelection's performance is now converted from the range used during an interactive drag, to the range used when animating from the previously selected tab to the new one.

TabBarSelection now requires a maxIndex parameter.
  • Loading branch information
Hans Muller committed Dec 16, 2015
2 parents bf75192 + 65dca5b commit fd4599f
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 26 deletions.
2 changes: 1 addition & 1 deletion examples/material_gallery/lib/demo/tabs_demo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import 'package:flutter/material.dart';

import 'widget_demo.dart';

final TabBarSelection _selection = new TabBarSelection();
final List<String> _iconNames = <String>["event", "home", "android", "alarm", "face", "language"];
final TabBarSelection _selection = new TabBarSelection(maxIndex: _iconNames.length - 1);

Widget buildTabBar(_) {
return new TabBar(
Expand Down
2 changes: 1 addition & 1 deletion examples/stocks/lib/stock_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class StockHomeState extends State<StockHome> {
super.initState();
_tabBarSelection = PageStorage.of(context)?.readState(context);
if (_tabBarSelection == null) {
_tabBarSelection = new TabBarSelection();
_tabBarSelection = new TabBarSelection(maxIndex: 1);
PageStorage.of(context)?.writeState(context, _tabBarSelection);
}
}
Expand Down
74 changes: 52 additions & 22 deletions packages/flutter/lib/src/material/tabs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,14 @@ class _TabsScrollBehavior extends BoundedBehavior {
}

class TabBarSelection {
TabBarSelection({ int index: 0, this.onChanged }) : _index = index;
TabBarSelection({ int index: 0, this.maxIndex, this.onChanged }) : _index = index {
assert(maxIndex != null);
assert(index != null);
assert(_index >= 0 && _index <= maxIndex);
}

final VoidCallback onChanged;
final int maxIndex;

PerformanceView get performance => _performance.view;
final _performance = new Performance(duration: _kTabBarScroll, progress: 1.0);
Expand All @@ -400,9 +405,33 @@ class TabBarSelection {
_previousIndex = _index;
_index = value;
_indexIsChanging = true;

// If the selected index change was triggered by a drag gesture, the current
// value of _performance.progress will reflect where the gesture ended. While
// the drag was underway progress indicates where the indicator and TabBarView
// scrollPosition are vis the indices of the two tabs adjacent to the selected
// one. So 0.5 means the drag didn't move at all, 0.0 means the drag extended
// to the beginning of the tab on the left and 1.0 likewise for the tab on the
// right. That is unless the selected index was 0 or maxIndex. In those cases
// progress just moves between the selected tab and the adjacent one.
// Convert progress to reflect the fact that we're now moving between (just)
// the previous and current selection index.

double progress;
if (_performance.status == PerformanceStatus.completed)
progress = 0.0;
else if (_previousIndex == 0)
progress = _performance.progress;
else if (_previousIndex == maxIndex)
progress = 1.0 - _performance.progress;
else if (_previousIndex < _index)
progress = (_performance.progress - 0.5) * 2.0;
else
progress = 1.0 - _performance.progress * 2.0;

_performance
..progress = 0.0
..play().then((_) {
..progress = progress
..forward().then((_) {
if (onChanged != null)
onChanged();
_indexIsChanging = false;
Expand All @@ -425,7 +454,9 @@ class TabBar extends Scrollable {
this.isScrollable: false
}) : super(key: key, scrollDirection: ScrollDirection.horizontal) {
assert(labels != null);
assert(labels.length > 1);
assert(selection != null);
assert(selection.maxIndex == labels.length - 1);
}

final Iterable<TabLabel> labels;
Expand Down Expand Up @@ -648,7 +679,10 @@ class TabBarView<T> extends PageableList<T> {
itemBuilder: itemBuilder,
itemsWrap: false
) {
assert(items != null);
assert(items.length > 1);
assert(selection != null);
assert(selection.maxIndex == items.length - 1);
}

final TabBarSelection selection;
Expand Down Expand Up @@ -690,43 +724,39 @@ class _TabBarViewState<T> extends PageableListState<T, TabBarView<T>> {
super.initState();
_initItemIndicesAndScrollPosition();
_performance
..addStatusListener(_handleStatusChange)
..addListener(_handleProgressChange);
}

void dispose() {
_performance
..removeStatusListener(_handleStatusChange)
..removeListener(_handleProgressChange)
..stop();
super.dispose();
}

void _handleStatusChange(PerformanceStatus status) {
void _handleProgressChange() {
if (!config.selection.indexIsChanging)
return;
// The TabBar is driving the TabBarSelection performance.

final int selectedIndex = config.selection.index;
final int previousSelectedIndex = config.selection.previousIndex;

if (status == PerformanceStatus.forward) {
if (selectedIndex < previousSelectedIndex) {
_itemIndices = <int>[selectedIndex, previousSelectedIndex];
_scrollDirection = AnimationDirection.reverse;
} else {
_itemIndices = <int>[previousSelectedIndex, selectedIndex];
_scrollDirection = AnimationDirection.forward;
}
} else if (status == PerformanceStatus.completed) {
if (_performance.status == PerformanceStatus.completed) {
_initItemIndicesAndScrollPosition();
return;
}
}

void _handleProgressChange() {
if (!config.selection.indexIsChanging)
if (_performance.status != PerformanceStatus.forward)
return;
// The TabBar is driving the TabBarSelection performance.

final int selectedIndex = config.selection.index;
final int previousSelectedIndex = config.selection.previousIndex;

if (selectedIndex < previousSelectedIndex) {
_itemIndices = <int>[selectedIndex, previousSelectedIndex];
_scrollDirection = AnimationDirection.reverse;
} else {
_itemIndices = <int>[previousSelectedIndex, selectedIndex];
_scrollDirection = AnimationDirection.forward;
}

if (_scrollDirection == AnimationDirection.forward)
scrollTo(_performance.progress);
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter/test/widget/tabs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ void main() {
test('TabBar tap selects tab', () {
testWidgets((WidgetTester tester) {
List<String> tabs = <String>['A', 'B', 'C'];
selection = new TabBarSelection(index: 2);
selection = new TabBarSelection(index: 2, maxIndex: tabs.length - 1);

tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
expect(tester.findText('A'), isNotNull);
Expand Down Expand Up @@ -51,7 +51,7 @@ void main() {
test('Scrollable TabBar tap selects tab', () {
testWidgets((WidgetTester tester) {
List<String> tabs = <String>['A', 'B', 'C'];
selection = new TabBarSelection(index: 2);
selection = new TabBarSelection(index: 2, maxIndex: tabs.length - 1);

tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
expect(tester.findText('A'), isNotNull);
Expand Down

0 comments on commit fd4599f

Please sign in to comment.