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
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
// Copyright 2016 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/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:vector_math/vector_math_64.dart';
import 'binding.dart';
import 'box.dart';
import 'object.dart';
import 'sliver.dart';
/// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children.
///
/// [RenderSliverMultiBoxAdaptor] objects reify their children lazily to avoid
/// spending resources on children that are not visible in the viewport. This
/// delegate lets these objects create and remove children as well as estimate
/// the total scroll offset extent occupied by the full child list.
abstract class RenderSliverBoxChildManager {
/// Called during layout when a new child is needed. The child should be
/// inserted into the child list in the appropriate position, after the
/// `after` child (at the start of the list if `after` is null). Its index and
/// scroll offsets will automatically be set appropriately.
///
/// The `index` argument gives the index of the child to show. It is possible
/// for negative indices to be requested. For example: if the user scrolls
/// from child 0 to child 10, and then those children get much smaller, and
/// then the user scrolls back up again, this method will eventually be asked
/// to produce a child for index -1.
///
/// If no child corresponds to `index`, then do nothing.
///
/// Which child is indicated by index zero depends on the [GrowthDirection]
/// specified in the [RenderSliverMultiBoxAdaptor.constraints]. For example
/// if the children are the alphabet, then if
/// [SliverConstraints.growthDirection] is [GrowthDirection.forward] then
/// index zero is A, and index 25 is Z. On the other hand if
/// [SliverConstraints.growthDirection] is [GrowthDirection.reverse]
/// then index zero is Z, and index 25 is A.
///
/// During a call to [createChild] it is valid to remove other children from
/// the [RenderSliverMultiBoxAdaptor] object if they were not created during
/// this frame and have not yet been updated during this frame. It is not
/// valid to add any other children to this render object.
///
/// If this method does not create a child for a given `index` greater than or
/// equal to zero, then [computeMaxScrollOffset] must be able to return a
/// precise value.
void createChild(int index, { @required RenderBox after });
/// Remove the given child from the child list.
///
/// Called by [RenderSliverMultiBoxAdaptor.collectGarbage], which itself is
/// called from [RenderSliverMultiBoxAdaptor.performLayout].
///
/// The index of the given child can be obtained using the
/// [RenderSliverMultiBoxAdaptor.indexOf] method, which reads it from the
/// [SliverMultiBoxAdaptorParentData.index] field of the child's
/// [RenderObject.parentData].
void removeChild(RenderBox child);
/// Called to estimate the total scrollable extents of this object.
///
/// Must return the total distance from the start of the child with the
/// earliest possible index to the end of the child with the last possible
/// index.
double estimateMaxScrollOffset(SliverConstraints constraints, {
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
});
/// Called to obtain a precise measure of the total number of children.
///
/// Must return the number that is one greater than the greatest `index` for
/// which `createChild` will actually create a child.
///
/// This is used when [createChild] cannot add a child for a positive `index`,
/// to determine the precise dimensions of the sliver. It must return an
/// accurate and precise non-null value. It will not be called if
/// [createChild] is always able to create a child (e.g. for an infinite
/// list).
int get childCount;
/// Called during [RenderSliverMultiBoxAdaptor.adoptChild].
///
/// Subclasses must ensure that the [SliverMultiBoxAdaptorParentData.index]
/// field of the child's [RenderObject.parentData] accurately reflects the
/// child's index in the child list after this function returns.
void didAdoptChild(RenderBox child);
/// Called during layout to indicate whether this object provided insufficient
/// children for the [RenderSliverMultiBoxAdaptor] to fill the
/// [SliverConstraints.remainingPaintExtent].
///
/// Typically called unconditionally at the start of layout with false and
/// then later called with true when the [RenderSliverMultiBoxAdaptor]
/// fails to create a child required to fill the
/// [SliverConstraints.remainingPaintExtent].
///
/// Useful for subclasses to determine whether newly added children could
/// affect the visible contents of the [RenderSliverMultiBoxAdaptor].
void setDidUnderflow(bool value);
/// Called at the beginning of layout to indicate that layout is about to
/// occur.
void didStartLayout() { }
/// Called at the end of layout to indicate that layout is now complete.
void didFinishLayout() { }
/// In debug mode, asserts that this manager is not expecting any
/// modifications to the [RenderSliverMultiBoxAdaptor]'s child list.
///
/// This function always returns true.
///
/// The manager is not required to track whether it is expecting modifications
/// to the [RenderSliverMultiBoxAdaptor]'s child list and can simply return
/// true without making any assertions.
bool debugAssertChildListLocked() => true;
}
/// Parent data structure used by [RenderSliverMultiBoxAdaptor].
class SliverMultiBoxAdaptorParentData extends SliverLogicalParentData with ContainerParentDataMixin<RenderBox> {
/// The index of this child according to the [RenderSliverBoxChildManager].
int index;
/// Whether to keep the child alive even when it is no longer visible.
bool keepAlive = false;
/// Whether the widget is currently in the
/// [RenderSliverMultiBoxAdaptor._keepAliveBucket].
bool _keptAlive = false;
@override
String toString() => 'index=$index; ${keepAlive == true ? "keepAlive; " : ""}${super.toString()}';
}
/// A sliver with multiple box children.
///
/// [RenderSliverMultiBoxAdaptor] is a base class for slivers that have multiple
/// box children. The children are managed by a [RenderSliverBoxChildManager],
/// which lets subclasses create children lazily during layout. Typically
/// subclasses will create only those children that are actually needed to fill
/// the [SliverConstraints.remainingPaintExtent].
///
/// The contract for adding and removing children from this render object is
/// more strict than for normal render objects:
///
/// * Children can be removed except during a layout pass if they have already
/// been laid out during that layout pass.
/// * Children cannot be added except during a call to [childManager], and
/// then only if there is no child corresponding to that index (or the child
/// child corresponding to that index was first removed).
///
/// See also:
///
/// * [RenderSliverToBoxAdapter], which has a single box child.
/// * [RenderSliverList], which places its children in a linear
/// array.
/// * [RenderSliverFixedExtentList], which places its children in a linear
/// array with a fixed extent in the main axis.
/// * [RenderSliverGrid], which places its children in arbitrary positions.
abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
with ContainerRenderObjectMixin<RenderBox, SliverMultiBoxAdaptorParentData>,
RenderSliverHelpers {
/// Creates a sliver with multiple box children.
///
/// The [childManager] argument must not be null.
RenderSliverMultiBoxAdaptor({
@required RenderSliverBoxChildManager childManager
}) : assert(childManager != null),
_childManager = childManager;
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverMultiBoxAdaptorParentData)
child.parentData = new SliverMultiBoxAdaptorParentData();
}
/// The delegate that manages the children of this object.
///
/// Rather than having a concrete list of children, a
/// [RenderSliverMultiBoxAdaptor] uses a [RenderSliverBoxChildManager] to
/// create children during layout in order to fill the
/// [SliverConstraints.remainingPaintExtent].
@protected
RenderSliverBoxChildManager get childManager => _childManager;
final RenderSliverBoxChildManager _childManager;
/// The nodes being kept alive despite not being visible.
final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{};
@override
void adoptChild(RenderObject child) {
super.adoptChild(child);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
if (!childParentData._keptAlive)
childManager.didAdoptChild(child);
}
bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked();
@override
void insert(RenderBox child, { RenderBox after }) {
super.insert(child, after: after);
assert(firstChild != null);
assert(() {
int index = indexOf(firstChild);
RenderBox child = childAfter(firstChild);
while (child != null) {
assert(indexOf(child) > index);
index = indexOf(child);
child = childAfter(child);
}
return true;
}());
}
@override
void remove(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
if (!childParentData._keptAlive) {
super.remove(child);
return;
}
assert(_keepAliveBucket[childParentData.index] == child);
_keepAliveBucket.remove(childParentData.index);
dropChild(child);
}
@override
void removeAll() {
super.removeAll();
_keepAliveBucket.values.forEach(dropChild);
_keepAliveBucket.clear();
}
void _createOrObtainChild(int index, { RenderBox after }) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
if (_keepAliveBucket.containsKey(index)) {
final RenderBox child = _keepAliveBucket.remove(index);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(childParentData._keptAlive);
dropChild(child);
child.parentData = childParentData;
insert(child, after: after);
childParentData._keptAlive = false;
} else {
_childManager.createChild(index, after: after);
}
});
}
void _destroyOrCacheChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
if (childParentData.keepAlive) {
assert(!childParentData._keptAlive);
remove(child);
_keepAliveBucket[childParentData.index] = child;
child.parentData = childParentData;
super.adoptChild(child);
childParentData._keptAlive = true;
} else {
assert(child.parent == this);
_childManager.removeChild(child);
assert(child.parent == null);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
for (RenderBox child in _keepAliveBucket.values)
child.attach(owner);
}
@override
void detach() {
super.detach();
for (RenderBox child in _keepAliveBucket.values)
child.detach();
}
@override
void redepthChildren() {
super.redepthChildren();
_keepAliveBucket.values.forEach(redepthChild);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
super.visitChildren(visitor);
_keepAliveBucket.values.forEach(visitor);
}
/// Called during layout to create and add the child with the given index and
/// scroll offset.
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child if necessary. The child may instead be obtained from a cache;
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
///
/// Returns false if there was no cached child and `createChild` did not add
/// any child, otherwise returns true.
///
/// Does not layout the new child.
///
/// When this is called, there are no visible children, so no children can be
/// removed during the call to `createChild`. No child should be added during
/// that call either, except for the one that is created and returned by
/// `createChild`.
@protected
bool addInitialChild({ int index = 0, double layoutOffset = 0.0 }) {
assert(_debugAssertChildListLocked());
assert(firstChild == null);
_createOrObtainChild(index, after: null);
if (firstChild != null) {
assert(firstChild == lastChild);
assert(indexOf(firstChild) == index);
final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild.parentData;
firstChildParentData.layoutOffset = layoutOffset;
return true;
}
childManager.setDidUnderflow(true);
return false;
}
/// Called during layout to create, add, and layout the child before
/// [firstChild].
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child if necessary. The child may instead be obtained from a cache;
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
///
/// Returns the new child or null if no child was obtained.
///
/// The child that was previously the first child, as well as any subsequent
/// children, may be removed by this call if they have not yet been laid out
/// during this layout pass. No child should be added during that call except
/// for the one that is created and returned by `createChild`.
@protected
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {
bool parentUsesSize = false,
}) {
assert(_debugAssertChildListLocked());
final int index = indexOf(firstChild) - 1;
_createOrObtainChild(index, after: null);
if (indexOf(firstChild) == index) {
firstChild.layout(childConstraints, parentUsesSize: parentUsesSize);
return firstChild;
}
childManager.setDidUnderflow(true);
return null;
}
/// Called during layout to create, add, and layout the child after
/// the given child.
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child if necessary. The child may instead be obtained from a cache;
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
///
/// Returns the new child. It is the responsibility of the caller to configure
/// the child's scroll offset.
///
/// Children after the `after` child may be removed in the process. Only the
/// new child may be added.
@protected
RenderBox insertAndLayoutChild(BoxConstraints childConstraints, {
@required RenderBox after,
bool parentUsesSize = false,
}) {
assert(_debugAssertChildListLocked());
assert(after != null);
final int index = indexOf(after) + 1;
_createOrObtainChild(index, after: after);
final RenderBox child = childAfter(after);
if (child != null && indexOf(child) == index) {
child.layout(childConstraints, parentUsesSize: parentUsesSize);
return child;
}
childManager.setDidUnderflow(true);
return null;
}
/// Called after layout with the number of children that can be garbage
/// collected at the head and tail of the child list.
///
/// Children whose [SliverMultiBoxAdaptorParentData.keepAlive] property is
/// set to true will be removed to a cache instead of being dropped.
///
/// This method also collects any children that were previously kept alive but
/// are now no longer necessary. As such, it should be called every time
/// [performLayout] is run, even if the arguments are both zero.
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
assert(_debugAssertChildListLocked());
assert(childCount >= leadingGarbage + trailingGarbage);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
while (leadingGarbage > 0) {
_destroyOrCacheChild(firstChild);
leadingGarbage -= 1;
}
while (trailingGarbage > 0) {
_destroyOrCacheChild(lastChild);
trailingGarbage -= 1;
}
// Ask the child manager to remove the children that are no longer being
// kept alive. (This should cause _keepAliveBucket to change, so we have
// to prepare our list ahead of time.)
_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
return !childParentData.keepAlive;
}).toList().forEach(_childManager.removeChild);
assert(_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
return !childParentData.keepAlive;
}).isEmpty);
});
}
/// Returns the index of the given child, as given by the
/// [SliverMultiBoxAdaptorParentData.index] field of the child's [parentData].
int indexOf(RenderBox child) {
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(childParentData.index != null);
return childParentData.index;
}
/// Returns the dimension of the given child in the main axis, as given by the
/// child's [RenderBox.size] property. This is only valid after layout.
@protected
double paintExtentOf(RenderBox child) {
assert(child != null);
assert(child.hasSize);
switch (constraints.axis) {
case Axis.horizontal:
return child.size.width;
case Axis.vertical:
return child.size.height;
}
return null;
}
@override
bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
RenderBox child = lastChild;
while (child != null) {
if (hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition))
return true;
child = childBefore(child);
}
return false;
}
@override
double childMainAxisPosition(RenderBox child) {
return childScrollOffset(child) - constraints.scrollOffset;
}
@override
double childScrollOffset(RenderObject child) {
assert(child != null);
assert(child.parent == this);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(childParentData.layoutOffset != null);
return childParentData.layoutOffset;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
applyPaintTransformForBoxChild(child, transform);
}
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
// offset is to the top-left corner, regardless of our axis direction.
// originOffset gives us the delta from the real origin to the origin in the axis direction.
Offset mainAxisUnit, crossAxisUnit, originOffset;
bool addExtent;
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
mainAxisUnit = const Offset(0.0, -1.0);
crossAxisUnit = const Offset(1.0, 0.0);
originOffset = offset + new Offset(0.0, geometry.paintExtent);
addExtent = true;
break;
case AxisDirection.right:
mainAxisUnit = const Offset(1.0, 0.0);
crossAxisUnit = const Offset(0.0, 1.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.down:
mainAxisUnit = const Offset(0.0, 1.0);
crossAxisUnit = const Offset(1.0, 0.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.left:
mainAxisUnit = const Offset(-1.0, 0.0);
crossAxisUnit = const Offset(0.0, 1.0);
originOffset = offset + new Offset(geometry.paintExtent, 0.0);
addExtent = true;
break;
}
assert(mainAxisUnit != null);
assert(addExtent != null);
RenderBox child = firstChild;
while (child != null) {
final double mainAxisDelta = childMainAxisPosition(child);
final double crossAxisDelta = childCrossAxisPosition(child);
Offset childOffset = new Offset(
originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta,
originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta,
);
if (addExtent)
childOffset += mainAxisUnit * paintExtentOf(child);
context.paintChild(child, childOffset);
child = childAfter(child);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsNode.message(firstChild != null ? 'currently live children: ${indexOf(firstChild)} to ${indexOf(lastChild)}' : 'no children current live'));
}
/// Asserts that the reified child list is not empty and has a contiguous
/// sequence of indices.
///
/// Always returns true.
bool debugAssertChildListIsNonEmptyAndContiguous() {
assert(() {
assert(firstChild != null);
int index = indexOf(firstChild);
RenderBox child = childAfter(firstChild);
while (child != null) {
index += 1;
assert(indexOf(child) == index);
child = childAfter(child);
}
return true;
}());
return true;
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> children = <DiagnosticsNode>[];
if (firstChild != null) {
RenderBox child = firstChild;
while (true) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
children.add(child.toDiagnosticsNode(name: 'child with index ${childParentData.index}'));
if (child == lastChild)
break;
child = childParentData.nextSibling;
}
}
if (_keepAliveBucket.isNotEmpty) {
final List<int> indices = _keepAliveBucket.keys.toList()..sort();
for (int index in indices) {
children.add(_keepAliveBucket[index].toDiagnosticsNode(
name: 'child with index $index (kept alive offstage)',
style: DiagnosticsTreeStyle.offstage,
));
}
}
return children;
}
}