1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6
7import 'package:flutter/animation.dart';
8import 'package:flutter/foundation.dart';
9import 'package:flutter/semantics.dart';
10
11import 'box.dart';
12import 'debug.dart';
13import 'layer.dart';
14import 'object.dart';
15import 'sliver.dart';
16import 'viewport_offset.dart';
17
18/// The unit of measurement for a [Viewport.cacheExtent].
19enum CacheExtentStyle {
20 /// Treat the [Viewport.cacheExtent] as logical pixels.
21 pixel,
22 /// Treat the [Viewport.cacheExtent] as a multiplier of the main axis extent.
23 viewport,
24}
25
26/// An interface for render objects that are bigger on the inside.
27///
28/// Some render objects, such as [RenderViewport], present a portion of their
29/// content, which can be controlled by a [ViewportOffset]. This interface lets
30/// the framework recognize such render objects and interact with them without
31/// having specific knowledge of all the various types of viewports.
32abstract interface class RenderAbstractViewport extends RenderObject {
33 /// Returns the [RenderAbstractViewport] that most tightly encloses the given
34 /// render object.
35 ///
36 /// If the object does not have a [RenderAbstractViewport] as an ancestor,
37 /// this function returns null.
38 ///
39 /// See also:
40 ///
41 /// * [RenderAbstractViewport.of], which is similar to this method, but
42 /// asserts if no [RenderAbstractViewport] ancestor is found.
43 static RenderAbstractViewport? maybeOf(RenderObject? object) {
44 while (object != null) {
45 if (object is RenderAbstractViewport) {
46 return object;
47 }
48 object = object.parent;
49 }
50 return null;
51 }
52
53 /// Returns the [RenderAbstractViewport] that most tightly encloses the given
54 /// render object.
55 ///
56 /// If the object does not have a [RenderAbstractViewport] as an ancestor,
57 /// this function will assert in debug mode, and throw an exception in release
58 /// mode.
59 ///
60 /// See also:
61 ///
62 /// * [RenderAbstractViewport.maybeOf], which is similar to this method, but
63 /// returns null if no [RenderAbstractViewport] ancestor is found.
64 static RenderAbstractViewport of(RenderObject? object) {
65 final RenderAbstractViewport? viewport = maybeOf(object);
66 assert(() {
67 if (viewport == null) {
68 throw FlutterError(
69 'RenderAbstractViewport.of() was called with a render object that was '
70 'not a descendant of a RenderAbstractViewport.\n'
71 'No RenderAbstractViewport render object ancestor could be found starting '
72 'from the object that was passed to RenderAbstractViewport.of().\n'
73 'The render object where the viewport search started was:\n'
74 ' $object',
75 );
76 }
77 return true;
78 }());
79 return viewport!;
80 }
81
82 /// Returns the offset that would be needed to reveal the `target`
83 /// [RenderObject].
84 ///
85 /// This is used by [RenderViewportBase.showInViewport], which is
86 /// itself used by [RenderObject.showOnScreen] for
87 /// [RenderViewportBase], which is in turn used by the semantics
88 /// system to implement scrolling for accessibility tools.
89 ///
90 /// The optional `rect` parameter describes which area of that `target` object
91 /// should be revealed in the viewport. If `rect` is null, the entire
92 /// `target` [RenderObject] (as defined by its [RenderObject.paintBounds])
93 /// will be revealed. If `rect` is provided it has to be given in the
94 /// coordinate system of the `target` object.
95 ///
96 /// The `alignment` argument describes where the target should be positioned
97 /// after applying the returned offset. If `alignment` is 0.0, the child must
98 /// be positioned as close to the leading edge of the viewport as possible. If
99 /// `alignment` is 1.0, the child must be positioned as close to the trailing
100 /// edge of the viewport as possible. If `alignment` is 0.5, the child must be
101 /// positioned as close to the center of the viewport as possible.
102 ///
103 /// The `target` might not be a direct child of this viewport but it must be a
104 /// descendant of the viewport. Other viewports in between this viewport and
105 /// the `target` will not be adjusted.
106 ///
107 /// This method assumes that the content of the viewport moves linearly, i.e.
108 /// when the offset of the viewport is changed by x then `target` also moves
109 /// by x within the viewport.
110 ///
111 /// The optional [Axis] is used by
112 /// [RenderTwoDimensionalViewport.getOffsetToReveal] to
113 /// determine which of the two axes to compute an offset for. One dimensional
114 /// subclasses like [RenderViewportBase] and [RenderListWheelViewport]
115 /// will ignore the `axis` value if provided, since there is only one [Axis].
116 ///
117 /// If the `axis` is omitted when called on [RenderTwoDimensionalViewport],
118 /// the [RenderTwoDimensionalViewport.mainAxis] is used. To reveal an object
119 /// properly in both axes, this method should be called for each [Axis] as the
120 /// returned [RevealedOffset.offset] only represents the offset of one of the
121 /// the two [ScrollPosition]s.
122 ///
123 /// See also:
124 ///
125 /// * [RevealedOffset], which describes the return value of this method.
126 RevealedOffset getOffsetToReveal(
127 RenderObject target,
128 double alignment, {
129 Rect? rect,
130 Axis? axis,
131 });
132
133 /// The default value for the cache extent of the viewport.
134 ///
135 /// This default assumes [CacheExtentStyle.pixel].
136 ///
137 /// See also:
138 ///
139 /// * [RenderViewportBase.cacheExtent] for a definition of the cache extent.
140 static const double defaultCacheExtent = 250.0;
141}
142
143/// Return value for [RenderAbstractViewport.getOffsetToReveal].
144///
145/// It indicates the [offset] required to reveal an element in a viewport and
146/// the [rect] position said element would have in the viewport at that
147/// [offset].
148class RevealedOffset {
149 /// Instantiates a return value for [RenderAbstractViewport.getOffsetToReveal].
150 const RevealedOffset({
151 required this.offset,
152 required this.rect,
153 });
154
155 /// Offset for the viewport to reveal a specific element in the viewport.
156 ///
157 /// See also:
158 ///
159 /// * [RenderAbstractViewport.getOffsetToReveal], which calculates this
160 /// value for a specific element.
161 final double offset;
162
163 /// The [Rect] in the outer coordinate system of the viewport at which the
164 /// to-be-revealed element would be located if the viewport's offset is set
165 /// to [offset].
166 ///
167 /// A viewport usually has two coordinate systems and works as an adapter
168 /// between the two:
169 ///
170 /// The inner coordinate system has its origin at the top left corner of the
171 /// content that moves inside the viewport. The origin of this coordinate
172 /// system usually moves around relative to the leading edge of the viewport
173 /// when the viewport offset changes.
174 ///
175 /// The outer coordinate system has its origin at the top left corner of the
176 /// visible part of the viewport. This origin stays at the same position
177 /// regardless of the current viewport offset.
178 ///
179 /// In other words: [rect] describes where the revealed element would be
180 /// located relative to the top left corner of the visible part of the
181 /// viewport if the viewport's offset is set to [offset].
182 ///
183 /// See also:
184 ///
185 /// * [RenderAbstractViewport.getOffsetToReveal], which calculates this
186 /// value for a specific element.
187 final Rect rect;
188
189 /// Determines which provided leading or trailing edge of the viewport, as
190 /// [RevealedOffset]s, will be used for [RenderViewportBase.showInViewport]
191 /// accounting for the size and already visible portion of the [RenderObject]
192 /// that is being revealed.
193 ///
194 /// Also used by [RenderTwoDimensionalViewport.showInViewport] for each
195 /// horizontal and vertical [Axis].
196 ///
197 /// If the target [RenderObject] is already fully visible, this will return
198 /// null.
199 static RevealedOffset? clampOffset({
200 required RevealedOffset leadingEdgeOffset,
201 required RevealedOffset trailingEdgeOffset,
202 required double currentOffset,
203 }) {
204 // scrollOffset
205 // 0 +---------+
206 // | |
207 // _ | |
208 // viewport position | | |
209 // with `descendant` at | | | _
210 // trailing edge |_ | xxxxxxx | | viewport position
211 // | | | with `descendant` at
212 // | | _| leading edge
213 // | |
214 // 800 +---------+
215 //
216 // `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the
217 // viewport on the left in image above.
218 // `leadingEdgeOffset`: Distance from scrollOffset 0 to the start of the
219 // viewport on the right in image above.
220 //
221 // The viewport position on the left is achieved by setting `offset.pixels`
222 // to `trailingEdgeOffset`, the one on the right by setting it to
223 // `leadingEdgeOffset`.
224 final bool inverted = leadingEdgeOffset.offset < trailingEdgeOffset.offset;
225 final RevealedOffset smaller;
226 final RevealedOffset larger;
227 (smaller, larger) = inverted
228 ? (leadingEdgeOffset, trailingEdgeOffset)
229 : (trailingEdgeOffset, leadingEdgeOffset);
230 if (currentOffset > larger.offset) {
231 return larger;
232 } else if (currentOffset < smaller.offset) {
233 return smaller;
234 } else {
235 return null;
236 }
237 }
238
239 @override
240 String toString() {
241 return '${objectRuntimeType(this, 'RevealedOffset')}(offset: $offset, rect: $rect)';
242 }
243}
244
245/// A base class for render objects that are bigger on the inside.
246///
247/// This render object provides the shared code for render objects that host
248/// [RenderSliver] render objects inside a [RenderBox]. The viewport establishes
249/// an [axisDirection], which orients the sliver's coordinate system, which is
250/// based on scroll offsets rather than Cartesian coordinates.
251///
252/// The viewport also listens to an [offset], which determines the
253/// [SliverConstraints.scrollOffset] input to the sliver layout protocol.
254///
255/// Subclasses typically override [performLayout] and call
256/// [layoutChildSequence], perhaps multiple times.
257///
258/// See also:
259///
260/// * [RenderSliver], which explains more about the Sliver protocol.
261/// * [RenderBox], which explains more about the Box protocol.
262/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
263/// placed inside a [RenderSliver] (the opposite of this class).
264abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMixin<RenderSliver>>
265 extends RenderBox with ContainerRenderObjectMixin<RenderSliver, ParentDataClass>
266 implements RenderAbstractViewport {
267 /// Initializes fields for subclasses.
268 ///
269 /// The [cacheExtent], if null, defaults to [RenderAbstractViewport.defaultCacheExtent].
270 ///
271 /// The [cacheExtent] must be specified if [cacheExtentStyle] is not [CacheExtentStyle.pixel].
272 RenderViewportBase({
273 AxisDirection axisDirection = AxisDirection.down,
274 required AxisDirection crossAxisDirection,
275 required ViewportOffset offset,
276 double? cacheExtent,
277 CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
278 Clip clipBehavior = Clip.hardEdge,
279 }) : assert(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)),
280 assert(cacheExtent != null || cacheExtentStyle == CacheExtentStyle.pixel),
281 _axisDirection = axisDirection,
282 _crossAxisDirection = crossAxisDirection,
283 _offset = offset,
284 _cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent,
285 _cacheExtentStyle = cacheExtentStyle,
286 _clipBehavior = clipBehavior;
287
288 /// Report the semantics of this node, for example for accessibility purposes.
289 ///
290 /// [RenderViewportBase] adds [RenderViewport.useTwoPaneSemantics] to the
291 /// provided [SemanticsConfiguration] to support children using
292 /// [RenderViewport.excludeFromScrolling].
293 ///
294 /// This method should be overridden by subclasses that have interesting
295 /// semantic information. Overriding subclasses should call
296 /// `super.describeSemanticsConfiguration(config)` to ensure
297 /// [RenderViewport.useTwoPaneSemantics] is still added to `config`.
298 ///
299 /// See also:
300 ///
301 /// * [RenderObject.describeSemanticsConfiguration], for important
302 /// details about not mutating a [SemanticsConfiguration] out of context.
303 @override
304 void describeSemanticsConfiguration(SemanticsConfiguration config) {
305 super.describeSemanticsConfiguration(config);
306
307 config.addTagForChildren(RenderViewport.useTwoPaneSemantics);
308 }
309
310 @override
311 void visitChildrenForSemantics(RenderObjectVisitor visitor) {
312 childrenInPaintOrder
313 .where((RenderSliver sliver) => sliver.geometry!.visible || sliver.geometry!.cacheExtent > 0.0)
314 .forEach(visitor);
315 }
316
317 /// The direction in which the [SliverConstraints.scrollOffset] increases.
318 ///
319 /// For example, if the [axisDirection] is [AxisDirection.down], a scroll
320 /// offset of zero is at the top of the viewport and increases towards the
321 /// bottom of the viewport.
322 AxisDirection get axisDirection => _axisDirection;
323 AxisDirection _axisDirection;
324 set axisDirection(AxisDirection value) {
325 if (value == _axisDirection) {
326 return;
327 }
328 _axisDirection = value;
329 markNeedsLayout();
330 }
331
332 /// The direction in which child should be laid out in the cross axis.
333 ///
334 /// For example, if the [axisDirection] is [AxisDirection.down], this property
335 /// is typically [AxisDirection.left] if the ambient [TextDirection] is
336 /// [TextDirection.rtl] and [AxisDirection.right] if the ambient
337 /// [TextDirection] is [TextDirection.ltr].
338 AxisDirection get crossAxisDirection => _crossAxisDirection;
339 AxisDirection _crossAxisDirection;
340 set crossAxisDirection(AxisDirection value) {
341 if (value == _crossAxisDirection) {
342 return;
343 }
344 _crossAxisDirection = value;
345 markNeedsLayout();
346 }
347
348 /// The axis along which the viewport scrolls.
349 ///
350 /// For example, if the [axisDirection] is [AxisDirection.down], then the
351 /// [axis] is [Axis.vertical] and the viewport scrolls vertically.
352 Axis get axis => axisDirectionToAxis(axisDirection);
353
354 /// Which part of the content inside the viewport should be visible.
355 ///
356 /// The [ViewportOffset.pixels] value determines the scroll offset that the
357 /// viewport uses to select which part of its content to display. As the user
358 /// scrolls the viewport, this value changes, which changes the content that
359 /// is displayed.
360 ViewportOffset get offset => _offset;
361 ViewportOffset _offset;
362 set offset(ViewportOffset value) {
363 if (value == _offset) {
364 return;
365 }
366 if (attached) {
367 _offset.removeListener(markNeedsLayout);
368 }
369 _offset = value;
370 if (attached) {
371 _offset.addListener(markNeedsLayout);
372 }
373 // We need to go through layout even if the new offset has the same pixels
374 // value as the old offset so that we will apply our viewport and content
375 // dimensions.
376 markNeedsLayout();
377 }
378
379 // TODO(ianh): cacheExtent/cacheExtentStyle should be a single
380 // object that specifies both the scalar value and the unit, not a
381 // pair of independent setters. Changing that would allow a more
382 // rational API and would let us make the getter non-nullable.
383
384 /// {@template flutter.rendering.RenderViewportBase.cacheExtent}
385 /// The viewport has an area before and after the visible area to cache items
386 /// that are about to become visible when the user scrolls.
387 ///
388 /// Items that fall in this cache area are laid out even though they are not
389 /// (yet) visible on screen. The [cacheExtent] describes how many pixels
390 /// the cache area extends before the leading edge and after the trailing edge
391 /// of the viewport.
392 ///
393 /// The total extent, which the viewport will try to cover with children, is
394 /// [cacheExtent] before the leading edge + extent of the main axis +
395 /// [cacheExtent] after the trailing edge.
396 ///
397 /// The cache area is also used to implement implicit accessibility scrolling
398 /// on iOS: When the accessibility focus moves from an item in the visible
399 /// viewport to an invisible item in the cache area, the framework will bring
400 /// that item into view with an (implicit) scroll action.
401 /// {@endtemplate}
402 ///
403 /// The getter can never return null, but the field is nullable
404 /// because the setter can be set to null to reset the value to
405 /// [RenderAbstractViewport.defaultCacheExtent] (in which case
406 /// [cacheExtentStyle] must be [CacheExtentStyle.pixel]).
407 ///
408 /// See also:
409 ///
410 /// * [cacheExtentStyle], which controls the units of the [cacheExtent].
411 double? get cacheExtent => _cacheExtent;
412 double _cacheExtent;
413 set cacheExtent(double? value) {
414 value ??= RenderAbstractViewport.defaultCacheExtent;
415 if (value == _cacheExtent) {
416 return;
417 }
418 _cacheExtent = value;
419 markNeedsLayout();
420 }
421
422 /// This value is set during layout based on the [CacheExtentStyle].
423 ///
424 /// When the style is [CacheExtentStyle.viewport], it is the main axis extent
425 /// of the viewport multiplied by the requested cache extent, which is still
426 /// expressed in pixels.
427 double? _calculatedCacheExtent;
428
429 /// {@template flutter.rendering.RenderViewportBase.cacheExtentStyle}
430 /// Controls how the [cacheExtent] is interpreted.
431 ///
432 /// If set to [CacheExtentStyle.pixel], the [cacheExtent] will be
433 /// treated as a logical pixels, and the default [cacheExtent] is
434 /// [RenderAbstractViewport.defaultCacheExtent].
435 ///
436 /// If set to [CacheExtentStyle.viewport], the [cacheExtent] will be
437 /// treated as a multiplier for the main axis extent of the
438 /// viewport. In this case there is no default [cacheExtent]; it
439 /// must be explicitly specified.
440 /// {@endtemplate}
441 ///
442 /// Changing the [cacheExtentStyle] without also changing the [cacheExtent]
443 /// is rarely the correct choice.
444 CacheExtentStyle get cacheExtentStyle => _cacheExtentStyle;
445 CacheExtentStyle _cacheExtentStyle;
446 set cacheExtentStyle(CacheExtentStyle value) {
447 if (value == _cacheExtentStyle) {
448 return;
449 }
450 _cacheExtentStyle = value;
451 markNeedsLayout();
452 }
453
454 /// {@macro flutter.material.Material.clipBehavior}
455 ///
456 /// Defaults to [Clip.hardEdge].
457 Clip get clipBehavior => _clipBehavior;
458 Clip _clipBehavior = Clip.hardEdge;
459 set clipBehavior(Clip value) {
460 if (value != _clipBehavior) {
461 _clipBehavior = value;
462 markNeedsPaint();
463 markNeedsSemanticsUpdate();
464 }
465 }
466
467 @override
468 void attach(PipelineOwner owner) {
469 super.attach(owner);
470 _offset.addListener(markNeedsLayout);
471 }
472
473 @override
474 void detach() {
475 _offset.removeListener(markNeedsLayout);
476 super.detach();
477 }
478
479 /// Throws an exception saying that the object does not support returning
480 /// intrinsic dimensions if, in debug mode, we are not in the
481 /// [RenderObject.debugCheckingIntrinsics] mode.
482 ///
483 /// This is used by [computeMinIntrinsicWidth] et al because viewports do not
484 /// generally support returning intrinsic dimensions. See the discussion at
485 /// [computeMinIntrinsicWidth].
486 @protected
487 bool debugThrowIfNotCheckingIntrinsics() {
488 assert(() {
489 if (!RenderObject.debugCheckingIntrinsics) {
490 assert(this is! RenderShrinkWrappingViewport); // it has its own message
491 throw FlutterError.fromParts(<DiagnosticsNode>[
492 ErrorSummary('$runtimeType does not support returning intrinsic dimensions.'),
493 ErrorDescription(
494 'Calculating the intrinsic dimensions would require instantiating every child of '
495 'the viewport, which defeats the point of viewports being lazy.',
496 ),
497 ErrorHint(
498 'If you are merely trying to shrink-wrap the viewport in the main axis direction, '
499 'consider a RenderShrinkWrappingViewport render object (ShrinkWrappingViewport widget), '
500 'which achieves that effect without implementing the intrinsic dimension API.',
501 ),
502 ]);
503 }
504 return true;
505 }());
506 return true;
507 }
508
509 @override
510 double computeMinIntrinsicWidth(double height) {
511 assert(debugThrowIfNotCheckingIntrinsics());
512 return 0.0;
513 }
514
515 @override
516 double computeMaxIntrinsicWidth(double height) {
517 assert(debugThrowIfNotCheckingIntrinsics());
518 return 0.0;
519 }
520
521 @override
522 double computeMinIntrinsicHeight(double width) {
523 assert(debugThrowIfNotCheckingIntrinsics());
524 return 0.0;
525 }
526
527 @override
528 double computeMaxIntrinsicHeight(double width) {
529 assert(debugThrowIfNotCheckingIntrinsics());
530 return 0.0;
531 }
532
533 @override
534 bool get isRepaintBoundary => true;
535
536 /// Determines the size and position of some of the children of the viewport.
537 ///
538 /// This function is the workhorse of `performLayout` implementations in
539 /// subclasses.
540 ///
541 /// Layout starts with `child`, proceeds according to the `advance` callback,
542 /// and stops once `advance` returns null.
543 ///
544 /// * `scrollOffset` is the [SliverConstraints.scrollOffset] to pass the
545 /// first child. The scroll offset is adjusted by
546 /// [SliverGeometry.scrollExtent] for subsequent children.
547 /// * `overlap` is the [SliverConstraints.overlap] to pass the first child.
548 /// The overlay is adjusted by the [SliverGeometry.paintOrigin] and
549 /// [SliverGeometry.paintExtent] for subsequent children.
550 /// * `layoutOffset` is the layout offset at which to place the first child.
551 /// The layout offset is updated by the [SliverGeometry.layoutExtent] for
552 /// subsequent children.
553 /// * `remainingPaintExtent` is [SliverConstraints.remainingPaintExtent] to
554 /// pass the first child. The remaining paint extent is updated by the
555 /// [SliverGeometry.layoutExtent] for subsequent children.
556 /// * `mainAxisExtent` is the [SliverConstraints.viewportMainAxisExtent] to
557 /// pass to each child.
558 /// * `crossAxisExtent` is the [SliverConstraints.crossAxisExtent] to pass to
559 /// each child.
560 /// * `growthDirection` is the [SliverConstraints.growthDirection] to pass to
561 /// each child.
562 ///
563 /// Returns the first non-zero [SliverGeometry.scrollOffsetCorrection]
564 /// encountered, if any. Otherwise returns 0.0. Typical callers will call this
565 /// function repeatedly until it returns 0.0.
566 @protected
567 double layoutChildSequence({
568 required RenderSliver? child,
569 required double scrollOffset,
570 required double overlap,
571 required double layoutOffset,
572 required double remainingPaintExtent,
573 required double mainAxisExtent,
574 required double crossAxisExtent,
575 required GrowthDirection growthDirection,
576 required RenderSliver? Function(RenderSliver child) advance,
577 required double remainingCacheExtent,
578 required double cacheOrigin,
579 }) {
580 assert(scrollOffset.isFinite);
581 assert(scrollOffset >= 0.0);
582 final double initialLayoutOffset = layoutOffset;
583 final ScrollDirection adjustedUserScrollDirection =
584 applyGrowthDirectionToScrollDirection(offset.userScrollDirection, growthDirection);
585 double maxPaintOffset = layoutOffset + overlap;
586 double precedingScrollExtent = 0.0;
587
588 while (child != null) {
589 final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;
590 // If the scrollOffset is too small we adjust the paddedOrigin because it
591 // doesn't make sense to ask a sliver for content before its scroll
592 // offset.
593 final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);
594 final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;
595
596 assert(sliverScrollOffset >= correctedCacheOrigin.abs());
597 assert(correctedCacheOrigin <= 0.0);
598 assert(sliverScrollOffset >= 0.0);
599 assert(cacheExtentCorrection <= 0.0);
600
601 child.layout(SliverConstraints(
602 axisDirection: axisDirection,
603 growthDirection: growthDirection,
604 userScrollDirection: adjustedUserScrollDirection,
605 scrollOffset: sliverScrollOffset,
606 precedingScrollExtent: precedingScrollExtent,
607 overlap: maxPaintOffset - layoutOffset,
608 remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
609 crossAxisExtent: crossAxisExtent,
610 crossAxisDirection: crossAxisDirection,
611 viewportMainAxisExtent: mainAxisExtent,
612 remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
613 cacheOrigin: correctedCacheOrigin,
614 ), parentUsesSize: true);
615
616 final SliverGeometry childLayoutGeometry = child.geometry!;
617 assert(childLayoutGeometry.debugAssertIsValid());
618
619 // If there is a correction to apply, we'll have to start over.
620 if (childLayoutGeometry.scrollOffsetCorrection != null) {
621 return childLayoutGeometry.scrollOffsetCorrection!;
622 }
623
624 // We use the child's paint origin in our coordinate system as the
625 // layoutOffset we store in the child's parent data.
626 final double effectiveLayoutOffset = layoutOffset + childLayoutGeometry.paintOrigin;
627
628 // `effectiveLayoutOffset` becomes meaningless once we moved past the trailing edge
629 // because `childLayoutGeometry.layoutExtent` is zero. Using the still increasing
630 // 'scrollOffset` to roughly position these invisible slivers in the right order.
631 if (childLayoutGeometry.visible || scrollOffset > 0) {
632 updateChildLayoutOffset(child, effectiveLayoutOffset, growthDirection);
633 } else {
634 updateChildLayoutOffset(child, -scrollOffset + initialLayoutOffset, growthDirection);
635 }
636
637 maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
638 scrollOffset -= childLayoutGeometry.scrollExtent;
639 precedingScrollExtent += childLayoutGeometry.scrollExtent;
640 layoutOffset += childLayoutGeometry.layoutExtent;
641 if (childLayoutGeometry.cacheExtent != 0.0) {
642 remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
643 cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
644 }
645
646 updateOutOfBandData(growthDirection, childLayoutGeometry);
647
648 // move on to the next child
649 child = advance(child);
650 }
651
652 // we made it without a correction, whee!
653 return 0.0;
654 }
655
656 @override
657 Rect? describeApproximatePaintClip(RenderSliver child) {
658 switch (clipBehavior) {
659 case Clip.none:
660 return null;
661 case Clip.hardEdge:
662 case Clip.antiAlias:
663 case Clip.antiAliasWithSaveLayer:
664 break;
665 }
666
667 final Rect viewportClip = Offset.zero & size;
668 // The child's viewportMainAxisExtent can be infinite when a
669 // RenderShrinkWrappingViewport is given infinite constraints, such as when
670 // it is the child of a Row or Column (depending on orientation).
671 //
672 // For example, a shrink wrapping render sliver may have infinite
673 // constraints along the viewport's main axis but may also have bouncing
674 // scroll physics, which will allow for some scrolling effect to occur.
675 // We should just use the viewportClip - the start of the overlap is at
676 // double.infinity and so it is effectively meaningless.
677 if (child.constraints.overlap == 0 || !child.constraints.viewportMainAxisExtent.isFinite) {
678 return viewportClip;
679 }
680
681 // Adjust the clip rect for this sliver by the overlap from the previous sliver.
682 double left = viewportClip.left;
683 double right = viewportClip.right;
684 double top = viewportClip.top;
685 double bottom = viewportClip.bottom;
686 final double startOfOverlap = child.constraints.viewportMainAxisExtent - child.constraints.remainingPaintExtent;
687 final double overlapCorrection = startOfOverlap + child.constraints.overlap;
688 switch (applyGrowthDirectionToAxisDirection(axisDirection, child.constraints.growthDirection)) {
689 case AxisDirection.down:
690 top += overlapCorrection;
691 case AxisDirection.up:
692 bottom -= overlapCorrection;
693 case AxisDirection.right:
694 left += overlapCorrection;
695 case AxisDirection.left:
696 right -= overlapCorrection;
697 }
698 return Rect.fromLTRB(left, top, right, bottom);
699 }
700
701 @override
702 Rect describeSemanticsClip(RenderSliver? child) {
703
704 if (_calculatedCacheExtent == null) {
705 return semanticBounds;
706 }
707
708 switch (axis) {
709 case Axis.vertical:
710 return Rect.fromLTRB(
711 semanticBounds.left,
712 semanticBounds.top - _calculatedCacheExtent!,
713 semanticBounds.right,
714 semanticBounds.bottom + _calculatedCacheExtent!,
715 );
716 case Axis.horizontal:
717 return Rect.fromLTRB(
718 semanticBounds.left - _calculatedCacheExtent!,
719 semanticBounds.top,
720 semanticBounds.right + _calculatedCacheExtent!,
721 semanticBounds.bottom,
722 );
723 }
724 }
725
726 @override
727 void paint(PaintingContext context, Offset offset) {
728 if (firstChild == null) {
729 return;
730 }
731 if (hasVisualOverflow && clipBehavior != Clip.none) {
732 _clipRectLayer.layer = context.pushClipRect(
733 needsCompositing,
734 offset,
735 Offset.zero & size,
736 _paintContents,
737 clipBehavior: clipBehavior,
738 oldLayer: _clipRectLayer.layer,
739 );
740 } else {
741 _clipRectLayer.layer = null;
742 _paintContents(context, offset);
743 }
744 }
745
746 final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
747
748 @override
749 void dispose() {
750 _clipRectLayer.layer = null;
751 super.dispose();
752 }
753
754 void _paintContents(PaintingContext context, Offset offset) {
755 for (final RenderSliver child in childrenInPaintOrder) {
756 if (child.geometry!.visible) {
757 context.paintChild(child, offset + paintOffsetOf(child));
758 }
759 }
760 }
761
762 @override
763 void debugPaintSize(PaintingContext context, Offset offset) {
764 assert(() {
765 super.debugPaintSize(context, offset);
766 final Paint paint = Paint()
767 ..style = PaintingStyle.stroke
768 ..strokeWidth = 1.0
769 ..color = const Color(0xFF00FF00);
770 final Canvas canvas = context.canvas;
771 RenderSliver? child = firstChild;
772 while (child != null) {
773 final Size size;
774 switch (axis) {
775 case Axis.vertical:
776 size = Size(child.constraints.crossAxisExtent, child.geometry!.layoutExtent);
777 case Axis.horizontal:
778 size = Size(child.geometry!.layoutExtent, child.constraints.crossAxisExtent);
779 }
780 canvas.drawRect(((offset + paintOffsetOf(child)) & size).deflate(0.5), paint);
781 child = childAfter(child);
782 }
783 return true;
784 }());
785 }
786
787 @override
788 bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
789 double mainAxisPosition, crossAxisPosition;
790 switch (axis) {
791 case Axis.vertical:
792 mainAxisPosition = position.dy;
793 crossAxisPosition = position.dx;
794 case Axis.horizontal:
795 mainAxisPosition = position.dx;
796 crossAxisPosition = position.dy;
797 }
798 final SliverHitTestResult sliverResult = SliverHitTestResult.wrap(result);
799 for (final RenderSliver child in childrenInHitTestOrder) {
800 if (!child.geometry!.visible) {
801 continue;
802 }
803 final Matrix4 transform = Matrix4.identity();
804 applyPaintTransform(child, transform); // must be invertible
805 final bool isHit = result.addWithOutOfBandPosition(
806 paintTransform: transform,
807 hitTest: (BoxHitTestResult result) {
808 return child.hitTest(
809 sliverResult,
810 mainAxisPosition: computeChildMainAxisPosition(child, mainAxisPosition),
811 crossAxisPosition: crossAxisPosition,
812 );
813 },
814 );
815 if (isHit) {
816 return true;
817 }
818 }
819 return false;
820 }
821
822 @override
823 RevealedOffset getOffsetToReveal(
824 RenderObject target,
825 double alignment, {
826 Rect? rect,
827 Axis? axis,
828 }) {
829 // One dimensional viewport has only one axis, override if it was
830 // provided/may be mismatched.
831 axis = this.axis;
832
833 // Steps to convert `rect` (from a RenderBox coordinate system) to its
834 // scroll offset within this viewport (not in the exact order):
835 //
836 // 1. Pick the outermost RenderBox (between which, and the viewport, there
837 // is nothing but RenderSlivers) as an intermediate reference frame
838 // (the `pivot`), convert `rect` to that coordinate space.
839 //
840 // 2. Convert `rect` from the `pivot` coordinate space to its sliver
841 // parent's sliver coordinate system (i.e., to a scroll offset), based on
842 // the axis direction and growth direction of the parent.
843 //
844 // 3. Convert the scroll offset to its sliver parent's coordinate space
845 // using `childScrollOffset`, until we reach the viewport.
846 //
847 // 4. Make the final conversion from the outmost sliver to the viewport
848 // using `scrollOffsetOf`.
849
850 double leadingScrollOffset = 0.0;
851 // Starting at `target` and walking towards the root:
852 // - `child` will be the last object before we reach this viewport, and
853 // - `pivot` will be the last RenderBox before we reach this viewport.
854 RenderObject child = target;
855 RenderBox? pivot;
856 bool onlySlivers = target is RenderSliver; // ... between viewport and `target` (`target` included).
857 while (child.parent != this) {
858 final RenderObject parent = child.parent!;
859 if (child is RenderBox) {
860 pivot = child;
861 }
862 if (parent is RenderSliver) {
863 leadingScrollOffset += parent.childScrollOffset(child)!;
864 } else {
865 onlySlivers = false;
866 leadingScrollOffset = 0.0;
867 }
868 child = parent;
869 }
870
871 // `rect` in the new intermediate coordinate system.
872 final Rect rectLocal;
873 // Our new reference frame render object's main axis extent.
874 final double pivotExtent;
875 final GrowthDirection growthDirection;
876
877 // `leadingScrollOffset` is currently the scrollOffset of our new reference
878 // frame (`pivot` or `target`), within `child`.
879 if (pivot != null) {
880 assert(pivot.parent != null);
881 assert(pivot.parent != this);
882 assert(pivot != this);
883 assert(pivot.parent is RenderSliver); // TODO(abarth): Support other kinds of render objects besides slivers.
884 final RenderSliver pivotParent = pivot.parent! as RenderSliver;
885 growthDirection = pivotParent.constraints.growthDirection;
886 switch (axis) {
887 case Axis.horizontal:
888 pivotExtent = pivot.size.width;
889 case Axis.vertical:
890 pivotExtent = pivot.size.height;
891 }
892 rect ??= target.paintBounds;
893 rectLocal = MatrixUtils.transformRect(target.getTransformTo(pivot), rect);
894 } else if (onlySlivers) {
895 // `pivot` does not exist. We'll have to make up one from `target`, the
896 // innermost sliver.
897 final RenderSliver targetSliver = target as RenderSliver;
898 growthDirection = targetSliver.constraints.growthDirection;
899 // TODO(LongCatIsLooong): make sure this works if `targetSliver` is a
900 // persistent header, when #56413 relands.
901 pivotExtent = targetSliver.geometry!.scrollExtent;
902 if (rect == null) {
903 switch (axis) {
904 case Axis.horizontal:
905 rect = Rect.fromLTWH(
906 0, 0,
907 targetSliver.geometry!.scrollExtent,
908 targetSliver.constraints.crossAxisExtent,
909 );
910 case Axis.vertical:
911 rect = Rect.fromLTWH(
912 0, 0,
913 targetSliver.constraints.crossAxisExtent,
914 targetSliver.geometry!.scrollExtent,
915 );
916 }
917 }
918 rectLocal = rect;
919 } else {
920 assert(rect != null);
921 return RevealedOffset(offset: offset.pixels, rect: rect!);
922 }
923
924 assert(child.parent == this);
925 assert(child is RenderSliver);
926 final RenderSliver sliver = child as RenderSliver;
927
928 final double targetMainAxisExtent;
929 // The scroll offset of `rect` within `child`.
930 switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
931 case AxisDirection.up:
932 leadingScrollOffset += pivotExtent - rectLocal.bottom;
933 targetMainAxisExtent = rectLocal.height;
934 case AxisDirection.right:
935 leadingScrollOffset += rectLocal.left;
936 targetMainAxisExtent = rectLocal.width;
937 case AxisDirection.down:
938 leadingScrollOffset += rectLocal.top;
939 targetMainAxisExtent = rectLocal.height;
940 case AxisDirection.left:
941 leadingScrollOffset += pivotExtent - rectLocal.right;
942 targetMainAxisExtent = rectLocal.width;
943 }
944
945 // So far leadingScrollOffset is the scroll offset of `rect` in the `child`
946 // sliver's sliver coordinate system. The sign of this value indicates
947 // whether the `rect` protrudes the leading edge of the `child` sliver. When
948 // this value is non-negative and `child`'s `maxScrollObstructionExtent` is
949 // greater than 0, we assume `rect` can't be obstructed by the leading edge
950 // of the viewport (i.e. its pinned to the leading edge).
951 final bool isPinned = sliver.geometry!.maxScrollObstructionExtent > 0 && leadingScrollOffset >= 0;
952
953 // The scroll offset in the viewport to `rect`.
954 leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
955
956 // This step assumes the viewport's layout is up-to-date, i.e., if
957 // offset.pixels is changed after the last performLayout, the new scroll
958 // position will not be accounted for.
959 final Matrix4 transform = target.getTransformTo(this);
960 Rect targetRect = MatrixUtils.transformRect(transform, rect);
961 final double extentOfPinnedSlivers = maxScrollObstructionExtentBefore(sliver);
962
963 switch (sliver.constraints.growthDirection) {
964 case GrowthDirection.forward:
965 if (isPinned && alignment <= 0) {
966 return RevealedOffset(offset: double.infinity, rect: targetRect);
967 }
968 leadingScrollOffset -= extentOfPinnedSlivers;
969 case GrowthDirection.reverse:
970 if (isPinned && alignment >= 1) {
971 return RevealedOffset(offset: double.negativeInfinity, rect: targetRect);
972 }
973 // If child's growth direction is reverse, when viewport.offset is
974 // `leadingScrollOffset`, it is positioned just outside of the leading
975 // edge of the viewport.
976 switch (axis) {
977 case Axis.vertical:
978 leadingScrollOffset -= targetRect.height;
979 case Axis.horizontal:
980 leadingScrollOffset -= targetRect.width;
981 }
982 }
983
984 final double mainAxisExtent;
985 switch (axis) {
986 case Axis.horizontal:
987 mainAxisExtent = size.width - extentOfPinnedSlivers;
988 case Axis.vertical:
989 mainAxisExtent = size.height - extentOfPinnedSlivers;
990 }
991
992 final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
993 final double offsetDifference = offset.pixels - targetOffset;
994
995 switch (axisDirection) {
996 case AxisDirection.down:
997 targetRect = targetRect.translate(0.0, offsetDifference);
998 case AxisDirection.right:
999 targetRect = targetRect.translate(offsetDifference, 0.0);
1000 case AxisDirection.up:
1001 targetRect = targetRect.translate(0.0, -offsetDifference);
1002 case AxisDirection.left:
1003 targetRect = targetRect.translate(-offsetDifference, 0.0);
1004 }
1005
1006 return RevealedOffset(offset: targetOffset, rect: targetRect);
1007 }
1008
1009 /// The offset at which the given `child` should be painted.
1010 ///
1011 /// The returned offset is from the top left corner of the inside of the
1012 /// viewport to the top left corner of the paint coordinate system of the
1013 /// `child`.
1014 ///
1015 /// See also:
1016 ///
1017 /// * [paintOffsetOf], which uses the layout offset and growth direction
1018 /// computed for the child during layout.
1019 @protected
1020 Offset computeAbsolutePaintOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
1021 assert(hasSize); // this is only usable once we have a size
1022 assert(child.geometry != null);
1023 switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
1024 case AxisDirection.up:
1025 return Offset(0.0, size.height - (layoutOffset + child.geometry!.paintExtent));
1026 case AxisDirection.right:
1027 return Offset(layoutOffset, 0.0);
1028 case AxisDirection.down:
1029 return Offset(0.0, layoutOffset);
1030 case AxisDirection.left:
1031 return Offset(size.width - (layoutOffset + child.geometry!.paintExtent), 0.0);
1032 }
1033 }
1034
1035 @override
1036 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1037 super.debugFillProperties(properties);
1038 properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
1039 properties.add(EnumProperty<AxisDirection>('crossAxisDirection', crossAxisDirection));
1040 properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
1041 }
1042
1043 @override
1044 List<DiagnosticsNode> debugDescribeChildren() {
1045 final List<DiagnosticsNode> children = <DiagnosticsNode>[];
1046 RenderSliver? child = firstChild;
1047 if (child == null) {
1048 return children;
1049 }
1050
1051 int count = indexOfFirstChild;
1052 while (true) {
1053 children.add(child!.toDiagnosticsNode(name: labelForChild(count)));
1054 if (child == lastChild) {
1055 break;
1056 }
1057 count += 1;
1058 child = childAfter(child);
1059 }
1060 return children;
1061 }
1062
1063 // API TO BE IMPLEMENTED BY SUBCLASSES
1064
1065 // setupParentData
1066
1067 // performLayout (and optionally sizedByParent and performResize)
1068
1069 /// Whether the contents of this viewport would paint outside the bounds of
1070 /// the viewport if [paint] did not clip.
1071 ///
1072 /// This property enables an optimization whereby [paint] can skip apply a
1073 /// clip of the contents of the viewport are known to paint entirely within
1074 /// the bounds of the viewport.
1075 @protected
1076 bool get hasVisualOverflow;
1077
1078 /// Called during [layoutChildSequence] for each child.
1079 ///
1080 /// Typically used by subclasses to update any out-of-band data, such as the
1081 /// max scroll extent, for each child.
1082 @protected
1083 void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry);
1084
1085 /// Called during [layoutChildSequence] to store the layout offset for the
1086 /// given child.
1087 ///
1088 /// Different subclasses using different representations for their children's
1089 /// layout offset (e.g., logical or physical coordinates). This function lets
1090 /// subclasses transform the child's layout offset before storing it in the
1091 /// child's parent data.
1092 @protected
1093 void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection);
1094
1095 /// The offset at which the given `child` should be painted.
1096 ///
1097 /// The returned offset is from the top left corner of the inside of the
1098 /// viewport to the top left corner of the paint coordinate system of the
1099 /// `child`.
1100 ///
1101 /// See also:
1102 ///
1103 /// * [computeAbsolutePaintOffset], which computes the paint offset from an
1104 /// explicit layout offset and growth direction instead of using the values
1105 /// computed for the child during layout.
1106 @protected
1107 Offset paintOffsetOf(RenderSliver child);
1108
1109 /// Returns the scroll offset within the viewport for the given
1110 /// `scrollOffsetWithinChild` within the given `child`.
1111 ///
1112 /// The returned value is an estimate that assumes the slivers within the
1113 /// viewport do not change the layout extent in response to changes in their
1114 /// scroll offset.
1115 @protected
1116 double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild);
1117
1118 /// Returns the total scroll obstruction extent of all slivers in the viewport
1119 /// before [child].
1120 ///
1121 /// This is the extent by which the actual area in which content can scroll
1122 /// is reduced. For example, an app bar that is pinned at the top will reduce
1123 /// the area in which content can actually scroll by the height of the app bar.
1124 @protected
1125 double maxScrollObstructionExtentBefore(RenderSliver child);
1126
1127 /// Converts the `parentMainAxisPosition` into the child's coordinate system.
1128 ///
1129 /// The `parentMainAxisPosition` is a distance from the top edge (for vertical
1130 /// viewports) or left edge (for horizontal viewports) of the viewport bounds.
1131 /// This describes a line, perpendicular to the viewport's main axis, heretofore
1132 /// known as the target line.
1133 ///
1134 /// The child's coordinate system's origin in the main axis is at the leading
1135 /// edge of the given child, as given by the child's
1136 /// [SliverConstraints.axisDirection] and [SliverConstraints.growthDirection].
1137 ///
1138 /// This method returns the distance from the leading edge of the given child to
1139 /// the target line described above.
1140 ///
1141 /// (The `parentMainAxisPosition` is not from the leading edge of the
1142 /// viewport, it's always the top or left edge.)
1143 @protected
1144 double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition);
1145
1146 /// The index of the first child of the viewport relative to the center child.
1147 ///
1148 /// For example, the center child has index zero and the first child in the
1149 /// reverse growth direction has index -1.
1150 @protected
1151 int get indexOfFirstChild;
1152
1153 /// A short string to identify the child with the given index.
1154 ///
1155 /// Used by [debugDescribeChildren] to label the children.
1156 @protected
1157 String labelForChild(int index);
1158
1159 /// Provides an iterable that walks the children of the viewport, in the order
1160 /// that they should be painted.
1161 ///
1162 /// This should be the reverse order of [childrenInHitTestOrder].
1163 @protected
1164 Iterable<RenderSliver> get childrenInPaintOrder;
1165
1166 /// Provides an iterable that walks the children of the viewport, in the order
1167 /// that hit-testing should use.
1168 ///
1169 /// This should be the reverse order of [childrenInPaintOrder].
1170 @protected
1171 Iterable<RenderSliver> get childrenInHitTestOrder;
1172
1173 @override
1174 void showOnScreen({
1175 RenderObject? descendant,
1176 Rect? rect,
1177 Duration duration = Duration.zero,
1178 Curve curve = Curves.ease,
1179 }) {
1180 if (!offset.allowImplicitScrolling) {
1181 return super.showOnScreen(
1182 descendant: descendant,
1183 rect: rect,
1184 duration: duration,
1185 curve: curve,
1186 );
1187 }
1188
1189 final Rect? newRect = RenderViewportBase.showInViewport(
1190 descendant: descendant,
1191 viewport: this,
1192 offset: offset,
1193 rect: rect,
1194 duration: duration,
1195 curve: curve,
1196 );
1197 super.showOnScreen(
1198 rect: newRect,
1199 duration: duration,
1200 curve: curve,
1201 );
1202 }
1203
1204 /// Make (a portion of) the given `descendant` of the given `viewport` fully
1205 /// visible in the `viewport` by manipulating the provided [ViewportOffset]
1206 /// `offset`.
1207 ///
1208 /// The optional `rect` parameter describes which area of the `descendant`
1209 /// should be shown in the viewport. If `rect` is null, the entire
1210 /// `descendant` will be revealed. The `rect` parameter is interpreted
1211 /// relative to the coordinate system of `descendant`.
1212 ///
1213 /// The returned [Rect] describes the new location of `descendant` or `rect`
1214 /// in the viewport after it has been revealed. See [RevealedOffset.rect]
1215 /// for a full definition of this [Rect].
1216 ///
1217 /// If `descendant` is null, this is a no-op and `rect` is returned.
1218 ///
1219 /// If both `descendant` and `rect` are null, null is returned because there is
1220 /// nothing to be shown in the viewport.
1221 ///
1222 /// The `duration` parameter can be set to a non-zero value to animate the
1223 /// target object into the viewport with an animation defined by `curve`.
1224 ///
1225 /// See also:
1226 ///
1227 /// * [RenderObject.showOnScreen], overridden by [RenderViewportBase] and the
1228 /// renderer for [SingleChildScrollView] to delegate to this method.
1229 static Rect? showInViewport({
1230 RenderObject? descendant,
1231 Rect? rect,
1232 required RenderAbstractViewport viewport,
1233 required ViewportOffset offset,
1234 Duration duration = Duration.zero,
1235 Curve curve = Curves.ease,
1236 }) {
1237 if (descendant == null) {
1238 return rect;
1239 }
1240 final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal(descendant, 0.0, rect: rect);
1241 final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal(descendant, 1.0, rect: rect);
1242 final double currentOffset = offset.pixels;
1243 final RevealedOffset? targetOffset = RevealedOffset.clampOffset(
1244 leadingEdgeOffset: leadingEdgeOffset,
1245 trailingEdgeOffset: trailingEdgeOffset,
1246 currentOffset: currentOffset,
1247 );
1248 if (targetOffset == null) {
1249 // `descendant` is between leading and trailing edge and hence already
1250 // fully shown on screen. No action necessary.
1251 assert(viewport.parent != null);
1252 final Matrix4 transform = descendant.getTransformTo(viewport.parent);
1253 return MatrixUtils.transformRect(transform, rect ?? descendant.paintBounds);
1254 }
1255
1256 offset.moveTo(targetOffset.offset, duration: duration, curve: curve);
1257 return targetOffset.rect;
1258 }
1259}
1260
1261/// A render object that is bigger on the inside.
1262///
1263/// [RenderViewport] is the visual workhorse of the scrolling machinery. It
1264/// displays a subset of its children according to its own dimensions and the
1265/// given [offset]. As the offset varies, different children are visible through
1266/// the viewport.
1267///
1268/// [RenderViewport] hosts a bidirectional list of slivers in a single shared
1269/// [Axis], anchored on a [center] sliver, which is placed at the zero scroll
1270/// offset. The center widget is displayed in the viewport according to the
1271/// [anchor] property.
1272///
1273/// Slivers that are earlier in the child list than [center] are displayed in
1274/// reverse order in the reverse [axisDirection] starting from the [center]. For
1275/// example, if the [axisDirection] is [AxisDirection.down], the first sliver
1276/// before [center] is placed above the [center]. The slivers that are later in
1277/// the child list than [center] are placed in order in the [axisDirection]. For
1278/// example, in the preceding scenario, the first sliver after [center] is
1279/// placed below the [center].
1280///
1281/// {@macro flutter.rendering.GrowthDirection.sample}
1282///
1283/// [RenderViewport] cannot contain [RenderBox] children directly. Instead, use
1284/// a [RenderSliverList], [RenderSliverFixedExtentList], [RenderSliverGrid], or
1285/// a [RenderSliverToBoxAdapter], for example.
1286///
1287/// See also:
1288///
1289/// * [RenderSliver], which explains more about the Sliver protocol.
1290/// * [RenderBox], which explains more about the Box protocol.
1291/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
1292/// placed inside a [RenderSliver] (the opposite of this class).
1293/// * [RenderShrinkWrappingViewport], a variant of [RenderViewport] that
1294/// shrink-wraps its contents along the main axis.
1295class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentData> {
1296 /// Creates a viewport for [RenderSliver] objects.
1297 ///
1298 /// If the [center] is not specified, then the first child in the `children`
1299 /// list, if any, is used.
1300 ///
1301 /// The [offset] must be specified. For testing purposes, consider passing a
1302 /// [ViewportOffset.zero] or [ViewportOffset.fixed].
1303 RenderViewport({
1304 super.axisDirection,
1305 required super.crossAxisDirection,
1306 required super.offset,
1307 double anchor = 0.0,
1308 List<RenderSliver>? children,
1309 RenderSliver? center,
1310 super.cacheExtent,
1311 super.cacheExtentStyle,
1312 super.clipBehavior,
1313 }) : assert(anchor >= 0.0 && anchor <= 1.0),
1314 assert(cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null),
1315 _anchor = anchor,
1316 _center = center {
1317 addAll(children);
1318 if (center == null && firstChild != null) {
1319 _center = firstChild;
1320 }
1321 }
1322
1323 /// If a [RenderAbstractViewport] overrides
1324 /// [RenderObject.describeSemanticsConfiguration] to add the [SemanticsTag]
1325 /// [useTwoPaneSemantics] to its [SemanticsConfiguration], two semantics nodes
1326 /// will be used to represent the viewport with its associated scrolling
1327 /// actions in the semantics tree.
1328 ///
1329 /// Two semantics nodes (an inner and an outer node) are necessary to exclude
1330 /// certain child nodes (via the [excludeFromScrolling] tag) from the
1331 /// scrollable area for semantic purposes: The [SemanticsNode]s of children
1332 /// that should be excluded from scrolling will be attached to the outer node.
1333 /// The semantic scrolling actions and the [SemanticsNode]s of scrollable
1334 /// children will be attached to the inner node, which itself is a child of
1335 /// the outer node.
1336 ///
1337 /// See also:
1338 ///
1339 /// * [RenderViewportBase.describeSemanticsConfiguration], which adds this
1340 /// tag to its [SemanticsConfiguration].
1341 static const SemanticsTag useTwoPaneSemantics = SemanticsTag('RenderViewport.twoPane');
1342
1343 /// When a top-level [SemanticsNode] below a [RenderAbstractViewport] is
1344 /// tagged with [excludeFromScrolling] it will not be part of the scrolling
1345 /// area for semantic purposes.
1346 ///
1347 /// This behavior is only active if the [RenderAbstractViewport]
1348 /// tagged its [SemanticsConfiguration] with [useTwoPaneSemantics].
1349 /// Otherwise, the [excludeFromScrolling] tag is ignored.
1350 ///
1351 /// As an example, a [RenderSliver] that stays on the screen within a
1352 /// [Scrollable] even though the user has scrolled past it (e.g. a pinned app
1353 /// bar) can tag its [SemanticsNode] with [excludeFromScrolling] to indicate
1354 /// that it should no longer be considered for semantic actions related to
1355 /// scrolling.
1356 static const SemanticsTag excludeFromScrolling = SemanticsTag('RenderViewport.excludeFromScrolling');
1357
1358 @override
1359 void setupParentData(RenderObject child) {
1360 if (child.parentData is! SliverPhysicalContainerParentData) {
1361 child.parentData = SliverPhysicalContainerParentData();
1362 }
1363 }
1364
1365 /// The relative position of the zero scroll offset.
1366 ///
1367 /// For example, if [anchor] is 0.5 and the [axisDirection] is
1368 /// [AxisDirection.down] or [AxisDirection.up], then the zero scroll offset is
1369 /// vertically centered within the viewport. If the [anchor] is 1.0, and the
1370 /// [axisDirection] is [AxisDirection.right], then the zero scroll offset is
1371 /// on the left edge of the viewport.
1372 ///
1373 /// {@macro flutter.rendering.GrowthDirection.sample}
1374 double get anchor => _anchor;
1375 double _anchor;
1376 set anchor(double value) {
1377 assert(value >= 0.0 && value <= 1.0);
1378 if (value == _anchor) {
1379 return;
1380 }
1381 _anchor = value;
1382 markNeedsLayout();
1383 }
1384
1385 /// The first child in the [GrowthDirection.forward] growth direction.
1386 ///
1387 /// This child that will be at the position defined by [anchor] when the
1388 /// [ViewportOffset.pixels] of [offset] is `0`.
1389 ///
1390 /// Children after [center] will be placed in the [axisDirection] relative to
1391 /// the [center].
1392 ///
1393 /// Children before [center] will be placed in the opposite of
1394 /// the [axisDirection] relative to the [center]. These children above
1395 /// [center] will have a growth direction of [GrowthDirection.reverse].
1396 ///
1397 /// The [center] must be a direct child of the viewport.
1398 ///
1399 /// {@macro flutter.rendering.GrowthDirection.sample}
1400 RenderSliver? get center => _center;
1401 RenderSliver? _center;
1402 set center(RenderSliver? value) {
1403 if (value == _center) {
1404 return;
1405 }
1406 _center = value;
1407 markNeedsLayout();
1408 }
1409
1410 @override
1411 bool get sizedByParent => true;
1412
1413 @override
1414 @protected
1415 Size computeDryLayout(covariant BoxConstraints constraints) {
1416 assert(debugCheckHasBoundedAxis(axis, constraints));
1417 return constraints.biggest;
1418 }
1419
1420 static const int _maxLayoutCycles = 10;
1421
1422 // Out-of-band data computed during layout.
1423 late double _minScrollExtent;
1424 late double _maxScrollExtent;
1425 bool _hasVisualOverflow = false;
1426
1427 @override
1428 void performLayout() {
1429 // Ignore the return value of applyViewportDimension because we are
1430 // doing a layout regardless.
1431 switch (axis) {
1432 case Axis.vertical:
1433 offset.applyViewportDimension(size.height);
1434 case Axis.horizontal:
1435 offset.applyViewportDimension(size.width);
1436 }
1437
1438 if (center == null) {
1439 assert(firstChild == null);
1440 _minScrollExtent = 0.0;
1441 _maxScrollExtent = 0.0;
1442 _hasVisualOverflow = false;
1443 offset.applyContentDimensions(0.0, 0.0);
1444 return;
1445 }
1446 assert(center!.parent == this);
1447
1448 final double mainAxisExtent;
1449 final double crossAxisExtent;
1450 switch (axis) {
1451 case Axis.vertical:
1452 mainAxisExtent = size.height;
1453 crossAxisExtent = size.width;
1454 case Axis.horizontal:
1455 mainAxisExtent = size.width;
1456 crossAxisExtent = size.height;
1457 }
1458
1459 final double centerOffsetAdjustment = center!.centerOffsetAdjustment;
1460
1461 double correction;
1462 int count = 0;
1463 do {
1464 correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
1465 if (correction != 0.0) {
1466 offset.correctBy(correction);
1467 } else {
1468 if (offset.applyContentDimensions(
1469 math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
1470 math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
1471 )) {
1472 break;
1473 }
1474 }
1475 count += 1;
1476 } while (count < _maxLayoutCycles);
1477 assert(() {
1478 if (count >= _maxLayoutCycles) {
1479 assert(count != 1);
1480 throw FlutterError(
1481 'A RenderViewport exceeded its maximum number of layout cycles.\n'
1482 'RenderViewport render objects, during layout, can retry if either their '
1483 'slivers or their ViewportOffset decide that the offset should be corrected '
1484 'to take into account information collected during that layout.\n'
1485 'In the case of this RenderViewport object, however, this happened $count '
1486 'times and still there was no consensus on the scroll offset. This usually '
1487 'indicates a bug. Specifically, it means that one of the following three '
1488 'problems is being experienced by the RenderViewport object:\n'
1489 ' * One of the RenderSliver children or the ViewportOffset have a bug such'
1490 ' that they always think that they need to correct the offset regardless.\n'
1491 ' * Some combination of the RenderSliver children and the ViewportOffset'
1492 ' have a bad interaction such that one applies a correction then another'
1493 ' applies a reverse correction, leading to an infinite loop of corrections.\n'
1494 ' * There is a pathological case that would eventually resolve, but it is'
1495 ' so complicated that it cannot be resolved in any reasonable number of'
1496 ' layout passes.',
1497 );
1498 }
1499 return true;
1500 }());
1501 }
1502
1503 double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
1504 assert(!mainAxisExtent.isNaN);
1505 assert(mainAxisExtent >= 0.0);
1506 assert(crossAxisExtent.isFinite);
1507 assert(crossAxisExtent >= 0.0);
1508 assert(correctedOffset.isFinite);
1509 _minScrollExtent = 0.0;
1510 _maxScrollExtent = 0.0;
1511 _hasVisualOverflow = false;
1512
1513 // centerOffset is the offset from the leading edge of the RenderViewport
1514 // to the zero scroll offset (the line between the forward slivers and the
1515 // reverse slivers).
1516 final double centerOffset = mainAxisExtent * anchor - correctedOffset;
1517 final double reverseDirectionRemainingPaintExtent = clampDouble(centerOffset, 0.0, mainAxisExtent);
1518 final double forwardDirectionRemainingPaintExtent = clampDouble(mainAxisExtent - centerOffset, 0.0, mainAxisExtent);
1519
1520 switch (cacheExtentStyle) {
1521 case CacheExtentStyle.pixel:
1522 _calculatedCacheExtent = cacheExtent;
1523 case CacheExtentStyle.viewport:
1524 _calculatedCacheExtent = mainAxisExtent * _cacheExtent;
1525 }
1526
1527 final double fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent!;
1528 final double centerCacheOffset = centerOffset + _calculatedCacheExtent!;
1529 final double reverseDirectionRemainingCacheExtent = clampDouble(centerCacheOffset, 0.0, fullCacheExtent);
1530 final double forwardDirectionRemainingCacheExtent = clampDouble(fullCacheExtent - centerCacheOffset, 0.0, fullCacheExtent);
1531
1532 final RenderSliver? leadingNegativeChild = childBefore(center!);
1533
1534 if (leadingNegativeChild != null) {
1535 // negative scroll offsets
1536 final double result = layoutChildSequence(
1537 child: leadingNegativeChild,
1538 scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
1539 overlap: 0.0,
1540 layoutOffset: forwardDirectionRemainingPaintExtent,
1541 remainingPaintExtent: reverseDirectionRemainingPaintExtent,
1542 mainAxisExtent: mainAxisExtent,
1543 crossAxisExtent: crossAxisExtent,
1544 growthDirection: GrowthDirection.reverse,
1545 advance: childBefore,
1546 remainingCacheExtent: reverseDirectionRemainingCacheExtent,
1547 cacheOrigin: clampDouble(mainAxisExtent - centerOffset, -_calculatedCacheExtent!, 0.0),
1548 );
1549 if (result != 0.0) {
1550 return -result;
1551 }
1552 }
1553
1554 // positive scroll offsets
1555 return layoutChildSequence(
1556 child: center,
1557 scrollOffset: math.max(0.0, -centerOffset),
1558 overlap: leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
1559 layoutOffset: centerOffset >= mainAxisExtent ? centerOffset: reverseDirectionRemainingPaintExtent,
1560 remainingPaintExtent: forwardDirectionRemainingPaintExtent,
1561 mainAxisExtent: mainAxisExtent,
1562 crossAxisExtent: crossAxisExtent,
1563 growthDirection: GrowthDirection.forward,
1564 advance: childAfter,
1565 remainingCacheExtent: forwardDirectionRemainingCacheExtent,
1566 cacheOrigin: clampDouble(centerOffset, -_calculatedCacheExtent!, 0.0),
1567 );
1568 }
1569
1570 @override
1571 bool get hasVisualOverflow => _hasVisualOverflow;
1572
1573 @override
1574 void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
1575 switch (growthDirection) {
1576 case GrowthDirection.forward:
1577 _maxScrollExtent += childLayoutGeometry.scrollExtent;
1578 case GrowthDirection.reverse:
1579 _minScrollExtent -= childLayoutGeometry.scrollExtent;
1580 }
1581 if (childLayoutGeometry.hasVisualOverflow) {
1582 _hasVisualOverflow = true;
1583 }
1584 }
1585
1586 @override
1587 void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
1588 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
1589 childParentData.paintOffset = computeAbsolutePaintOffset(child, layoutOffset, growthDirection);
1590 }
1591
1592 @override
1593 Offset paintOffsetOf(RenderSliver child) {
1594 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
1595 return childParentData.paintOffset;
1596 }
1597
1598 @override
1599 double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) {
1600 assert(child.parent == this);
1601 final GrowthDirection growthDirection = child.constraints.growthDirection;
1602 switch (growthDirection) {
1603 case GrowthDirection.forward:
1604 double scrollOffsetToChild = 0.0;
1605 RenderSliver? current = center;
1606 while (current != child) {
1607 scrollOffsetToChild += current!.geometry!.scrollExtent;
1608 current = childAfter(current);
1609 }
1610 return scrollOffsetToChild + scrollOffsetWithinChild;
1611 case GrowthDirection.reverse:
1612 double scrollOffsetToChild = 0.0;
1613 RenderSliver? current = childBefore(center!);
1614 while (current != child) {
1615 scrollOffsetToChild -= current!.geometry!.scrollExtent;
1616 current = childBefore(current);
1617 }
1618 return scrollOffsetToChild - scrollOffsetWithinChild;
1619 }
1620 }
1621
1622 @override
1623 double maxScrollObstructionExtentBefore(RenderSliver child) {
1624 assert(child.parent == this);
1625 final GrowthDirection growthDirection = child.constraints.growthDirection;
1626 switch (growthDirection) {
1627 case GrowthDirection.forward:
1628 double pinnedExtent = 0.0;
1629 RenderSliver? current = center;
1630 while (current != child) {
1631 pinnedExtent += current!.geometry!.maxScrollObstructionExtent;
1632 current = childAfter(current);
1633 }
1634 return pinnedExtent;
1635 case GrowthDirection.reverse:
1636 double pinnedExtent = 0.0;
1637 RenderSliver? current = childBefore(center!);
1638 while (current != child) {
1639 pinnedExtent += current!.geometry!.maxScrollObstructionExtent;
1640 current = childBefore(current);
1641 }
1642 return pinnedExtent;
1643 }
1644 }
1645
1646 @override
1647 void applyPaintTransform(RenderObject child, Matrix4 transform) {
1648 // Hit test logic relies on this always providing an invertible matrix.
1649 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
1650 childParentData.applyPaintTransform(transform);
1651 }
1652
1653 @override
1654 double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
1655 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
1656 switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
1657 case AxisDirection.down:
1658 return parentMainAxisPosition - childParentData.paintOffset.dy;
1659 case AxisDirection.right:
1660 return parentMainAxisPosition - childParentData.paintOffset.dx;
1661 case AxisDirection.up:
1662 return child.geometry!.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dy);
1663 case AxisDirection.left:
1664 return child.geometry!.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dx);
1665 }
1666 }
1667
1668 @override
1669 int get indexOfFirstChild {
1670 assert(center != null);
1671 assert(center!.parent == this);
1672 assert(firstChild != null);
1673 int count = 0;
1674 RenderSliver? child = center;
1675 while (child != firstChild) {
1676 count -= 1;
1677 child = childBefore(child!);
1678 }
1679 return count;
1680 }
1681
1682 @override
1683 String labelForChild(int index) {
1684 if (index == 0) {
1685 return 'center child';
1686 }
1687 return 'child $index';
1688 }
1689
1690 @override
1691 Iterable<RenderSliver> get childrenInPaintOrder {
1692 final List<RenderSliver> children = <RenderSliver>[];
1693 if (firstChild == null) {
1694 return children;
1695 }
1696 RenderSliver? child = firstChild;
1697 while (child != center) {
1698 children.add(child!);
1699 child = childAfter(child);
1700 }
1701 child = lastChild;
1702 while (true) {
1703 children.add(child!);
1704 if (child == center) {
1705 return children;
1706 }
1707 child = childBefore(child);
1708 }
1709 }
1710
1711 @override
1712 Iterable<RenderSliver> get childrenInHitTestOrder {
1713 final List<RenderSliver> children = <RenderSliver>[];
1714 if (firstChild == null) {
1715 return children;
1716 }
1717 RenderSliver? child = center;
1718 while (child != null) {
1719 children.add(child);
1720 child = childAfter(child);
1721 }
1722 child = childBefore(center!);
1723 while (child != null) {
1724 children.add(child);
1725 child = childBefore(child);
1726 }
1727 return children;
1728 }
1729
1730 @override
1731 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1732 super.debugFillProperties(properties);
1733 properties.add(DoubleProperty('anchor', anchor));
1734 }
1735}
1736
1737/// A render object that is bigger on the inside and shrink wraps its children
1738/// in the main axis.
1739///
1740/// [RenderShrinkWrappingViewport] displays a subset of its children according
1741/// to its own dimensions and the given [offset]. As the offset varies, different
1742/// children are visible through the viewport.
1743///
1744/// [RenderShrinkWrappingViewport] differs from [RenderViewport] in that
1745/// [RenderViewport] expands to fill the main axis whereas
1746/// [RenderShrinkWrappingViewport] sizes itself to match its children in the
1747/// main axis. This shrink wrapping behavior is expensive because the children,
1748/// and hence the viewport, could potentially change size whenever the [offset]
1749/// changes (e.g., because of a collapsing header).
1750///
1751/// [RenderShrinkWrappingViewport] cannot contain [RenderBox] children directly.
1752/// Instead, use a [RenderSliverList], [RenderSliverFixedExtentList],
1753/// [RenderSliverGrid], or a [RenderSliverToBoxAdapter], for example.
1754///
1755/// See also:
1756///
1757/// * [RenderViewport], a viewport that does not shrink-wrap its contents.
1758/// * [RenderSliver], which explains more about the Sliver protocol.
1759/// * [RenderBox], which explains more about the Box protocol.
1760/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
1761/// placed inside a [RenderSliver] (the opposite of this class).
1762class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalContainerParentData> {
1763 /// Creates a viewport (for [RenderSliver] objects) that shrink-wraps its
1764 /// contents.
1765 ///
1766 /// The [offset] must be specified. For testing purposes, consider passing a
1767 /// [ViewportOffset.zero] or [ViewportOffset.fixed].
1768 RenderShrinkWrappingViewport({
1769 super.axisDirection,
1770 required super.crossAxisDirection,
1771 required super.offset,
1772 super.clipBehavior,
1773 List<RenderSliver>? children,
1774 }) {
1775 addAll(children);
1776 }
1777
1778 @override
1779 void setupParentData(RenderObject child) {
1780 if (child.parentData is! SliverLogicalContainerParentData) {
1781 child.parentData = SliverLogicalContainerParentData();
1782 }
1783 }
1784
1785 @override
1786 bool debugThrowIfNotCheckingIntrinsics() {
1787 assert(() {
1788 if (!RenderObject.debugCheckingIntrinsics) {
1789 throw FlutterError.fromParts(<DiagnosticsNode>[
1790 ErrorSummary('$runtimeType does not support returning intrinsic dimensions.'),
1791 ErrorDescription(
1792 'Calculating the intrinsic dimensions would require instantiating every child of '
1793 'the viewport, which defeats the point of viewports being lazy.',
1794 ),
1795 ErrorHint(
1796 'If you are merely trying to shrink-wrap the viewport in the main axis direction, '
1797 'you should be able to achieve that effect by just giving the viewport loose '
1798 'constraints, without needing to measure its intrinsic dimensions.',
1799 ),
1800 ]);
1801 }
1802 return true;
1803 }());
1804 return true;
1805 }
1806
1807 // Out-of-band data computed during layout.
1808 late double _maxScrollExtent;
1809 late double _shrinkWrapExtent;
1810 bool _hasVisualOverflow = false;
1811
1812 bool _debugCheckHasBoundedCrossAxis() {
1813 assert(() {
1814 switch (axis) {
1815 case Axis.vertical:
1816 if (!constraints.hasBoundedWidth) {
1817 throw FlutterError(
1818 'Vertical viewport was given unbounded width.\n'
1819 'Viewports expand in the cross axis to fill their container and '
1820 'constrain their children to match their extent in the cross axis. '
1821 'In this case, a vertical shrinkwrapping viewport was given an '
1822 'unlimited amount of horizontal space in which to expand.',
1823 );
1824 }
1825 case Axis.horizontal:
1826 if (!constraints.hasBoundedHeight) {
1827 throw FlutterError(
1828 'Horizontal viewport was given unbounded height.\n'
1829 'Viewports expand in the cross axis to fill their container and '
1830 'constrain their children to match their extent in the cross axis. '
1831 'In this case, a horizontal shrinkwrapping viewport was given an '
1832 'unlimited amount of vertical space in which to expand.',
1833 );
1834 }
1835 }
1836 return true;
1837 }());
1838 return true;
1839 }
1840
1841 @override
1842 void performLayout() {
1843 final BoxConstraints constraints = this.constraints;
1844 if (firstChild == null) {
1845 // Shrinkwrapping viewport only requires the cross axis to be bounded.
1846 assert(_debugCheckHasBoundedCrossAxis());
1847 switch (axis) {
1848 case Axis.vertical:
1849 size = Size(constraints.maxWidth, constraints.minHeight);
1850 case Axis.horizontal:
1851 size = Size(constraints.minWidth, constraints.maxHeight);
1852 }
1853 offset.applyViewportDimension(0.0);
1854 _maxScrollExtent = 0.0;
1855 _shrinkWrapExtent = 0.0;
1856 _hasVisualOverflow = false;
1857 offset.applyContentDimensions(0.0, 0.0);
1858 return;
1859 }
1860
1861 final double mainAxisExtent;
1862 final double crossAxisExtent;
1863 // Shrinkwrapping viewport only requires the cross axis to be bounded.
1864 assert(_debugCheckHasBoundedCrossAxis());
1865 switch (axis) {
1866 case Axis.vertical:
1867 mainAxisExtent = constraints.maxHeight;
1868 crossAxisExtent = constraints.maxWidth;
1869 case Axis.horizontal:
1870 mainAxisExtent = constraints.maxWidth;
1871 crossAxisExtent = constraints.maxHeight;
1872 }
1873
1874 double correction;
1875 double effectiveExtent;
1876 while (true) {
1877 correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels);
1878 if (correction != 0.0) {
1879 offset.correctBy(correction);
1880 } else {
1881 switch (axis) {
1882 case Axis.vertical:
1883 effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent);
1884 case Axis.horizontal:
1885 effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent);
1886 }
1887 final bool didAcceptViewportDimension = offset.applyViewportDimension(effectiveExtent);
1888 final bool didAcceptContentDimension = offset.applyContentDimensions(0.0, math.max(0.0, _maxScrollExtent - effectiveExtent));
1889 if (didAcceptViewportDimension && didAcceptContentDimension) {
1890 break;
1891 }
1892 }
1893 }
1894 switch (axis) {
1895 case Axis.vertical:
1896 size = constraints.constrainDimensions(crossAxisExtent, effectiveExtent);
1897 case Axis.horizontal:
1898 size = constraints.constrainDimensions(effectiveExtent, crossAxisExtent);
1899 }
1900 }
1901
1902 double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
1903 // We can't assert mainAxisExtent is finite, because it could be infinite if
1904 // it is within a column or row for example. In such a case, there's not
1905 // even any scrolling to do, although some scroll physics (i.e.
1906 // BouncingScrollPhysics) could still temporarily scroll the content in a
1907 // simulation.
1908 assert(!mainAxisExtent.isNaN);
1909 assert(mainAxisExtent >= 0.0);
1910 assert(crossAxisExtent.isFinite);
1911 assert(crossAxisExtent >= 0.0);
1912 assert(correctedOffset.isFinite);
1913 _maxScrollExtent = 0.0;
1914 _shrinkWrapExtent = 0.0;
1915 // Since the viewport is shrinkwrapped, we know that any negative overscroll
1916 // into the potentially infinite mainAxisExtent will overflow the end of
1917 // the viewport.
1918 _hasVisualOverflow = correctedOffset < 0.0;
1919 switch (cacheExtentStyle) {
1920 case CacheExtentStyle.pixel:
1921 _calculatedCacheExtent = cacheExtent;
1922 case CacheExtentStyle.viewport:
1923 _calculatedCacheExtent = mainAxisExtent * _cacheExtent;
1924 }
1925
1926 return layoutChildSequence(
1927 child: firstChild,
1928 scrollOffset: math.max(0.0, correctedOffset),
1929 overlap: math.min(0.0, correctedOffset),
1930 layoutOffset: math.max(0.0, -correctedOffset),
1931 remainingPaintExtent: mainAxisExtent + math.min(0.0, correctedOffset),
1932 mainAxisExtent: mainAxisExtent,
1933 crossAxisExtent: crossAxisExtent,
1934 growthDirection: GrowthDirection.forward,
1935 advance: childAfter,
1936 remainingCacheExtent: mainAxisExtent + 2 * _calculatedCacheExtent!,
1937 cacheOrigin: -_calculatedCacheExtent!,
1938 );
1939 }
1940
1941 @override
1942 bool get hasVisualOverflow => _hasVisualOverflow;
1943
1944 @override
1945 void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
1946 assert(growthDirection == GrowthDirection.forward);
1947 _maxScrollExtent += childLayoutGeometry.scrollExtent;
1948 if (childLayoutGeometry.hasVisualOverflow) {
1949 _hasVisualOverflow = true;
1950 }
1951 _shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
1952 }
1953
1954 @override
1955 void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
1956 assert(growthDirection == GrowthDirection.forward);
1957 final SliverLogicalParentData childParentData = child.parentData! as SliverLogicalParentData;
1958 childParentData.layoutOffset = layoutOffset;
1959 }
1960
1961 @override
1962 Offset paintOffsetOf(RenderSliver child) {
1963 final SliverLogicalParentData childParentData = child.parentData! as SliverLogicalParentData;
1964 return computeAbsolutePaintOffset(child, childParentData.layoutOffset!, GrowthDirection.forward);
1965 }
1966
1967 @override
1968 double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) {
1969 assert(child.parent == this);
1970 assert(child.constraints.growthDirection == GrowthDirection.forward);
1971 double scrollOffsetToChild = 0.0;
1972 RenderSliver? current = firstChild;
1973 while (current != child) {
1974 scrollOffsetToChild += current!.geometry!.scrollExtent;
1975 current = childAfter(current);
1976 }
1977 return scrollOffsetToChild + scrollOffsetWithinChild;
1978 }
1979
1980 @override
1981 double maxScrollObstructionExtentBefore(RenderSliver child) {
1982 assert(child.parent == this);
1983 assert(child.constraints.growthDirection == GrowthDirection.forward);
1984 double pinnedExtent = 0.0;
1985 RenderSliver? current = firstChild;
1986 while (current != child) {
1987 pinnedExtent += current!.geometry!.maxScrollObstructionExtent;
1988 current = childAfter(current);
1989 }
1990 return pinnedExtent;
1991 }
1992
1993 @override
1994 void applyPaintTransform(RenderObject child, Matrix4 transform) {
1995 // Hit test logic relies on this always providing an invertible matrix.
1996 final Offset offset = paintOffsetOf(child as RenderSliver);
1997 transform.translate(offset.dx, offset.dy);
1998 }
1999
2000 @override
2001 double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
2002 assert(hasSize);
2003 final SliverLogicalParentData childParentData = child.parentData! as SliverLogicalParentData;
2004 switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
2005 case AxisDirection.down:
2006 case AxisDirection.right:
2007 return parentMainAxisPosition - childParentData.layoutOffset!;
2008 case AxisDirection.up:
2009 return (size.height - parentMainAxisPosition) - childParentData.layoutOffset!;
2010 case AxisDirection.left:
2011 return (size.width - parentMainAxisPosition) - childParentData.layoutOffset!;
2012 }
2013 }
2014
2015 @override
2016 int get indexOfFirstChild => 0;
2017
2018 @override
2019 String labelForChild(int index) => 'child $index';
2020
2021 @override
2022 Iterable<RenderSliver> get childrenInPaintOrder {
2023 final List<RenderSliver> children = <RenderSliver>[];
2024 RenderSliver? child = lastChild;
2025 while (child != null) {
2026 children.add(child);
2027 child = childBefore(child);
2028 }
2029 return children;
2030 }
2031
2032 @override
2033 Iterable<RenderSliver> get childrenInHitTestOrder {
2034 final List<RenderSliver> children = <RenderSliver>[];
2035 RenderSliver? child = firstChild;
2036 while (child != null) {
2037 children.add(child);
2038 child = childAfter(child);
2039 }
2040 return children;
2041 }
2042}
2043