-
Notifications
You must be signed in to change notification settings - Fork 26.8k
/
viewport.dart
1981 lines (1829 loc) · 76.1 KB
/
viewport.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
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
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2014 The Flutter 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 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';
import 'box.dart';
import 'debug.dart';
import 'layer.dart';
import 'object.dart';
import 'sliver.dart';
import 'viewport_offset.dart';
/// The unit of measurement for a [Viewport.cacheExtent].
enum CacheExtentStyle {
/// Treat the [Viewport.cacheExtent] as logical pixels.
pixel,
/// Treat the [Viewport.cacheExtent] as a multiplier of the main axis extent.
viewport,
}
/// An interface for render objects that are bigger on the inside.
///
/// Some render objects, such as [RenderViewport], present a portion of their
/// content, which can be controlled by a [ViewportOffset]. This interface lets
/// the framework recognize such render objects and interact with them without
/// having specific knowledge of all the various types of viewports.
abstract interface class RenderAbstractViewport extends RenderObject {
/// Returns the [RenderAbstractViewport] that most tightly encloses the given
/// render object.
///
/// If the object does not have a [RenderAbstractViewport] as an ancestor,
/// this function returns null.
///
/// See also:
///
/// * [RenderAbstractViewport.of], which is similar to this method, but
/// asserts if no [RenderAbstractViewport] ancestor is found.
static RenderAbstractViewport? maybeOf(RenderObject? object) {
while (object != null) {
if (object is RenderAbstractViewport) {
return object;
}
object = object.parent;
}
return null;
}
/// Returns the [RenderAbstractViewport] that most tightly encloses the given
/// render object.
///
/// If the object does not have a [RenderAbstractViewport] as an ancestor,
/// this function will assert in debug mode, and throw an exception in release
/// mode.
///
/// See also:
///
/// * [RenderAbstractViewport.maybeOf], which is similar to this method, but
/// returns null if no [RenderAbstractViewport] ancestor is found.
static RenderAbstractViewport of(RenderObject? object) {
final RenderAbstractViewport? viewport = maybeOf(object);
assert(() {
if (viewport == null) {
throw FlutterError(
'RenderAbstractViewport.of() was called with a render object that was '
'not a descendant of a RenderAbstractViewport.\n'
'No RenderAbstractViewport render object ancestor could be found starting '
'from the object that was passed to RenderAbstractViewport.of().\n'
'The render object where the viewport search started was:\n'
' $object',
);
}
return true;
}());
return viewport!;
}
/// Returns the offset that would be needed to reveal the `target`
/// [RenderObject].
///
/// This is used by [RenderViewportBase.showInViewport], which is
/// itself used by [RenderObject.showOnScreen] for
/// [RenderViewportBase], which is in turn used by the semantics
/// system to implement scrolling for accessibility tools.
///
/// The optional `rect` parameter describes which area of that `target` object
/// should be revealed in the viewport. If `rect` is null, the entire
/// `target` [RenderObject] (as defined by its [RenderObject.paintBounds])
/// will be revealed. If `rect` is provided it has to be given in the
/// coordinate system of the `target` object.
///
/// The `alignment` argument describes where the target should be positioned
/// after applying the returned offset. If `alignment` is 0.0, the child must
/// be positioned as close to the leading edge of the viewport as possible. If
/// `alignment` is 1.0, the child must be positioned as close to the trailing
/// edge of the viewport as possible. If `alignment` is 0.5, the child must be
/// positioned as close to the center of the viewport as possible.
///
/// The `target` might not be a direct child of this viewport but it must be a
/// descendant of the viewport. Other viewports in between this viewport and
/// the `target` will not be adjusted.
///
/// This method assumes that the content of the viewport moves linearly, i.e.
/// when the offset of the viewport is changed by x then `target` also moves
/// by x within the viewport.
///
/// The optional [Axis] is used by
/// [RenderTwoDimensionalViewport.getOffsetToReveal] to
/// determine which of the two axes to compute an offset for. One dimensional
/// subclasses like [RenderViewportBase] and [RenderListWheelViewport]
/// will ignore the `axis` value if provided, since there is only one [Axis].
///
/// If the `axis` is omitted when called on [RenderTwoDimensionalViewport],
/// the [RenderTwoDimensionalViewport.mainAxis] is used. To reveal an object
/// properly in both axes, this method should be called for each [Axis] as the
/// returned [RevealedOffset.offset] only represents the offset of one of the
/// the two [ScrollPosition]s.
///
/// See also:
///
/// * [RevealedOffset], which describes the return value of this method.
RevealedOffset getOffsetToReveal(
RenderObject target,
double alignment, {
Rect? rect,
Axis? axis,
});
/// The default value for the cache extent of the viewport.
///
/// This default assumes [CacheExtentStyle.pixel].
///
/// See also:
///
/// * [RenderViewportBase.cacheExtent] for a definition of the cache extent.
static const double defaultCacheExtent = 250.0;
}
/// Return value for [RenderAbstractViewport.getOffsetToReveal].
///
/// It indicates the [offset] required to reveal an element in a viewport and
/// the [rect] position said element would have in the viewport at that
/// [offset].
class RevealedOffset {
/// Instantiates a return value for [RenderAbstractViewport.getOffsetToReveal].
const RevealedOffset({
required this.offset,
required this.rect,
});
/// Offset for the viewport to reveal a specific element in the viewport.
///
/// See also:
///
/// * [RenderAbstractViewport.getOffsetToReveal], which calculates this
/// value for a specific element.
final double offset;
/// The [Rect] in the outer coordinate system of the viewport at which the
/// to-be-revealed element would be located if the viewport's offset is set
/// to [offset].
///
/// A viewport usually has two coordinate systems and works as an adapter
/// between the two:
///
/// The inner coordinate system has its origin at the top left corner of the
/// content that moves inside the viewport. The origin of this coordinate
/// system usually moves around relative to the leading edge of the viewport
/// when the viewport offset changes.
///
/// The outer coordinate system has its origin at the top left corner of the
/// visible part of the viewport. This origin stays at the same position
/// regardless of the current viewport offset.
///
/// In other words: [rect] describes where the revealed element would be
/// located relative to the top left corner of the visible part of the
/// viewport if the viewport's offset is set to [offset].
///
/// See also:
///
/// * [RenderAbstractViewport.getOffsetToReveal], which calculates this
/// value for a specific element.
final Rect rect;
/// Determines which provided leading or trailing edge of the viewport, as
/// [RevealedOffset]s, will be used for [RenderViewportBase.showInViewport]
/// accounting for the size and already visible portion of the [RenderObject]
/// that is being revealed.
///
/// Also used by [RenderTwoDimensionalViewport.showInViewport] for each
/// horizontal and vertical [Axis].
///
/// If the target [RenderObject] is already fully visible, this will return
/// null.
static RevealedOffset? clampOffset({
required RevealedOffset leadingEdgeOffset,
required RevealedOffset trailingEdgeOffset,
required double currentOffset,
}) {
// scrollOffset
// 0 +---------+
// | |
// _ | |
// viewport position | | |
// with `descendant` at | | | _
// trailing edge |_ | xxxxxxx | | viewport position
// | | | with `descendant` at
// | | _| leading edge
// | |
// 800 +---------+
//
// `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the
// viewport on the left in image above.
// `leadingEdgeOffset`: Distance from scrollOffset 0 to the start of the
// viewport on the right in image above.
//
// The viewport position on the left is achieved by setting `offset.pixels`
// to `trailingEdgeOffset`, the one on the right by setting it to
// `leadingEdgeOffset`.
final bool inverted = leadingEdgeOffset.offset < trailingEdgeOffset.offset;
final RevealedOffset smaller;
final RevealedOffset larger;
(smaller, larger) = inverted
? (leadingEdgeOffset, trailingEdgeOffset)
: (trailingEdgeOffset, leadingEdgeOffset);
if (currentOffset > larger.offset) {
return larger;
} else if (currentOffset < smaller.offset) {
return smaller;
} else {
return null;
}
}
@override
String toString() {
return '${objectRuntimeType(this, 'RevealedOffset')}(offset: $offset, rect: $rect)';
}
}
/// A base class for render objects that are bigger on the inside.
///
/// This render object provides the shared code for render objects that host
/// [RenderSliver] render objects inside a [RenderBox]. The viewport establishes
/// an [axisDirection], which orients the sliver's coordinate system, which is
/// based on scroll offsets rather than Cartesian coordinates.
///
/// The viewport also listens to an [offset], which determines the
/// [SliverConstraints.scrollOffset] input to the sliver layout protocol.
///
/// Subclasses typically override [performLayout] and call
/// [layoutChildSequence], perhaps multiple times.
///
/// See also:
///
/// * [RenderSliver], which explains more about the Sliver protocol.
/// * [RenderBox], which explains more about the Box protocol.
/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
/// placed inside a [RenderSliver] (the opposite of this class).
abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMixin<RenderSliver>>
extends RenderBox with ContainerRenderObjectMixin<RenderSliver, ParentDataClass>
implements RenderAbstractViewport {
/// Initializes fields for subclasses.
///
/// The [cacheExtent], if null, defaults to [RenderAbstractViewport.defaultCacheExtent].
///
/// The [cacheExtent] must be specified if [cacheExtentStyle] is not [CacheExtentStyle.pixel].
RenderViewportBase({
AxisDirection axisDirection = AxisDirection.down,
required AxisDirection crossAxisDirection,
required ViewportOffset offset,
double? cacheExtent,
CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
Clip clipBehavior = Clip.hardEdge,
}) : assert(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)),
assert(cacheExtent != null || cacheExtentStyle == CacheExtentStyle.pixel),
_axisDirection = axisDirection,
_crossAxisDirection = crossAxisDirection,
_offset = offset,
_cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent,
_cacheExtentStyle = cacheExtentStyle,
_clipBehavior = clipBehavior;
/// Report the semantics of this node, for example for accessibility purposes.
///
/// [RenderViewportBase] adds [RenderViewport.useTwoPaneSemantics] to the
/// provided [SemanticsConfiguration] to support children using
/// [RenderViewport.excludeFromScrolling].
///
/// This method should be overridden by subclasses that have interesting
/// semantic information. Overriding subclasses should call
/// `super.describeSemanticsConfiguration(config)` to ensure
/// [RenderViewport.useTwoPaneSemantics] is still added to `config`.
///
/// See also:
///
/// * [RenderObject.describeSemanticsConfiguration], for important
/// details about not mutating a [SemanticsConfiguration] out of context.
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.addTagForChildren(RenderViewport.useTwoPaneSemantics);
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
childrenInPaintOrder
.where((RenderSliver sliver) => sliver.geometry!.visible || sliver.geometry!.cacheExtent > 0.0)
.forEach(visitor);
}
/// The direction in which the [SliverConstraints.scrollOffset] increases.
///
/// For example, if the [axisDirection] is [AxisDirection.down], a scroll
/// offset of zero is at the top of the viewport and increases towards the
/// bottom of the viewport.
AxisDirection get axisDirection => _axisDirection;
AxisDirection _axisDirection;
set axisDirection(AxisDirection value) {
if (value == _axisDirection) {
return;
}
_axisDirection = value;
markNeedsLayout();
}
/// The direction in which child should be laid out in the cross axis.
///
/// For example, if the [axisDirection] is [AxisDirection.down], this property
/// is typically [AxisDirection.left] if the ambient [TextDirection] is
/// [TextDirection.rtl] and [AxisDirection.right] if the ambient
/// [TextDirection] is [TextDirection.ltr].
AxisDirection get crossAxisDirection => _crossAxisDirection;
AxisDirection _crossAxisDirection;
set crossAxisDirection(AxisDirection value) {
if (value == _crossAxisDirection) {
return;
}
_crossAxisDirection = value;
markNeedsLayout();
}
/// The axis along which the viewport scrolls.
///
/// For example, if the [axisDirection] is [AxisDirection.down], then the
/// [axis] is [Axis.vertical] and the viewport scrolls vertically.
Axis get axis => axisDirectionToAxis(axisDirection);
/// Which part of the content inside the viewport should be visible.
///
/// The [ViewportOffset.pixels] value determines the scroll offset that the
/// viewport uses to select which part of its content to display. As the user
/// scrolls the viewport, this value changes, which changes the content that
/// is displayed.
ViewportOffset get offset => _offset;
ViewportOffset _offset;
set offset(ViewportOffset value) {
if (value == _offset) {
return;
}
if (attached) {
_offset.removeListener(markNeedsLayout);
}
_offset = value;
if (attached) {
_offset.addListener(markNeedsLayout);
}
// We need to go through layout even if the new offset has the same pixels
// value as the old offset so that we will apply our viewport and content
// dimensions.
markNeedsLayout();
}
// TODO(ianh): cacheExtent/cacheExtentStyle should be a single
// object that specifies both the scalar value and the unit, not a
// pair of independent setters. Changing that would allow a more
// rational API and would let us make the getter non-nullable.
/// {@template flutter.rendering.RenderViewportBase.cacheExtent}
/// The viewport has an area before and after the visible area to cache items
/// that are about to become visible when the user scrolls.
///
/// Items that fall in this cache area are laid out even though they are not
/// (yet) visible on screen. The [cacheExtent] describes how many pixels
/// the cache area extends before the leading edge and after the trailing edge
/// of the viewport.
///
/// The total extent, which the viewport will try to cover with children, is
/// [cacheExtent] before the leading edge + extent of the main axis +
/// [cacheExtent] after the trailing edge.
///
/// The cache area is also used to implement implicit accessibility scrolling
/// on iOS: When the accessibility focus moves from an item in the visible
/// viewport to an invisible item in the cache area, the framework will bring
/// that item into view with an (implicit) scroll action.
/// {@endtemplate}
///
/// The getter can never return null, but the field is nullable
/// because the setter can be set to null to reset the value to
/// [RenderAbstractViewport.defaultCacheExtent] (in which case
/// [cacheExtentStyle] must be [CacheExtentStyle.pixel]).
///
/// See also:
///
/// * [cacheExtentStyle], which controls the units of the [cacheExtent].
double? get cacheExtent => _cacheExtent;
double _cacheExtent;
set cacheExtent(double? value) {
value ??= RenderAbstractViewport.defaultCacheExtent;
if (value == _cacheExtent) {
return;
}
_cacheExtent = value;
markNeedsLayout();
}
/// This value is set during layout based on the [CacheExtentStyle].
///
/// When the style is [CacheExtentStyle.viewport], it is the main axis extent
/// of the viewport multiplied by the requested cache extent, which is still
/// expressed in pixels.
double? _calculatedCacheExtent;
/// {@template flutter.rendering.RenderViewportBase.cacheExtentStyle}
/// Controls how the [cacheExtent] is interpreted.
///
/// If set to [CacheExtentStyle.pixel], the [cacheExtent] will be
/// treated as a logical pixels, and the default [cacheExtent] is
/// [RenderAbstractViewport.defaultCacheExtent].
///
/// If set to [CacheExtentStyle.viewport], the [cacheExtent] will be
/// treated as a multiplier for the main axis extent of the
/// viewport. In this case there is no default [cacheExtent]; it
/// must be explicitly specified.
/// {@endtemplate}
///
/// Changing the [cacheExtentStyle] without also changing the [cacheExtent]
/// is rarely the correct choice.
CacheExtentStyle get cacheExtentStyle => _cacheExtentStyle;
CacheExtentStyle _cacheExtentStyle;
set cacheExtentStyle(CacheExtentStyle value) {
if (value == _cacheExtentStyle) {
return;
}
_cacheExtentStyle = value;
markNeedsLayout();
}
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(markNeedsLayout);
}
@override
void detach() {
_offset.removeListener(markNeedsLayout);
super.detach();
}
/// Throws an exception saying that the object does not support returning
/// intrinsic dimensions if, in debug mode, we are not in the
/// [RenderObject.debugCheckingIntrinsics] mode.
///
/// This is used by [computeMinIntrinsicWidth] et al because viewports do not
/// generally support returning intrinsic dimensions. See the discussion at
/// [computeMinIntrinsicWidth].
@protected
bool debugThrowIfNotCheckingIntrinsics() {
assert(() {
if (!RenderObject.debugCheckingIntrinsics) {
assert(this is! RenderShrinkWrappingViewport); // it has its own message
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('$runtimeType does not support returning intrinsic dimensions.'),
ErrorDescription(
'Calculating the intrinsic dimensions would require instantiating every child of '
'the viewport, which defeats the point of viewports being lazy.',
),
ErrorHint(
'If you are merely trying to shrink-wrap the viewport in the main axis direction, '
'consider a RenderShrinkWrappingViewport render object (ShrinkWrappingViewport widget), '
'which achieves that effect without implementing the intrinsic dimension API.',
),
]);
}
return true;
}());
return true;
}
@override
double computeMinIntrinsicWidth(double height) {
assert(debugThrowIfNotCheckingIntrinsics());
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
assert(debugThrowIfNotCheckingIntrinsics());
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
assert(debugThrowIfNotCheckingIntrinsics());
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
assert(debugThrowIfNotCheckingIntrinsics());
return 0.0;
}
@override
bool get isRepaintBoundary => true;
/// Determines the size and position of some of the children of the viewport.
///
/// This function is the workhorse of `performLayout` implementations in
/// subclasses.
///
/// Layout starts with `child`, proceeds according to the `advance` callback,
/// and stops once `advance` returns null.
///
/// * `scrollOffset` is the [SliverConstraints.scrollOffset] to pass the
/// first child. The scroll offset is adjusted by
/// [SliverGeometry.scrollExtent] for subsequent children.
/// * `overlap` is the [SliverConstraints.overlap] to pass the first child.
/// The overlay is adjusted by the [SliverGeometry.paintOrigin] and
/// [SliverGeometry.paintExtent] for subsequent children.
/// * `layoutOffset` is the layout offset at which to place the first child.
/// The layout offset is updated by the [SliverGeometry.layoutExtent] for
/// subsequent children.
/// * `remainingPaintExtent` is [SliverConstraints.remainingPaintExtent] to
/// pass the first child. The remaining paint extent is updated by the
/// [SliverGeometry.layoutExtent] for subsequent children.
/// * `mainAxisExtent` is the [SliverConstraints.viewportMainAxisExtent] to
/// pass to each child.
/// * `crossAxisExtent` is the [SliverConstraints.crossAxisExtent] to pass to
/// each child.
/// * `growthDirection` is the [SliverConstraints.growthDirection] to pass to
/// each child.
///
/// Returns the first non-zero [SliverGeometry.scrollOffsetCorrection]
/// encountered, if any. Otherwise returns 0.0. Typical callers will call this
/// function repeatedly until it returns 0.0.
@protected
double layoutChildSequence({
required RenderSliver? child,
required double scrollOffset,
required double overlap,
required double layoutOffset,
required double remainingPaintExtent,
required double mainAxisExtent,
required double crossAxisExtent,
required GrowthDirection growthDirection,
required RenderSliver? Function(RenderSliver child) advance,
required double remainingCacheExtent,
required double cacheOrigin,
}) {
assert(scrollOffset.isFinite);
assert(scrollOffset >= 0.0);
final double initialLayoutOffset = layoutOffset;
final ScrollDirection adjustedUserScrollDirection =
applyGrowthDirectionToScrollDirection(offset.userScrollDirection, growthDirection);
double maxPaintOffset = layoutOffset + overlap;
double precedingScrollExtent = 0.0;
while (child != null) {
final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;
// If the scrollOffset is too small we adjust the paddedOrigin because it
// doesn't make sense to ask a sliver for content before its scroll
// offset.
final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);
final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;
assert(sliverScrollOffset >= correctedCacheOrigin.abs());
assert(correctedCacheOrigin <= 0.0);
assert(sliverScrollOffset >= 0.0);
assert(cacheExtentCorrection <= 0.0);
child.layout(SliverConstraints(
axisDirection: axisDirection,
growthDirection: growthDirection,
userScrollDirection: adjustedUserScrollDirection,
scrollOffset: sliverScrollOffset,
precedingScrollExtent: precedingScrollExtent,
overlap: maxPaintOffset - layoutOffset,
remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
crossAxisExtent: crossAxisExtent,
crossAxisDirection: crossAxisDirection,
viewportMainAxisExtent: mainAxisExtent,
remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
cacheOrigin: correctedCacheOrigin,
), parentUsesSize: true);
final SliverGeometry childLayoutGeometry = child.geometry!;
assert(childLayoutGeometry.debugAssertIsValid());
// If there is a correction to apply, we'll have to start over.
if (childLayoutGeometry.scrollOffsetCorrection != null) {
return childLayoutGeometry.scrollOffsetCorrection!;
}
// We use the child's paint origin in our coordinate system as the
// layoutOffset we store in the child's parent data.
final double effectiveLayoutOffset = layoutOffset + childLayoutGeometry.paintOrigin;
// `effectiveLayoutOffset` becomes meaningless once we moved past the trailing edge
// because `childLayoutGeometry.layoutExtent` is zero. Using the still increasing
// 'scrollOffset` to roughly position these invisible slivers in the right order.
if (childLayoutGeometry.visible || scrollOffset > 0) {
updateChildLayoutOffset(child, effectiveLayoutOffset, growthDirection);
} else {
updateChildLayoutOffset(child, -scrollOffset + initialLayoutOffset, growthDirection);
}
maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
scrollOffset -= childLayoutGeometry.scrollExtent;
precedingScrollExtent += childLayoutGeometry.scrollExtent;
layoutOffset += childLayoutGeometry.layoutExtent;
if (childLayoutGeometry.cacheExtent != 0.0) {
remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
}
updateOutOfBandData(growthDirection, childLayoutGeometry);
// move on to the next child
child = advance(child);
}
// we made it without a correction, whee!
return 0.0;
}
@override
Rect? describeApproximatePaintClip(RenderSliver child) {
switch (clipBehavior) {
case Clip.none:
return null;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
break;
}
final Rect viewportClip = Offset.zero & size;
// The child's viewportMainAxisExtent can be infinite when a
// RenderShrinkWrappingViewport is given infinite constraints, such as when
// it is the child of a Row or Column (depending on orientation).
//
// For example, a shrink wrapping render sliver may have infinite
// constraints along the viewport's main axis but may also have bouncing
// scroll physics, which will allow for some scrolling effect to occur.
// We should just use the viewportClip - the start of the overlap is at
// double.infinity and so it is effectively meaningless.
if (child.constraints.overlap == 0 || !child.constraints.viewportMainAxisExtent.isFinite) {
return viewportClip;
}
// Adjust the clip rect for this sliver by the overlap from the previous sliver.
double left = viewportClip.left;
double right = viewportClip.right;
double top = viewportClip.top;
double bottom = viewportClip.bottom;
final double startOfOverlap = child.constraints.viewportMainAxisExtent - child.constraints.remainingPaintExtent;
final double overlapCorrection = startOfOverlap + child.constraints.overlap;
switch (applyGrowthDirectionToAxisDirection(axisDirection, child.constraints.growthDirection)) {
case AxisDirection.down:
top += overlapCorrection;
case AxisDirection.up:
bottom -= overlapCorrection;
case AxisDirection.right:
left += overlapCorrection;
case AxisDirection.left:
right -= overlapCorrection;
}
return Rect.fromLTRB(left, top, right, bottom);
}
@override
Rect describeSemanticsClip(RenderSliver? child) {
if (_calculatedCacheExtent == null) {
return semanticBounds;
}
switch (axis) {
case Axis.vertical:
return Rect.fromLTRB(
semanticBounds.left,
semanticBounds.top - _calculatedCacheExtent!,
semanticBounds.right,
semanticBounds.bottom + _calculatedCacheExtent!,
);
case Axis.horizontal:
return Rect.fromLTRB(
semanticBounds.left - _calculatedCacheExtent!,
semanticBounds.top,
semanticBounds.right + _calculatedCacheExtent!,
semanticBounds.bottom,
);
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null) {
return;
}
if (hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintContents,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
_paintContents(context, offset);
}
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
void _paintContents(PaintingContext context, Offset offset) {
for (final RenderSliver child in childrenInPaintOrder) {
if (child.geometry!.visible) {
context.paintChild(child, offset + paintOffsetOf(child));
}
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
super.debugPaintSize(context, offset);
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color(0xFF00FF00);
final Canvas canvas = context.canvas;
RenderSliver? child = firstChild;
while (child != null) {
final Size size = switch (axis) {
Axis.vertical => Size(child.constraints.crossAxisExtent, child.geometry!.layoutExtent),
Axis.horizontal => Size(child.geometry!.layoutExtent, child.constraints.crossAxisExtent),
};
canvas.drawRect(((offset + paintOffsetOf(child)) & size).deflate(0.5), paint);
child = childAfter(child);
}
return true;
}());
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
final (double mainAxisPosition, double crossAxisPosition) = switch (axis) {
Axis.vertical => (position.dy, position.dx),
Axis.horizontal => (position.dx, position.dy),
};
final SliverHitTestResult sliverResult = SliverHitTestResult.wrap(result);
for (final RenderSliver child in childrenInHitTestOrder) {
if (!child.geometry!.visible) {
continue;
}
final Matrix4 transform = Matrix4.identity();
applyPaintTransform(child, transform); // must be invertible
final bool isHit = result.addWithOutOfBandPosition(
paintTransform: transform,
hitTest: (BoxHitTestResult result) {
return child.hitTest(
sliverResult,
mainAxisPosition: computeChildMainAxisPosition(child, mainAxisPosition),
crossAxisPosition: crossAxisPosition,
);
},
);
if (isHit) {
return true;
}
}
return false;
}
@override
RevealedOffset getOffsetToReveal(
RenderObject target,
double alignment, {
Rect? rect,
Axis? axis,
}) {
// One dimensional viewport has only one axis, override if it was
// provided/may be mismatched.
axis = this.axis;
// Steps to convert `rect` (from a RenderBox coordinate system) to its
// scroll offset within this viewport (not in the exact order):
//
// 1. Pick the outermost RenderBox (between which, and the viewport, there
// is nothing but RenderSlivers) as an intermediate reference frame
// (the `pivot`), convert `rect` to that coordinate space.
//
// 2. Convert `rect` from the `pivot` coordinate space to its sliver
// parent's sliver coordinate system (i.e., to a scroll offset), based on
// the axis direction and growth direction of the parent.
//
// 3. Convert the scroll offset to its sliver parent's coordinate space
// using `childScrollOffset`, until we reach the viewport.
//
// 4. Make the final conversion from the outmost sliver to the viewport
// using `scrollOffsetOf`.
double leadingScrollOffset = 0.0;
// Starting at `target` and walking towards the root:
// - `child` will be the last object before we reach this viewport, and
// - `pivot` will be the last RenderBox before we reach this viewport.
RenderObject child = target;
RenderBox? pivot;
bool onlySlivers = target is RenderSliver; // ... between viewport and `target` (`target` included).
while (child.parent != this) {
final RenderObject parent = child.parent!;
if (child is RenderBox) {
pivot = child;
}
if (parent is RenderSliver) {
leadingScrollOffset += parent.childScrollOffset(child)!;
} else {
onlySlivers = false;
leadingScrollOffset = 0.0;
}
child = parent;
}
// `rect` in the new intermediate coordinate system.
final Rect rectLocal;
// Our new reference frame render object's main axis extent.
final double pivotExtent;
final GrowthDirection growthDirection;
// `leadingScrollOffset` is currently the scrollOffset of our new reference
// frame (`pivot` or `target`), within `child`.
if (pivot != null) {
assert(pivot.parent != null);
assert(pivot.parent != this);
assert(pivot != this);
assert(pivot.parent is RenderSliver); // TODO(abarth): Support other kinds of render objects besides slivers.
final RenderSliver pivotParent = pivot.parent! as RenderSliver;
growthDirection = pivotParent.constraints.growthDirection;
pivotExtent = switch (axis) {
Axis.horizontal => pivot.size.width,
Axis.vertical => pivot.size.height,
};
rect ??= target.paintBounds;
rectLocal = MatrixUtils.transformRect(target.getTransformTo(pivot), rect);
} else if (onlySlivers) {
// `pivot` does not exist. We'll have to make up one from `target`, the
// innermost sliver.
final RenderSliver targetSliver = target as RenderSliver;
growthDirection = targetSliver.constraints.growthDirection;
// TODO(LongCatIsLooong): make sure this works if `targetSliver` is a
// persistent header, when #56413 relands.
pivotExtent = targetSliver.geometry!.scrollExtent;
if (rect == null) {
switch (axis) {
case Axis.horizontal:
rect = Rect.fromLTWH(
0, 0,
targetSliver.geometry!.scrollExtent,
targetSliver.constraints.crossAxisExtent,
);
case Axis.vertical:
rect = Rect.fromLTWH(
0, 0,
targetSliver.constraints.crossAxisExtent,
targetSliver.geometry!.scrollExtent,
);
}
}
rectLocal = rect;
} else {
assert(rect != null);
return RevealedOffset(offset: offset.pixels, rect: rect!);
}
assert(child.parent == this);
assert(child is RenderSliver);
final RenderSliver sliver = child as RenderSliver;
// The scroll offset of `rect` within `child`.
leadingScrollOffset += switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
AxisDirection.up => pivotExtent - rectLocal.bottom,
AxisDirection.left => pivotExtent - rectLocal.right,
AxisDirection.right => rectLocal.left,
AxisDirection.down => rectLocal.top,
};
// So far leadingScrollOffset is the scroll offset of `rect` in the `child`
// sliver's sliver coordinate system. The sign of this value indicates
// whether the `rect` protrudes the leading edge of the `child` sliver. When
// this value is non-negative and `child`'s `maxScrollObstructionExtent` is
// greater than 0, we assume `rect` can't be obstructed by the leading edge
// of the viewport (i.e. its pinned to the leading edge).
final bool isPinned = sliver.geometry!.maxScrollObstructionExtent > 0 && leadingScrollOffset >= 0;
// The scroll offset in the viewport to `rect`.
leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
// This step assumes the viewport's layout is up-to-date, i.e., if
// offset.pixels is changed after the last performLayout, the new scroll
// position will not be accounted for.
final Matrix4 transform = target.getTransformTo(this);
Rect targetRect = MatrixUtils.transformRect(transform, rect);
final double extentOfPinnedSlivers = maxScrollObstructionExtentBefore(sliver);
switch (sliver.constraints.growthDirection) {
case GrowthDirection.forward:
if (isPinned && alignment <= 0) {
return RevealedOffset(offset: double.infinity, rect: targetRect);
}
leadingScrollOffset -= extentOfPinnedSlivers;
case GrowthDirection.reverse:
if (isPinned && alignment >= 1) {
return RevealedOffset(offset: double.negativeInfinity, rect: targetRect);
}
// If child's growth direction is reverse, when viewport.offset is
// `leadingScrollOffset`, it is positioned just outside of the leading
// edge of the viewport.
leadingScrollOffset -= switch (axis) {
Axis.vertical => targetRect.height,
Axis.horizontal => targetRect.width,
};
}
final double mainAxisExtentDifference = switch (axis) {
Axis.horizontal => size.width - extentOfPinnedSlivers - rectLocal.width,
Axis.vertical => size.height - extentOfPinnedSlivers - rectLocal.height,
};
final double targetOffset = leadingScrollOffset - mainAxisExtentDifference * alignment;
final double offsetDifference = offset.pixels - targetOffset;
targetRect = switch (axisDirection) {
AxisDirection.up => targetRect.translate(0.0, -offsetDifference),
AxisDirection.down => targetRect.translate(0.0, offsetDifference),
AxisDirection.left => targetRect.translate(-offsetDifference, 0.0),
AxisDirection.right => targetRect.translate( offsetDifference, 0.0),
};
return RevealedOffset(offset: targetOffset, rect: targetRect);
}
/// The offset at which the given `child` should be painted.
///
/// The returned offset is from the top left corner of the inside of the
/// viewport to the top left corner of the paint coordinate system of the
/// `child`.
///
/// See also:
///
/// * [paintOffsetOf], which uses the layout offset and growth direction
/// computed for the child during layout.
@protected
Offset computeAbsolutePaintOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
assert(hasSize); // this is only usable once we have a size
assert(child.geometry != null);
return switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
AxisDirection.up => Offset(0.0, size.height - layoutOffset - child.geometry!.paintExtent),
AxisDirection.left => Offset(size.width - layoutOffset - child.geometry!.paintExtent, 0.0),
AxisDirection.right => Offset(layoutOffset, 0.0),
AxisDirection.down => Offset(0.0, layoutOffset),
};