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'; |
11 | library; |
12 | |
13 | import 'dart:math' as math; |
14 | |
15 | import 'package:flutter/animation.dart'; |
16 | import 'package:flutter/foundation.dart'; |
17 | import 'package:flutter/semantics.dart'; |
18 | |
19 | import 'box.dart'; |
20 | import 'debug.dart'; |
21 | import 'layer.dart'; |
22 | import 'object.dart'; |
23 | import 'sliver.dart'; |
24 | import 'viewport_offset.dart'; |
25 | |
26 | /// The unit of measurement for a [Viewport.cacheExtent]. |
27 | enum 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. |
41 | abstract 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]. |
152 | class 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). |
266 | abstract 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. |
1317 | class 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). |
1803 | class 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 |
Definitions
- CacheExtentStyle
- RenderAbstractViewport
- maybeOf
- of
- getOffsetToReveal
- RevealedOffset
- RevealedOffset
- clampOffset
- toString
- RenderViewportBase
- RenderViewportBase
- describeSemanticsConfiguration
- visitChildrenForSemantics
- axisDirection
- axisDirection
- crossAxisDirection
- crossAxisDirection
- axis
- offset
- offset
- cacheExtent
- cacheExtent
- cacheExtentStyle
- cacheExtentStyle
- clipBehavior
- clipBehavior
- attach
- detach
- debugThrowIfNotCheckingIntrinsics
- computeMinIntrinsicWidth
- computeMaxIntrinsicWidth
- computeMinIntrinsicHeight
- computeMaxIntrinsicHeight
- isRepaintBoundary
- layoutChildSequence
- describeApproximatePaintClip
- describeSemanticsClip
- paint
- dispose
- _paintContents
- debugPaintSize
- hitTestChildren
- getOffsetToReveal
- computeAbsolutePaintOffset
- debugFillProperties
- debugDescribeChildren
- hasVisualOverflow
- updateOutOfBandData
- updateChildLayoutOffset
- paintOffsetOf
- scrollOffsetOf
- maxScrollObstructionExtentBefore
- computeChildMainAxisPosition
- indexOfFirstChild
- labelForChild
- childrenInPaintOrder
- childrenInHitTestOrder
- showOnScreen
- showInViewport
- RenderViewport
- RenderViewport
- setupParentData
- anchor
- anchor
- center
- center
- sizedByParent
- computeDryLayout
- performLayout
- _attemptLayout
- hasVisualOverflow
- updateOutOfBandData
- updateChildLayoutOffset
- paintOffsetOf
- scrollOffsetOf
- maxScrollObstructionExtentBefore
- applyPaintTransform
- computeChildMainAxisPosition
- indexOfFirstChild
- labelForChild
- childrenInPaintOrder
- childrenInHitTestOrder
- debugFillProperties
- RenderShrinkWrappingViewport
- RenderShrinkWrappingViewport
- setupParentData
- debugThrowIfNotCheckingIntrinsics
- _debugCheckHasBoundedCrossAxis
- performLayout
- _attemptLayout
- hasVisualOverflow
- updateOutOfBandData
- updateChildLayoutOffset
- paintOffsetOf
- scrollOffsetOf
- maxScrollObstructionExtentBefore
- applyPaintTransform
- computeChildMainAxisPosition
- indexOfFirstChild
- labelForChild
- childrenInPaintOrder
Learn more about Flutter for embedded and desktop on industrialflutter.com