-
Notifications
You must be signed in to change notification settings - Fork 79
/
MotionTabController.dart
258 lines (229 loc) · 7.96 KB
/
MotionTabController.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
class MotionTabController extends ChangeNotifier {
MotionTabController({int initialIndex,this.length = 3, @required TickerProvider vsync})
: /*assert(length != null && length >= 0),*/
assert(initialIndex != null &&
initialIndex >= 0 &&
(length == 0 || initialIndex < length)),
_index = initialIndex,
_previousIndex = initialIndex,
_animationController = AnimationController.unbounded(
value: initialIndex.toDouble(),
vsync: vsync,
);
// Private constructor used by `_copyWith`. This allows a new TabController to
// be created without having to create a new animationController.
MotionTabController._({
int index,
int previousIndex,
AnimationController animationController,
this.length,
}) : _index = index,
_previousIndex = previousIndex,
_animationController = animationController;
MotionTabController _copyWith({int index, int length, int previousIndex}) {
return MotionTabController._(
index: index ?? _index,
length: length ?? this.length,
animationController: _animationController,
previousIndex: previousIndex ?? _previousIndex,
);
}
Animation<double> get animation => _animationController?.view;
AnimationController _animationController;
/// The total number of tabs.
///
/// Typically greater than one. Must match [TabBar.tabs]'s and
/// [TabBarView.children]'s length.
int length = 3;
void _changeIndex(int value, {Duration duration, Curve curve}) {
assert(value != null);
assert(value >= 0 && (value < length || length == 0));
assert(duration != null || curve == null);
assert(_indexIsChangingCount >= 0);
if (value == _index || length < 2) return;
_previousIndex = index;
_index = value;
if (duration != null) {
_indexIsChangingCount += 1;
notifyListeners(); // Because the value of indexIsChanging may have changed.
_animationController
.animateTo(_index.toDouble(), duration: duration, curve: curve)
.whenCompleteOrCancel(() {
_indexIsChangingCount -= 1;
notifyListeners();
});
} else {
_indexIsChangingCount += 1;
_animationController.value = _index.toDouble();
_indexIsChangingCount -= 1;
notifyListeners();
}
}
/// The index of the currently selected tab.
///
/// Changing the index also updates [previousIndex], sets the [animation]'s
/// value to index, resets [indexIsChanging] to false, and notifies listeners.
///
/// To change the currently selected tab and play the [animation] use [animateTo].
///
/// The value of [index] must be valid given [length]. If [length] is zero,
/// then [index] will also be zero.
int get index => _index;
int _index;
set index(int value) {
_changeIndex(value);
}
/// The index of the previously selected tab.
///
/// Initially the same as [index].
int get previousIndex => _previousIndex;
int _previousIndex;
/// True while we're animating from [previousIndex] to [index] as a
/// consequence of calling [animateTo].
///
/// This value is true during the [animateTo] animation that's triggered when
/// the user taps a [TabBar] tab. It is false when [offset] is changing as a
/// consequence of the user dragging (and "flinging") the [TabBarView].
bool get indexIsChanging => _indexIsChangingCount != 0;
int _indexIsChangingCount = 0;
/// Immediately sets [index] and [previousIndex] and then plays the
/// [animation] from its current value to [index].
///
/// While the animation is running [indexIsChanging] is true. When the
/// animation completes [offset] will be 0.0.
void animateTo(int value,
{Duration duration = kTabScrollDuration, Curve curve = Curves.ease}) {
_changeIndex(value, duration: duration, curve: curve);
}
/// The difference between the [animation]'s value and [index].
///
/// The offset value must be between -1.0 and 1.0.
///
/// This property is typically set by the [TabBarView] when the user
/// drags left or right. A value between -1.0 and 0.0 implies that the
/// TabBarView has been dragged to the left. Similarly a value between
/// 0.0 and 1.0 implies that the TabBarView has been dragged to the right.
double get offset => _animationController.value - _index.toDouble();
set offset(double value) {
assert(value != null);
assert(value >= -1.0 && value <= 1.0);
assert(!indexIsChanging);
if (value == offset) return;
_animationController.value = value + _index.toDouble();
}
@override
void dispose() {
_animationController?.dispose();
_animationController = null;
super.dispose();
}
}
class _TabControllerScope extends InheritedWidget {
const _TabControllerScope({
Key key,
this.controller,
this.enabled,
Widget child,
}) : super(key: key, child: child);
final MotionTabController controller;
final bool enabled;
@override
bool updateShouldNotify(_TabControllerScope old) {
return enabled != old.enabled || controller != old.controller;
}
}
class DefaultMotionTabController extends StatefulWidget {
/// Creates a default tab controller for the given [child] widget.
///
/// The [length] argument is typically greater than one. The [length] must
/// match [TabBar.tabs]'s and [TabBarView.children]'s length.
///
/// The [initialIndex] argument must not be null.
const DefaultMotionTabController({
Key key,
this.length,
this.initialIndex,
@required this.child,
}) : assert(initialIndex != null),
assert(length >= 0),
assert(length == 0 || (initialIndex >= 0 && initialIndex < length)),
super(key: key);
/// The total number of tabs.
///
/// Typically greater than one. Must match [TabBar.tabs]'s and
/// [TabBarView.children]'s length.
final int length;
/// The initial index of the selected tab.
///
/// Defaults to zero.
final int initialIndex;
/// The widget below this widget in the tree.
///
/// Typically a [Scaffold] whose [AppBar] includes a [TabBar].
///
/// {@macro flutter.widgets.child}
final Widget child;
/// The closest instance of this class that encloses the given context.
///
/// Typical usage:
///
/// ```dart
/// TabController controller = DefaultTabBarController.of(context);
/// ```
static MotionTabController of(BuildContext context) {
final _TabControllerScope scope =
context.dependOnInheritedWidgetOfExactType<_TabControllerScope>();
return scope?.controller;
}
@override
_DefaultMotionTabControllerState createState() =>
_DefaultMotionTabControllerState();
}
class _DefaultMotionTabControllerState extends State<DefaultMotionTabController>
with SingleTickerProviderStateMixin {
MotionTabController _controller;
@override
void initState() {
super.initState();
_controller = MotionTabController(
vsync: this,
length: widget.length,
initialIndex: widget.initialIndex,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _TabControllerScope(
controller: _controller,
enabled: TickerMode.of(context),
child: widget.child,
);
}
@override
void didUpdateWidget(DefaultMotionTabController oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.length != widget.length) {
// If the length is shortened while the last tab is selected, we should
// automatically update the index of the controller to be the new last tab.
int newIndex;
int previousIndex = _controller.previousIndex;
if (_controller.index >= widget.length) {
newIndex = math.max(0, widget.length - 1);
previousIndex = _controller.index;
}
_controller = _controller._copyWith(
length: widget.length,
index: newIndex,
previousIndex: previousIndex,
);
}
}
}