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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com