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