forked from peng8350/flutter_pulltorefresh
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrefresh_animatedlist.dart
381 lines (340 loc) · 14.3 KB
/
refresh_animatedlist.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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
/// Signature for the builder callback used by [SliverAnimatedList].
typedef SliverAnimatedListItemBuilder = Widget Function(
BuildContext context, int index, Animation<double> animation);
/// Signature for the builder callback used by [SliverAnimatedListState.removeItem].
typedef SliverAnimatedListRemovedItemBuilder = Widget Function(
BuildContext context, Animation<double> animation);
// The default insert/remove animation duration.
const Duration _kDuration = Duration(milliseconds: 300);
// Incoming and outgoing SliverAnimatedList items.
class _ActiveItem implements Comparable<_ActiveItem> {
_ActiveItem.incoming(this.controller, this.itemIndex)
: removedItemBuilder = null;
_ActiveItem.outgoing(
this.controller, this.itemIndex, this.removedItemBuilder);
_ActiveItem.index(this.itemIndex)
: controller = null,
removedItemBuilder = null;
final AnimationController controller;
final SliverAnimatedListRemovedItemBuilder removedItemBuilder;
int itemIndex;
@override
int compareTo(_ActiveItem other) => itemIndex - other.itemIndex;
}
/// A scrolling container that animates items when they are inserted or removed.
///
/// This widget's [SliverAnimatedListState] can be used to dynamically insert or remove
/// items. To refer to the [SliverAnimatedListState] either provide a [GlobalKey] or
/// use the static [of] method from an item's input callback.
///
/// This widget is similar to one created by [ListView.builder].
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
class SliverAnimatedList extends StatefulWidget {
/// Creates a scrolling container that animates items when they are inserted or removed.
const SliverAnimatedList({
Key key,
@required this.itemBuilder,
this.initialItemCount = 0,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
}) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0),
super(key: key);
/// Called, as needed, to build list item widgets.
///
/// List items are only built when they're scrolled into view.
///
/// The [SliverAnimatedListItemBuilder] index parameter indicates the item's
/// position in the list. The value of the index parameter will be between 0
/// and [initialItemCount] plus the total number of items that have been
/// inserted with [SliverAnimatedListState.insertItem] and less the total number of
/// items that have been removed with [SliverAnimatedListState.removeItem].
///
/// Implementations of this callback should assume that
/// [SliverAnimatedListState.removeItem] removes an item immediately.
final SliverAnimatedListItemBuilder itemBuilder;
/// The number of items the list will start with.
///
/// The appearance of the initial items is not animated. They
/// are created, as needed, by [itemBuilder] with an animation parameter
/// of [kAlwaysCompleteAnimation].
final int initialItemCount;
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
final Axis scrollDirection;
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final bool reverse;
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
final ScrollController controller;
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// On iOS, this identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool primary;
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions.
final ScrollPhysics physics;
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry padding;
/// The state from the closest instance of this class that encloses the given context.
///
/// This method is typically used by [SliverAnimatedList] item widgets that insert or
/// remove items in response to user input.
///
/// ```dart
/// SliverAnimatedListState SliverAnimatedList = SliverAnimatedList.of(context);
/// ```
static SliverAnimatedListState of(BuildContext context,
{bool nullOk = false}) {
assert(context != null);
assert(nullOk != null);
final SliverAnimatedListState result = context
.ancestorStateOfType(const TypeMatcher<SliverAnimatedListState>());
if (nullOk || result != null) return result;
throw FlutterError(
'SliverAnimatedList.of() called with a context that does not contain an SliverAnimatedList.\n'
'No SliverAnimatedList ancestor could be found starting from the context that was passed to SliverAnimatedList.of(). '
'This can happen when the context provided is from the same StatefulWidget that '
'built the SliverAnimatedList. Please see the SliverAnimatedList documentation for examples '
'of how to refer to an SliverAnimatedListState object: '
' https://api.flutter.dev/flutter/widgets/SliverAnimatedListState-class.html \n'
'The context used was:\n'
' $context');
}
@override
SliverAnimatedListState createState() => SliverAnimatedListState();
}
/// The state for a scrolling container that animates items when they are
/// inserted or removed.
///
/// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [SliverAnimatedList.itemBuilder] whenever the item's widget
/// is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder
/// parameter.
///
/// An app that needs to insert or remove items in response to an event
/// can refer to the [SliverAnimatedList]'s state with a global key:
///
/// ```dart
/// GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
/// ...
/// SliverAnimatedList(key: listKey, ...);
/// ...
/// listKey.currentState.insert(123);
/// ```
///
/// [SliverAnimatedList] item input handlers can also refer to their [SliverAnimatedListState]
/// with the static [SliverAnimatedList.of] method.
class SliverAnimatedListState extends State<SliverAnimatedList>
with TickerProviderStateMixin<SliverAnimatedList> {
final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
int _itemsCount = 0;
@override
void initState() {
super.initState();
_itemsCount = widget.initialItemCount;
}
@override
void dispose() {
for (_ActiveItem item in _incomingItems) item.controller.dispose();
for (_ActiveItem item in _outgoingItems) item.controller.dispose();
super.dispose();
}
_ActiveItem _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
final int i = binarySearch(items, _ActiveItem.index(itemIndex));
return i == -1 ? null : items.removeAt(i);
}
_ActiveItem _activeItemAt(List<_ActiveItem> items, int itemIndex) {
final int i = binarySearch(items, _ActiveItem.index(itemIndex));
return i == -1 ? null : items[i];
}
// The insertItem() and removeItem() index parameters are defined as if the
// removeItem() operation removed the corresponding list entry immediately.
// The entry is only actually removed from the ListView when the remove animation
// finishes. The entry is added to _outgoingItems when removeItem is called
// and removed from _outgoingItems when the remove animation finishes.
int _indexToItemIndex(int index) {
int itemIndex = index;
for (_ActiveItem item in _outgoingItems) {
if (item.itemIndex <= itemIndex)
itemIndex += 1;
else
break;
}
return itemIndex;
}
int _itemIndexToIndex(int itemIndex) {
int index = itemIndex;
for (_ActiveItem item in _outgoingItems) {
assert(item.itemIndex != itemIndex);
if (item.itemIndex < itemIndex)
index -= 1;
else
break;
}
return index;
}
/// Insert an item at [index] and start an animation that will be passed
/// to [SliverAnimatedList.itemBuilder] when the item is visible.
///
/// This method's semantics are the same as Dart's [List.insert] method:
/// it increases the length of the list by one and shifts all items at or
/// after [index] towards the end of the list.
void insertItem(int index, {Duration duration = _kDuration}) {
assert(index != null && index >= 0);
assert(duration != null);
final int itemIndex = _indexToItemIndex(index);
assert(itemIndex >= 0 && itemIndex <= _itemsCount);
// Increment the incoming and outgoing item indices to account
// for the insertion.
for (_ActiveItem item in _incomingItems) {
if (item.itemIndex >= itemIndex) item.itemIndex += 1;
}
for (_ActiveItem item in _outgoingItems) {
if (item.itemIndex >= itemIndex) item.itemIndex += 1;
}
final AnimationController controller =
AnimationController(duration: duration, vsync: this);
final _ActiveItem incomingItem =
_ActiveItem.incoming(controller, itemIndex);
setState(() {
_incomingItems
..add(incomingItem)
..sort();
_itemsCount += 1;
});
controller.forward().then<void>((_) {
_removeActiveItemAt(_incomingItems, incomingItem.itemIndex)
.controller
.dispose();
});
}
/// Remove the item at [index] and start an animation that will be passed
/// to [builder] when the item is visible.
///
/// Items are removed immediately. After an item has been removed, its index
/// will no longer be passed to the [SliverAnimatedList.itemBuilder]. However the
/// item will still appear in the list for [duration] and during that time
/// [builder] must construct its widget as needed.
///
/// This method's semantics are the same as Dart's [List.remove] method:
/// it decreases the length of the list by one and shifts all items at or
/// before [index] towards the beginning of the list.
void removeItem(int index, SliverAnimatedListRemovedItemBuilder builder,
{Duration duration = _kDuration}) {
assert(index != null && index >= 0);
assert(builder != null);
assert(duration != null);
final int itemIndex = _indexToItemIndex(index);
assert(itemIndex >= 0 && itemIndex < _itemsCount);
assert(_activeItemAt(_outgoingItems, itemIndex) == null);
final _ActiveItem incomingItem =
_removeActiveItemAt(_incomingItems, itemIndex);
final AnimationController controller = incomingItem?.controller ??
AnimationController(duration: duration, value: 1.0, vsync: this);
final _ActiveItem outgoingItem =
_ActiveItem.outgoing(controller, itemIndex, builder);
setState(() {
_outgoingItems
..add(outgoingItem)
..sort();
});
controller.reverse().then<void>((void value) {
_removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)
.controller
.dispose();
// Decrement the incoming and outgoing item indices to account
// for the removal.
for (_ActiveItem item in _incomingItems) {
if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1;
}
for (_ActiveItem item in _outgoingItems) {
if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1;
}
setState(() {
_itemsCount -= 1;
});
});
}
Widget _itemBuilder(BuildContext context, int itemIndex) {
final _ActiveItem outgoingItem = _activeItemAt(_outgoingItems, itemIndex);
if (outgoingItem != null)
return outgoingItem.removedItemBuilder(
context, outgoingItem.controller.view);
final _ActiveItem incomingItem = _activeItemAt(_incomingItems, itemIndex);
final Animation<double> animation =
incomingItem?.controller?.view ?? kAlwaysCompleteAnimation;
return widget.itemBuilder(context, _itemIndexToIndex(itemIndex), animation);
}
@override
Widget build(BuildContext context) {
return SliverList(
delegate: SliverChildBuilderDelegate(
_itemBuilder,
childCount: _itemsCount,
),
);
}
}