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:vector_math/vector_math_64.dart' show Matrix4; |
10 | |
11 | import 'box.dart'; |
12 | import 'layer.dart'; |
13 | import 'object.dart'; |
14 | import 'proxy_box.dart'; |
15 | import 'viewport.dart'; |
16 | import 'viewport_offset.dart'; |
17 | |
18 | typedef _ChildSizingFunction = double Function(RenderBox child); |
19 | |
20 | /// A delegate used by [RenderListWheelViewport] to manage its children. |
21 | /// |
22 | /// [RenderListWheelViewport] during layout will ask the delegate to create |
23 | /// children that are visible in the viewport and remove those that are not. |
24 | abstract class ListWheelChildManager { |
25 | /// The maximum number of children that can be provided to |
26 | /// [RenderListWheelViewport]. |
27 | /// |
28 | /// If non-null, the children will have index in the range |
29 | /// `[0, childCount - 1]`. |
30 | /// |
31 | /// If null, then there's no explicit limits to the range of the children |
32 | /// except that it has to be contiguous. If [childExistsAt] for a certain |
33 | /// index returns false, that index is already past the limit. |
34 | int? get childCount; |
35 | |
36 | /// Checks whether the delegate is able to provide a child widget at the given |
37 | /// index. |
38 | /// |
39 | /// This function is not about whether the child at the given index is |
40 | /// attached to the [RenderListWheelViewport] or not. |
41 | bool childExistsAt(int index); |
42 | |
43 | /// Creates a new child at the given index and updates it to the child list |
44 | /// of [RenderListWheelViewport]. If no child corresponds to `index`, then do |
45 | /// nothing. |
46 | /// |
47 | /// It is possible to create children with negative indices. |
48 | void createChild(int index, { required RenderBox? after }); |
49 | |
50 | /// Removes the child element corresponding with the given RenderBox. |
51 | void removeChild(RenderBox child); |
52 | } |
53 | |
54 | /// [ParentData] for use with [RenderListWheelViewport]. |
55 | class ListWheelParentData extends ContainerBoxParentData<RenderBox> { |
56 | /// Index of this child in its parent's child list. |
57 | /// |
58 | /// This must be maintained by the [ListWheelChildManager]. |
59 | int? index; |
60 | |
61 | /// Transform applied to this child during painting. |
62 | /// |
63 | /// Can be used to find the local bounds of this child in the viewport, |
64 | /// and then use it, for example, in hit testing. |
65 | /// |
66 | /// May be null if child was laid out, but not painted |
67 | /// by the parent, but normally this shouldn't happen, |
68 | /// because [RenderListWheelViewport] paints all of the |
69 | /// children it has laid out. |
70 | Matrix4? transform; |
71 | } |
72 | |
73 | /// Render, onto a wheel, a bigger sequential set of objects inside this viewport. |
74 | /// |
75 | /// Takes a scrollable set of fixed sized [RenderBox]es and renders them |
76 | /// sequentially from top down on a vertical scrolling axis. |
77 | /// |
78 | /// It starts with the first scrollable item in the center of the main axis |
79 | /// and ends with the last scrollable item in the center of the main axis. This |
80 | /// is in contrast to typical lists that start with the first scrollable item |
81 | /// at the start of the main axis and ends with the last scrollable item at the |
82 | /// end of the main axis. |
83 | /// |
84 | /// Instead of rendering its children on a flat plane, it renders them |
85 | /// as if each child is broken into its own plane and that plane is |
86 | /// perpendicularly fixed onto a cylinder which rotates along the scrolling |
87 | /// axis. |
88 | /// |
89 | /// This class works in 3 coordinate systems: |
90 | /// |
91 | /// 1. The **scrollable layout coordinates**. This coordinate system is used to |
92 | /// communicate with [ViewportOffset] and describes its children's abstract |
93 | /// offset from the beginning of the scrollable list at (0.0, 0.0). |
94 | /// |
95 | /// The list is scrollable from the start of the first child item to the |
96 | /// start of the last child item. |
97 | /// |
98 | /// Children's layout coordinates don't change as the viewport scrolls. |
99 | /// |
100 | /// 2. The **untransformed plane's viewport painting coordinates**. Children are |
101 | /// not painted in this coordinate system. It's an abstract intermediary used |
102 | /// before transforming into the next cylindrical coordinate system. |
103 | /// |
104 | /// This system is the **scrollable layout coordinates** translated by the |
105 | /// scroll offset such that (0.0, 0.0) is the top left corner of the |
106 | /// viewport. |
107 | /// |
108 | /// Because the viewport is centered at the scrollable list's scroll offset |
109 | /// instead of starting at the scroll offset, there are paintable children |
110 | /// ~1/2 viewport length before and after the scroll offset instead of ~1 |
111 | /// viewport length after the scroll offset. |
112 | /// |
113 | /// Children's visibility inclusion in the viewport is determined in this |
114 | /// system regardless of the cylinder's properties such as [diameterRatio] |
115 | /// or [perspective]. In other words, a 100px long viewport will always |
116 | /// paint 10-11 visible 10px children if there are enough children in the |
117 | /// viewport. |
118 | /// |
119 | /// 3. The **transformed cylindrical space viewport painting coordinates**. |
120 | /// Children from system 2 get their positions transformed into a cylindrical |
121 | /// projection matrix instead of its Cartesian offset with respect to the |
122 | /// scroll offset. |
123 | /// |
124 | /// Children in this coordinate system are painted. |
125 | /// |
126 | /// The wheel's size and the maximum and minimum visible angles are both |
127 | /// controlled by [diameterRatio]. Children visible in the **untransformed |
128 | /// plane's viewport painting coordinates**'s viewport will be radially |
129 | /// evenly laid out between the maximum and minimum angles determined by |
130 | /// intersecting the viewport's main axis length with a cylinder whose |
131 | /// diameter is [diameterRatio] times longer, as long as those angles are |
132 | /// between -pi/2 and pi/2. |
133 | /// |
134 | /// For example, if [diameterRatio] is 2.0 and this [RenderListWheelViewport] |
135 | /// is 100.0px in the main axis, then the diameter is 200.0. And children |
136 | /// will be evenly laid out between that cylinder's -arcsin(1/2) and |
137 | /// arcsin(1/2) angles. |
138 | /// |
139 | /// The cylinder's 0 degree side is always centered in the |
140 | /// [RenderListWheelViewport]. The transformation from **untransformed |
141 | /// plane's viewport painting coordinates** is also done such that the child |
142 | /// in the center of that plane will be mostly untransformed with children |
143 | /// above and below it being transformed more as the angle increases. |
144 | class RenderListWheelViewport |
145 | extends RenderBox |
146 | with ContainerRenderObjectMixin<RenderBox, ListWheelParentData> |
147 | implements RenderAbstractViewport { |
148 | /// Creates a [RenderListWheelViewport] which renders children on a wheel. |
149 | /// |
150 | /// Optional arguments have reasonable defaults. |
151 | RenderListWheelViewport({ |
152 | required this.childManager, |
153 | required ViewportOffset offset, |
154 | double diameterRatio = defaultDiameterRatio, |
155 | double perspective = defaultPerspective, |
156 | double offAxisFraction = 0, |
157 | bool useMagnifier = false, |
158 | double magnification = 1, |
159 | double overAndUnderCenterOpacity = 1, |
160 | required double itemExtent, |
161 | double squeeze = 1, |
162 | bool renderChildrenOutsideViewport = false, |
163 | Clip clipBehavior = Clip.none, |
164 | List<RenderBox>? children, |
165 | }) : assert(diameterRatio > 0, diameterRatioZeroMessage), |
166 | assert(perspective > 0), |
167 | assert(perspective <= 0.01, perspectiveTooHighMessage), |
168 | assert(magnification > 0), |
169 | assert(overAndUnderCenterOpacity >= 0 && overAndUnderCenterOpacity <= 1), |
170 | assert(squeeze > 0), |
171 | assert(itemExtent > 0), |
172 | assert( |
173 | !renderChildrenOutsideViewport || clipBehavior == Clip.none, |
174 | clipBehaviorAndRenderChildrenOutsideViewportConflict, |
175 | ), |
176 | _offset = offset, |
177 | _diameterRatio = diameterRatio, |
178 | _perspective = perspective, |
179 | _offAxisFraction = offAxisFraction, |
180 | _useMagnifier = useMagnifier, |
181 | _magnification = magnification, |
182 | _overAndUnderCenterOpacity = overAndUnderCenterOpacity, |
183 | _itemExtent = itemExtent, |
184 | _squeeze = squeeze, |
185 | _renderChildrenOutsideViewport = renderChildrenOutsideViewport, |
186 | _clipBehavior = clipBehavior { |
187 | addAll(children); |
188 | } |
189 | |
190 | /// An arbitrary but aesthetically reasonable default value for [diameterRatio]. |
191 | static const double defaultDiameterRatio = 2.0; |
192 | |
193 | /// An arbitrary but aesthetically reasonable default value for [perspective]. |
194 | static const double defaultPerspective = 0.003; |
195 | |
196 | /// An error message to show when the provided [diameterRatio] is zero. |
197 | static const String diameterRatioZeroMessage = "You can't set a diameterRatio " |
198 | 'of 0 or of a negative number. It would imply a cylinder of 0 in diameter ' |
199 | 'in which case nothing will be drawn.' ; |
200 | |
201 | /// An error message to show when the [perspective] value is too high. |
202 | static const String perspectiveTooHighMessage = 'A perspective too high will ' |
203 | 'be clipped in the z-axis and therefore not renderable. Value must be ' |
204 | 'between 0 and 0.01.' ; |
205 | |
206 | /// An error message to show when [clipBehavior] and [renderChildrenOutsideViewport] |
207 | /// are set to conflicting values. |
208 | static const String clipBehaviorAndRenderChildrenOutsideViewportConflict = |
209 | 'Cannot renderChildrenOutsideViewport and clip since children ' |
210 | 'rendered outside will be clipped anyway.' ; |
211 | |
212 | /// The delegate that manages the children of this object. |
213 | /// |
214 | /// This delegate must maintain the [ListWheelParentData.index] value. |
215 | final ListWheelChildManager childManager; |
216 | |
217 | /// The associated ViewportOffset object for the viewport describing the part |
218 | /// of the content inside that's visible. |
219 | /// |
220 | /// The [ViewportOffset.pixels] value determines the scroll offset that the |
221 | /// viewport uses to select which part of its content to display. As the user |
222 | /// scrolls the viewport, this value changes, which changes the content that |
223 | /// is displayed. |
224 | ViewportOffset get offset => _offset; |
225 | ViewportOffset _offset; |
226 | set offset(ViewportOffset value) { |
227 | if (value == _offset) { |
228 | return; |
229 | } |
230 | if (attached) { |
231 | _offset.removeListener(_hasScrolled); |
232 | } |
233 | _offset = value; |
234 | if (attached) { |
235 | _offset.addListener(_hasScrolled); |
236 | } |
237 | markNeedsLayout(); |
238 | } |
239 | |
240 | /// {@template flutter.rendering.RenderListWheelViewport.diameterRatio} |
241 | /// A ratio between the diameter of the cylinder and the viewport's size |
242 | /// in the main axis. |
243 | /// |
244 | /// A value of 1 means the cylinder has the same diameter as the viewport's |
245 | /// size. |
246 | /// |
247 | /// A value smaller than 1 means items at the edges of the cylinder are |
248 | /// entirely contained inside the viewport. |
249 | /// |
250 | /// A value larger than 1 means angles less than ±[pi] / 2 from the |
251 | /// center of the cylinder are visible. |
252 | /// |
253 | /// The same number of children will be visible in the viewport regardless of |
254 | /// the [diameterRatio]. The number of children visible is based on the |
255 | /// viewport's length along the main axis divided by the children's |
256 | /// [itemExtent]. Then the children are evenly distributed along the visible |
257 | /// angles up to ±[pi] / 2. |
258 | /// |
259 | /// Just as it's impossible to stretch a paper to cover the an entire |
260 | /// half of a cylinder's surface where the cylinder has the same diameter |
261 | /// as the paper's length, choosing a [diameterRatio] smaller than [pi] |
262 | /// will leave same gaps between the children. |
263 | /// |
264 | /// Defaults to an arbitrary but aesthetically reasonable number of 2.0. |
265 | /// |
266 | /// Must be a positive number. |
267 | /// {@endtemplate} |
268 | double get diameterRatio => _diameterRatio; |
269 | double _diameterRatio; |
270 | set diameterRatio(double value) { |
271 | assert( |
272 | value > 0, |
273 | diameterRatioZeroMessage, |
274 | ); |
275 | if (value == _diameterRatio) { |
276 | return; |
277 | } |
278 | _diameterRatio = value; |
279 | markNeedsPaint(); |
280 | markNeedsSemanticsUpdate(); |
281 | } |
282 | |
283 | /// {@template flutter.rendering.RenderListWheelViewport.perspective} |
284 | /// Perspective of the cylindrical projection. |
285 | /// |
286 | /// A number between 0 and 0.01 where 0 means looking at the cylinder from |
287 | /// infinitely far with an infinitely small field of view and 1 means looking |
288 | /// at the cylinder from infinitely close with an infinitely large field of |
289 | /// view (which cannot be rendered). |
290 | /// |
291 | /// Defaults to an arbitrary but aesthetically reasonable number of 0.003. |
292 | /// A larger number brings the vanishing point closer and a smaller number |
293 | /// pushes the vanishing point further. |
294 | /// |
295 | /// Must be a positive number. |
296 | /// {@endtemplate} |
297 | double get perspective => _perspective; |
298 | double _perspective; |
299 | set perspective(double value) { |
300 | assert(value > 0); |
301 | assert( |
302 | value <= 0.01, |
303 | perspectiveTooHighMessage, |
304 | ); |
305 | if (value == _perspective) { |
306 | return; |
307 | } |
308 | _perspective = value; |
309 | markNeedsPaint(); |
310 | markNeedsSemanticsUpdate(); |
311 | } |
312 | |
313 | /// {@template flutter.rendering.RenderListWheelViewport.offAxisFraction} |
314 | /// How much the wheel is horizontally off-center, as a fraction of its width. |
315 | |
316 | /// This property creates the visual effect of looking at a vertical wheel from |
317 | /// its side where its vanishing points at the edge curves to one side instead |
318 | /// of looking at the wheel head-on. |
319 | /// |
320 | /// The value is horizontal distance between the wheel's center and the vertical |
321 | /// vanishing line at the edges of the wheel, represented as a fraction of the |
322 | /// wheel's width. |
323 | /// |
324 | /// The value `0.0` means the wheel is looked at head-on and its vanishing |
325 | /// line runs through the center of the wheel. Negative values means moving |
326 | /// the wheel to the left of the observer, thus the edges curve to the right. |
327 | /// Positive values means moving the wheel to the right of the observer, |
328 | /// thus the edges curve to the left. |
329 | /// |
330 | /// The visual effect causes the wheel's edges to curve rather than moving |
331 | /// the center. So a value of `0.5` means the edges' vanishing line will touch |
332 | /// the wheel's size's left edge. |
333 | /// |
334 | /// Defaults to `0.0`, which means looking at the wheel head-on. |
335 | /// The visual effect can be unaesthetic if this value is too far from the |
336 | /// range `[-0.5, 0.5]`. |
337 | /// {@endtemplate} |
338 | double get offAxisFraction => _offAxisFraction; |
339 | double _offAxisFraction = 0.0; |
340 | set offAxisFraction(double value) { |
341 | if (value == _offAxisFraction) { |
342 | return; |
343 | } |
344 | _offAxisFraction = value; |
345 | markNeedsPaint(); |
346 | } |
347 | |
348 | /// {@template flutter.rendering.RenderListWheelViewport.useMagnifier} |
349 | /// Whether to use the magnifier for the center item of the wheel. |
350 | /// {@endtemplate} |
351 | bool get useMagnifier => _useMagnifier; |
352 | bool _useMagnifier = false; |
353 | set useMagnifier(bool value) { |
354 | if (value == _useMagnifier) { |
355 | return; |
356 | } |
357 | _useMagnifier = value; |
358 | markNeedsPaint(); |
359 | } |
360 | /// {@template flutter.rendering.RenderListWheelViewport.magnification} |
361 | /// The zoomed-in rate of the magnifier, if it is used. |
362 | /// |
363 | /// The default value is 1.0, which will not change anything. |
364 | /// If the value is > 1.0, the center item will be zoomed in by that rate, and |
365 | /// it will also be rendered as flat, not cylindrical like the rest of the list. |
366 | /// The item will be zoomed out if magnification < 1.0. |
367 | /// |
368 | /// Must be positive. |
369 | /// {@endtemplate} |
370 | double get magnification => _magnification; |
371 | double _magnification = 1.0; |
372 | set magnification(double value) { |
373 | assert(value > 0); |
374 | if (value == _magnification) { |
375 | return; |
376 | } |
377 | _magnification = value; |
378 | markNeedsPaint(); |
379 | } |
380 | |
381 | /// {@template flutter.rendering.RenderListWheelViewport.overAndUnderCenterOpacity} |
382 | /// The opacity value that will be applied to the wheel that appears below and |
383 | /// above the magnifier. |
384 | /// |
385 | /// The default value is 1.0, which will not change anything. |
386 | /// |
387 | /// Must be greater than or equal to 0, and less than or equal to 1. |
388 | /// {@endtemplate} |
389 | double get overAndUnderCenterOpacity => _overAndUnderCenterOpacity; |
390 | double _overAndUnderCenterOpacity = 1.0; |
391 | set overAndUnderCenterOpacity(double value) { |
392 | assert(value >= 0 && value <= 1); |
393 | if (value == _overAndUnderCenterOpacity) { |
394 | return; |
395 | } |
396 | _overAndUnderCenterOpacity = value; |
397 | markNeedsPaint(); |
398 | } |
399 | |
400 | /// {@template flutter.rendering.RenderListWheelViewport.itemExtent} |
401 | /// The size of the children along the main axis. Children [RenderBox]es will |
402 | /// be given the [BoxConstraints] of this exact size. |
403 | /// |
404 | /// Must be a positive number. |
405 | /// {@endtemplate} |
406 | double get itemExtent => _itemExtent; |
407 | double _itemExtent; |
408 | set itemExtent(double value) { |
409 | assert(value > 0); |
410 | if (value == _itemExtent) { |
411 | return; |
412 | } |
413 | _itemExtent = value; |
414 | markNeedsLayout(); |
415 | } |
416 | |
417 | |
418 | /// {@template flutter.rendering.RenderListWheelViewport.squeeze} |
419 | /// The angular compactness of the children on the wheel. |
420 | /// |
421 | /// This denotes a ratio of the number of children on the wheel vs the number |
422 | /// of children that would fit on a flat list of equivalent size, assuming |
423 | /// [diameterRatio] of 1. |
424 | /// |
425 | /// For instance, if this RenderListWheelViewport has a height of 100px and |
426 | /// [itemExtent] is 20px, 5 items would fit on an equivalent flat list. |
427 | /// With a [squeeze] of 1, 5 items would also be shown in the |
428 | /// RenderListWheelViewport. With a [squeeze] of 2, 10 items would be shown |
429 | /// in the RenderListWheelViewport. |
430 | /// |
431 | /// Changing this value will change the number of children built and shown |
432 | /// inside the wheel. |
433 | /// |
434 | /// Must be a positive number. |
435 | /// {@endtemplate} |
436 | /// |
437 | /// Defaults to 1. |
438 | double get squeeze => _squeeze; |
439 | double _squeeze; |
440 | set squeeze(double value) { |
441 | assert(value > 0); |
442 | if (value == _squeeze) { |
443 | return; |
444 | } |
445 | _squeeze = value; |
446 | markNeedsLayout(); |
447 | markNeedsSemanticsUpdate(); |
448 | } |
449 | |
450 | /// {@template flutter.rendering.RenderListWheelViewport.renderChildrenOutsideViewport} |
451 | /// Whether to paint children inside the viewport only. |
452 | /// |
453 | /// If false, every child will be painted. However the [Scrollable] is still |
454 | /// the size of the viewport and detects gestures inside only. |
455 | /// |
456 | /// Defaults to false. Cannot be true if [clipBehavior] is not [Clip.none] |
457 | /// since children outside the viewport will be clipped, and therefore cannot |
458 | /// render children outside the viewport. |
459 | /// {@endtemplate} |
460 | bool get renderChildrenOutsideViewport => _renderChildrenOutsideViewport; |
461 | bool _renderChildrenOutsideViewport; |
462 | set renderChildrenOutsideViewport(bool value) { |
463 | assert( |
464 | !renderChildrenOutsideViewport || clipBehavior == Clip.none, |
465 | clipBehaviorAndRenderChildrenOutsideViewportConflict, |
466 | ); |
467 | if (value == _renderChildrenOutsideViewport) { |
468 | return; |
469 | } |
470 | _renderChildrenOutsideViewport = value; |
471 | markNeedsLayout(); |
472 | markNeedsSemanticsUpdate(); |
473 | } |
474 | |
475 | /// {@macro flutter.material.Material.clipBehavior} |
476 | /// |
477 | /// Defaults to [Clip.hardEdge]. |
478 | Clip get clipBehavior => _clipBehavior; |
479 | Clip _clipBehavior = Clip.hardEdge; |
480 | set clipBehavior(Clip value) { |
481 | if (value != _clipBehavior) { |
482 | _clipBehavior = value; |
483 | markNeedsPaint(); |
484 | markNeedsSemanticsUpdate(); |
485 | } |
486 | } |
487 | |
488 | void _hasScrolled() { |
489 | markNeedsLayout(); |
490 | markNeedsSemanticsUpdate(); |
491 | } |
492 | |
493 | @override |
494 | void setupParentData(RenderObject child) { |
495 | if (child.parentData is! ListWheelParentData) { |
496 | child.parentData = ListWheelParentData(); |
497 | } |
498 | } |
499 | |
500 | @override |
501 | void attach(PipelineOwner owner) { |
502 | super.attach(owner); |
503 | _offset.addListener(_hasScrolled); |
504 | } |
505 | |
506 | @override |
507 | void detach() { |
508 | _offset.removeListener(_hasScrolled); |
509 | super.detach(); |
510 | } |
511 | |
512 | @override |
513 | bool get isRepaintBoundary => true; |
514 | |
515 | /// Main axis length in the untransformed plane. |
516 | double get _viewportExtent { |
517 | assert(hasSize); |
518 | return size.height; |
519 | } |
520 | |
521 | /// Main axis scroll extent in the **scrollable layout coordinates** that puts |
522 | /// the first item in the center. |
523 | double get _minEstimatedScrollExtent { |
524 | assert(hasSize); |
525 | if (childManager.childCount == null) { |
526 | return double.negativeInfinity; |
527 | } |
528 | return 0.0; |
529 | } |
530 | |
531 | /// Main axis scroll extent in the **scrollable layout coordinates** that puts |
532 | /// the last item in the center. |
533 | double get _maxEstimatedScrollExtent { |
534 | assert(hasSize); |
535 | if (childManager.childCount == null) { |
536 | return double.infinity; |
537 | } |
538 | |
539 | return math.max(0.0, (childManager.childCount! - 1) * _itemExtent); |
540 | } |
541 | |
542 | /// Scroll extent distance in the untransformed plane between the center |
543 | /// position in the viewport and the top position in the viewport. |
544 | /// |
545 | /// It's also the distance in the untransformed plane that children's painting |
546 | /// is offset by with respect to those children's [BoxParentData.offset]. |
547 | double get _topScrollMarginExtent { |
548 | assert(hasSize); |
549 | // Consider adding alignment options other than center. |
550 | return -size.height / 2.0 + _itemExtent / 2.0; |
551 | } |
552 | |
553 | /// Transforms a **scrollable layout coordinates**' y position to the |
554 | /// **untransformed plane's viewport painting coordinates**' y position given |
555 | /// the current scroll offset. |
556 | double _getUntransformedPaintingCoordinateY(double layoutCoordinateY) { |
557 | return layoutCoordinateY - _topScrollMarginExtent - offset.pixels; |
558 | } |
559 | |
560 | /// Given the _diameterRatio, return the largest absolute angle of the item |
561 | /// at the edge of the portion of the visible cylinder. |
562 | /// |
563 | /// For a _diameterRatio of 1 or less than 1 (i.e. the viewport is bigger |
564 | /// than the cylinder diameter), this value reaches and clips at pi / 2. |
565 | /// |
566 | /// When the center of children passes this angle, they are no longer painted |
567 | /// if [renderChildrenOutsideViewport] is false. |
568 | double get _maxVisibleRadian { |
569 | if (_diameterRatio < 1.0) { |
570 | return math.pi / 2.0; |
571 | } |
572 | return math.asin(1.0 / _diameterRatio); |
573 | } |
574 | |
575 | double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) { |
576 | double extent = 0.0; |
577 | RenderBox? child = firstChild; |
578 | while (child != null) { |
579 | extent = math.max(extent, childSize(child)); |
580 | child = childAfter(child); |
581 | } |
582 | return extent; |
583 | } |
584 | |
585 | @override |
586 | double computeMinIntrinsicWidth(double height) { |
587 | return _getIntrinsicCrossAxis( |
588 | (RenderBox child) => child.getMinIntrinsicWidth(height), |
589 | ); |
590 | } |
591 | |
592 | @override |
593 | double computeMaxIntrinsicWidth(double height) { |
594 | return _getIntrinsicCrossAxis( |
595 | (RenderBox child) => child.getMaxIntrinsicWidth(height), |
596 | ); |
597 | } |
598 | |
599 | @override |
600 | double computeMinIntrinsicHeight(double width) { |
601 | if (childManager.childCount == null) { |
602 | return 0.0; |
603 | } |
604 | return childManager.childCount! * _itemExtent; |
605 | } |
606 | |
607 | @override |
608 | double computeMaxIntrinsicHeight(double width) { |
609 | if (childManager.childCount == null) { |
610 | return 0.0; |
611 | } |
612 | return childManager.childCount! * _itemExtent; |
613 | } |
614 | |
615 | @override |
616 | bool get sizedByParent => true; |
617 | |
618 | @override |
619 | @protected |
620 | Size computeDryLayout(covariant BoxConstraints constraints) { |
621 | return constraints.biggest; |
622 | } |
623 | |
624 | /// Gets the index of a child by looking at its [parentData]. |
625 | /// |
626 | /// This relies on the [childManager] maintaining [ListWheelParentData.index]. |
627 | int indexOf(RenderBox child) { |
628 | final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; |
629 | assert(childParentData.index != null); |
630 | return childParentData.index!; |
631 | } |
632 | |
633 | /// Returns the index of the child at the given offset. |
634 | int scrollOffsetToIndex(double scrollOffset) => (scrollOffset / itemExtent).floor(); |
635 | |
636 | /// Returns the scroll offset of the child with the given index. |
637 | double indexToScrollOffset(int index) => index * itemExtent; |
638 | |
639 | void _createChild(int index, { RenderBox? after }) { |
640 | invokeLayoutCallback<BoxConstraints>((BoxConstraints constraints) { |
641 | assert(constraints == this.constraints); |
642 | childManager.createChild(index, after: after); |
643 | }); |
644 | } |
645 | |
646 | void _destroyChild(RenderBox child) { |
647 | invokeLayoutCallback<BoxConstraints>((BoxConstraints constraints) { |
648 | assert(constraints == this.constraints); |
649 | childManager.removeChild(child); |
650 | }); |
651 | } |
652 | |
653 | void _layoutChild(RenderBox child, BoxConstraints constraints, int index) { |
654 | child.layout(constraints, parentUsesSize: true); |
655 | final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; |
656 | // Centers the child horizontally. |
657 | final double crossPosition = size.width / 2.0 - child.size.width / 2.0; |
658 | childParentData.offset = Offset(crossPosition, indexToScrollOffset(index)); |
659 | } |
660 | |
661 | /// Performs layout based on how [childManager] provides children. |
662 | /// |
663 | /// From the current scroll offset, the minimum index and maximum index that |
664 | /// is visible in the viewport can be calculated. The index range of the |
665 | /// currently active children can also be acquired by looking directly at |
666 | /// the current child list. This function has to modify the current index |
667 | /// range to match the target index range by removing children that are no |
668 | /// longer visible and creating those that are visible but not yet provided |
669 | /// by [childManager]. |
670 | @override |
671 | void performLayout() { |
672 | offset.applyViewportDimension(_viewportExtent); |
673 | // Apply the content dimensions first if it has exact dimensions in case it |
674 | // changes the scroll offset which determines what should be shown. Such as |
675 | // if the child count decrease, we should correct the pixels first, otherwise, |
676 | // it may be shown blank null children. |
677 | if (childManager.childCount != null) { |
678 | offset.applyContentDimensions(_minEstimatedScrollExtent, _maxEstimatedScrollExtent); |
679 | } |
680 | |
681 | // The height, in pixel, that children will be visible and might be laid out |
682 | // and painted. |
683 | double visibleHeight = size.height * _squeeze; |
684 | // If renderChildrenOutsideViewport is true, we spawn extra children by |
685 | // doubling the visibility range, those that are in the backside of the |
686 | // cylinder won't be painted anyway. |
687 | if (renderChildrenOutsideViewport) { |
688 | visibleHeight *= 2; |
689 | } |
690 | |
691 | final double firstVisibleOffset = |
692 | offset.pixels + _itemExtent / 2 - visibleHeight / 2; |
693 | final double lastVisibleOffset = firstVisibleOffset + visibleHeight; |
694 | |
695 | // The index range that we want to spawn children. We find indexes that |
696 | // are in the interval [firstVisibleOffset, lastVisibleOffset). |
697 | int targetFirstIndex = scrollOffsetToIndex(firstVisibleOffset); |
698 | int targetLastIndex = scrollOffsetToIndex(lastVisibleOffset); |
699 | // Because we exclude lastVisibleOffset, if there's a new child starting at |
700 | // that offset, it is removed. |
701 | if (targetLastIndex * _itemExtent == lastVisibleOffset) { |
702 | targetLastIndex--; |
703 | } |
704 | |
705 | // Validates the target index range. |
706 | while (!childManager.childExistsAt(targetFirstIndex) && targetFirstIndex <= targetLastIndex) { |
707 | targetFirstIndex++; |
708 | } |
709 | while (!childManager.childExistsAt(targetLastIndex) && targetFirstIndex <= targetLastIndex) { |
710 | targetLastIndex--; |
711 | } |
712 | |
713 | // If it turns out there's no children to layout, we remove old children and |
714 | // return. |
715 | if (targetFirstIndex > targetLastIndex) { |
716 | while (firstChild != null) { |
717 | _destroyChild(firstChild!); |
718 | } |
719 | return; |
720 | } |
721 | |
722 | // Now there are 2 cases: |
723 | // - The target index range and our current index range have intersection: |
724 | // We shorten and extend our current child list so that the two lists |
725 | // match. Most of the time we are in this case. |
726 | // - The target list and our current child list have no intersection: |
727 | // We first remove all children and then add one child from the target |
728 | // list => this case becomes the other case. |
729 | |
730 | // Case when there is no intersection. |
731 | if (childCount > 0 && |
732 | (indexOf(firstChild!) > targetLastIndex || indexOf(lastChild!) < targetFirstIndex)) { |
733 | while (firstChild != null) { |
734 | _destroyChild(firstChild!); |
735 | } |
736 | } |
737 | |
738 | final BoxConstraints childConstraints = constraints.copyWith( |
739 | minHeight: _itemExtent, |
740 | maxHeight: _itemExtent, |
741 | minWidth: 0.0, |
742 | ); |
743 | // If there is no child at this stage, we add the first one that is in |
744 | // target range. |
745 | if (childCount == 0) { |
746 | _createChild(targetFirstIndex); |
747 | _layoutChild(firstChild!, childConstraints, targetFirstIndex); |
748 | } |
749 | |
750 | int currentFirstIndex = indexOf(firstChild!); |
751 | int currentLastIndex = indexOf(lastChild!); |
752 | |
753 | // Remove all unnecessary children by shortening the current child list, in |
754 | // both directions. |
755 | while (currentFirstIndex < targetFirstIndex) { |
756 | _destroyChild(firstChild!); |
757 | currentFirstIndex++; |
758 | } |
759 | while (currentLastIndex > targetLastIndex) { |
760 | _destroyChild(lastChild!); |
761 | currentLastIndex--; |
762 | } |
763 | |
764 | // Relayout all active children. |
765 | RenderBox? child = firstChild; |
766 | int index = currentFirstIndex; |
767 | while (child != null) { |
768 | _layoutChild(child, childConstraints, index++); |
769 | child = childAfter(child); |
770 | } |
771 | |
772 | // Spawning new children that are actually visible but not in child list yet. |
773 | while (currentFirstIndex > targetFirstIndex) { |
774 | _createChild(currentFirstIndex - 1); |
775 | _layoutChild(firstChild!, childConstraints, --currentFirstIndex); |
776 | } |
777 | while (currentLastIndex < targetLastIndex) { |
778 | _createChild(currentLastIndex + 1, after: lastChild); |
779 | _layoutChild(lastChild!, childConstraints, ++currentLastIndex); |
780 | } |
781 | |
782 | // Applying content dimensions bases on how the childManager builds widgets: |
783 | // if it is available to provide a child just out of target range, then |
784 | // we don't know whether there's a limit yet, and set the dimension to the |
785 | // estimated value. Otherwise, we set the dimension limited to our target |
786 | // range. |
787 | final double minScrollExtent = childManager.childExistsAt(targetFirstIndex - 1) |
788 | ? _minEstimatedScrollExtent |
789 | : indexToScrollOffset(targetFirstIndex); |
790 | final double maxScrollExtent = childManager.childExistsAt(targetLastIndex + 1) |
791 | ? _maxEstimatedScrollExtent |
792 | : indexToScrollOffset(targetLastIndex); |
793 | offset.applyContentDimensions(minScrollExtent, maxScrollExtent); |
794 | } |
795 | |
796 | bool _shouldClipAtCurrentOffset() { |
797 | final double highestUntransformedPaintY = |
798 | _getUntransformedPaintingCoordinateY(0.0); |
799 | return highestUntransformedPaintY < 0.0 |
800 | || size.height < highestUntransformedPaintY + _maxEstimatedScrollExtent + _itemExtent; |
801 | } |
802 | |
803 | @override |
804 | void paint(PaintingContext context, Offset offset) { |
805 | if (childCount > 0) { |
806 | if (_shouldClipAtCurrentOffset() && clipBehavior != Clip.none) { |
807 | _clipRectLayer.layer = context.pushClipRect( |
808 | needsCompositing, |
809 | offset, |
810 | Offset.zero & size, |
811 | _paintVisibleChildren, |
812 | clipBehavior: clipBehavior, |
813 | oldLayer: _clipRectLayer.layer, |
814 | ); |
815 | } else { |
816 | _clipRectLayer.layer = null; |
817 | _paintVisibleChildren(context, offset); |
818 | } |
819 | } |
820 | } |
821 | |
822 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
823 | |
824 | @override |
825 | void dispose() { |
826 | _clipRectLayer.layer = null; |
827 | _childOpacityLayerHandler.layer = null; |
828 | super.dispose(); |
829 | } |
830 | final LayerHandle<OpacityLayer> _childOpacityLayerHandler = LayerHandle<OpacityLayer>(); |
831 | /// Paints all children visible in the current viewport. |
832 | void _paintVisibleChildren(PaintingContext context, Offset offset) { |
833 | // The magnifier cannot be turned off if the opacity is less than 1.0. |
834 | if (overAndUnderCenterOpacity >= 1) { |
835 | _paintAllChildren(context, offset); |
836 | return; |
837 | } |
838 | |
839 | // In order to reduce the number of opacity layers, we first paint all |
840 | // partially opaque children, then finally paint the fully opaque children. |
841 | _childOpacityLayerHandler.layer = context.pushOpacity(offset, (overAndUnderCenterOpacity * 255).round(), (PaintingContext context, Offset offset) { |
842 | _paintAllChildren(context, offset, center: false); |
843 | }); |
844 | _paintAllChildren(context, offset, center: true); |
845 | } |
846 | |
847 | void _paintAllChildren(PaintingContext context, Offset offset, { bool? center }) { |
848 | RenderBox? childToPaint = firstChild; |
849 | while (childToPaint != null) { |
850 | final ListWheelParentData childParentData = childToPaint.parentData! as ListWheelParentData; |
851 | _paintTransformedChild(childToPaint, context, offset, childParentData.offset, center: center); |
852 | childToPaint = childAfter(childToPaint); |
853 | } |
854 | } |
855 | |
856 | // Takes in a child with a **scrollable layout offset** and paints it in the |
857 | // **transformed cylindrical space viewport painting coordinates**. |
858 | // |
859 | // The value of `center` is passed through to _paintChildWithMagnifier only |
860 | // if the magnifier is enabled and/or opacity is < 1.0. |
861 | void _paintTransformedChild( |
862 | RenderBox child, |
863 | PaintingContext context, |
864 | Offset offset, |
865 | Offset layoutOffset, { |
866 | required bool? center, |
867 | }) { |
868 | final Offset untransformedPaintingCoordinates = offset |
869 | + Offset( |
870 | layoutOffset.dx, |
871 | _getUntransformedPaintingCoordinateY(layoutOffset.dy), |
872 | ); |
873 | |
874 | // Get child's center as a fraction of the viewport's height. |
875 | final double fractionalY = |
876 | (untransformedPaintingCoordinates.dy + _itemExtent / 2.0) / size.height; |
877 | final double angle = -(fractionalY - 0.5) * 2.0 * _maxVisibleRadian / squeeze; |
878 | // Don't paint the backside of the cylinder when |
879 | // renderChildrenOutsideViewport is true. Otherwise, only children within |
880 | // suitable angles (via _first/lastVisibleLayoutOffset) reach the paint |
881 | // phase. |
882 | if (angle > math.pi / 2.0 || angle < -math.pi / 2.0) { |
883 | return; |
884 | } |
885 | |
886 | final Matrix4 transform = MatrixUtils.createCylindricalProjectionTransform( |
887 | radius: size.height * _diameterRatio / 2.0, |
888 | angle: angle, |
889 | perspective: _perspective, |
890 | ); |
891 | |
892 | // Offset that helps painting everything in the center (e.g. angle = 0). |
893 | final Offset offsetToCenter = Offset( |
894 | untransformedPaintingCoordinates.dx, |
895 | -_topScrollMarginExtent, |
896 | ); |
897 | |
898 | final bool shouldApplyOffCenterDim = overAndUnderCenterOpacity < 1; |
899 | if (useMagnifier || shouldApplyOffCenterDim) { |
900 | _paintChildWithMagnifier(context, offset, child, transform, offsetToCenter, untransformedPaintingCoordinates, center: center); |
901 | } else { |
902 | assert(center == null); |
903 | _paintChildCylindrically(context, offset, child, transform, offsetToCenter); |
904 | } |
905 | } |
906 | |
907 | // Paint child with the magnifier active - the child will be rendered |
908 | // differently if it intersects with the magnifier. |
909 | // |
910 | // `center` controls how items that partially intersect the center magnifier |
911 | // are rendered. If `center` is false, items are only painted cynlindrically. |
912 | // If `center` is true, only the clipped magnifier items are painted. |
913 | // If `center` is null, partially intersecting items are painted both as the |
914 | // magnifier and cynlidrical item, while non-intersecting items are painted |
915 | // only cylindrically. |
916 | // |
917 | // This property is used to lift the opacity that would be applied to each |
918 | // cylindrical item into a single layer, reducing the rendering cost of the |
919 | // pickers which use this viewport. |
920 | void _paintChildWithMagnifier( |
921 | PaintingContext context, |
922 | Offset offset, |
923 | RenderBox child, |
924 | Matrix4 cylindricalTransform, |
925 | Offset offsetToCenter, |
926 | Offset untransformedPaintingCoordinates, { |
927 | required bool? center, |
928 | }) { |
929 | final double magnifierTopLinePosition = |
930 | size.height / 2 - _itemExtent * _magnification / 2; |
931 | final double magnifierBottomLinePosition = |
932 | size.height / 2 + _itemExtent * _magnification / 2; |
933 | |
934 | final bool isAfterMagnifierTopLine = untransformedPaintingCoordinates.dy |
935 | >= magnifierTopLinePosition - _itemExtent * _magnification; |
936 | final bool isBeforeMagnifierBottomLine = untransformedPaintingCoordinates.dy |
937 | <= magnifierBottomLinePosition; |
938 | |
939 | final Rect centerRect = Rect.fromLTWH( |
940 | 0.0, |
941 | magnifierTopLinePosition, |
942 | size.width, |
943 | _itemExtent * _magnification, |
944 | ); |
945 | final Rect topHalfRect = Rect.fromLTWH( |
946 | 0.0, |
947 | 0.0, |
948 | size.width, |
949 | magnifierTopLinePosition, |
950 | ); |
951 | final Rect bottomHalfRect = Rect.fromLTWH( |
952 | 0.0, |
953 | magnifierBottomLinePosition, |
954 | size.width, |
955 | magnifierTopLinePosition, |
956 | ); |
957 | // Some part of the child is in the center magnifier. |
958 | final bool inCenter = isAfterMagnifierTopLine && isBeforeMagnifierBottomLine; |
959 | |
960 | if ((center == null || center) && inCenter) { |
961 | // Clipping the part in the center. |
962 | context.pushClipRect( |
963 | needsCompositing, |
964 | offset, |
965 | centerRect, |
966 | (PaintingContext context, Offset offset) { |
967 | context.pushTransform( |
968 | needsCompositing, |
969 | offset, |
970 | _magnifyTransform(), |
971 | (PaintingContext context, Offset offset) { |
972 | context.paintChild(child, offset + untransformedPaintingCoordinates); |
973 | }, |
974 | ); |
975 | }, |
976 | ); |
977 | } |
978 | |
979 | // Clipping the part in either the top-half or bottom-half of the wheel. |
980 | if ((center == null || !center) && inCenter) { |
981 | context.pushClipRect( |
982 | needsCompositing, |
983 | offset, |
984 | untransformedPaintingCoordinates.dy <= magnifierTopLinePosition |
985 | ? topHalfRect |
986 | : bottomHalfRect, |
987 | (PaintingContext context, Offset offset) { |
988 | _paintChildCylindrically( |
989 | context, |
990 | offset, |
991 | child, |
992 | cylindricalTransform, |
993 | offsetToCenter, |
994 | ); |
995 | }, |
996 | ); |
997 | } |
998 | |
999 | if ((center == null || !center) && !inCenter) { |
1000 | _paintChildCylindrically( |
1001 | context, |
1002 | offset, |
1003 | child, |
1004 | cylindricalTransform, |
1005 | offsetToCenter, |
1006 | ); |
1007 | } |
1008 | } |
1009 | |
1010 | // / Paint the child cylindrically at given offset. |
1011 | void _paintChildCylindrically( |
1012 | PaintingContext context, |
1013 | Offset offset, |
1014 | RenderBox child, |
1015 | Matrix4 cylindricalTransform, |
1016 | Offset offsetToCenter, |
1017 | ) { |
1018 | final Offset paintOriginOffset = offset + offsetToCenter; |
1019 | |
1020 | // Paint child cylindrically, without [overAndUnderCenterOpacity]. |
1021 | void painter(PaintingContext context, Offset offset) { |
1022 | context.paintChild( |
1023 | child, |
1024 | // Paint everything in the center (e.g. angle = 0), then transform. |
1025 | paintOriginOffset, |
1026 | ); |
1027 | } |
1028 | |
1029 | context.pushTransform( |
1030 | needsCompositing, |
1031 | offset, |
1032 | _centerOriginTransform(cylindricalTransform), |
1033 | // Pre-transform painting function. |
1034 | painter, |
1035 | ); |
1036 | |
1037 | final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; |
1038 | // Save the final transform that accounts both for the offset and cylindrical transform. |
1039 | final Matrix4 transform = _centerOriginTransform(cylindricalTransform) |
1040 | ..translate(paintOriginOffset.dx, paintOriginOffset.dy); |
1041 | childParentData.transform = transform; |
1042 | } |
1043 | |
1044 | /// Return the Matrix4 transformation that would zoom in content in the |
1045 | /// magnified area. |
1046 | Matrix4 _magnifyTransform() { |
1047 | final Matrix4 magnify = Matrix4.identity(); |
1048 | magnify.translate(size.width * (-_offAxisFraction + 0.5), size.height / 2); |
1049 | magnify.scale(_magnification, _magnification, _magnification); |
1050 | magnify.translate(-size.width * (-_offAxisFraction + 0.5), -size.height / 2); |
1051 | return magnify; |
1052 | } |
1053 | |
1054 | /// Apply incoming transformation with the transformation's origin at the |
1055 | /// viewport's center or horizontally off to the side based on offAxisFraction. |
1056 | Matrix4 _centerOriginTransform(Matrix4 originalMatrix) { |
1057 | final Matrix4 result = Matrix4.identity(); |
1058 | final Offset centerOriginTranslation = Alignment.center.alongSize(size); |
1059 | result.translate( |
1060 | centerOriginTranslation.dx * (-_offAxisFraction * 2 + 1), |
1061 | centerOriginTranslation.dy, |
1062 | ); |
1063 | result.multiply(originalMatrix); |
1064 | result.translate( |
1065 | -centerOriginTranslation.dx * (-_offAxisFraction * 2 + 1), |
1066 | -centerOriginTranslation.dy, |
1067 | ); |
1068 | return result; |
1069 | } |
1070 | |
1071 | static bool _debugAssertValidHitTestOffsets(String context, Offset offset1, Offset offset2) { |
1072 | if (offset1 != offset2) { |
1073 | throw FlutterError(" $context - hit test expected values didn't match: $offset1 != $offset2" ); |
1074 | } |
1075 | return true; |
1076 | } |
1077 | |
1078 | @override |
1079 | void applyPaintTransform(RenderBox child, Matrix4 transform) { |
1080 | final ListWheelParentData parentData = child.parentData! as ListWheelParentData; |
1081 | final Matrix4? paintTransform = parentData.transform; |
1082 | if (paintTransform != null) { |
1083 | transform.multiply(paintTransform); |
1084 | } |
1085 | } |
1086 | |
1087 | @override |
1088 | Rect? describeApproximatePaintClip(RenderObject child) { |
1089 | if (_shouldClipAtCurrentOffset()) { |
1090 | return Offset.zero & size; |
1091 | } |
1092 | return null; |
1093 | } |
1094 | |
1095 | @override |
1096 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
1097 | RenderBox? child = lastChild; |
1098 | while (child != null) { |
1099 | final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; |
1100 | final Matrix4? transform = childParentData.transform; |
1101 | // Skip not painted children |
1102 | if (transform != null) { |
1103 | final bool isHit = result.addWithPaintTransform( |
1104 | transform: transform, |
1105 | position: position, |
1106 | hitTest: (BoxHitTestResult result, Offset transformed) { |
1107 | assert(() { |
1108 | final Matrix4? inverted = Matrix4.tryInvert(PointerEvent.removePerspectiveTransform(transform)); |
1109 | if (inverted == null) { |
1110 | return _debugAssertValidHitTestOffsets('Null inverted transform' , transformed, position); |
1111 | } |
1112 | return _debugAssertValidHitTestOffsets('MatrixUtils.transformPoint' , transformed, MatrixUtils.transformPoint(inverted, position)); |
1113 | }()); |
1114 | return child!.hitTest(result, position: transformed); |
1115 | }, |
1116 | ); |
1117 | if (isHit) { |
1118 | return true; |
1119 | } |
1120 | } |
1121 | child = childParentData.previousSibling; |
1122 | } |
1123 | return false; |
1124 | } |
1125 | |
1126 | @override |
1127 | RevealedOffset getOffsetToReveal( |
1128 | RenderObject target, |
1129 | double alignment, { |
1130 | Rect? rect, |
1131 | Axis? axis, // Unused, only Axis.vertical supported by this viewport. |
1132 | }) { |
1133 | // `target` is only fully revealed when in the selected/center position. Therefore, |
1134 | // this method always returns the offset that shows `target` in the center position, |
1135 | // which is the same offset for all `alignment` values. |
1136 | rect ??= target.paintBounds; |
1137 | |
1138 | // `child` will be the last RenderObject before the viewport when walking up from `target`. |
1139 | RenderObject child = target; |
1140 | while (child.parent != this) { |
1141 | child = child.parent!; |
1142 | } |
1143 | |
1144 | final ListWheelParentData parentData = child.parentData! as ListWheelParentData; |
1145 | final double targetOffset = parentData.offset.dy; // the so-called "centerPosition" |
1146 | |
1147 | final Matrix4 transform = target.getTransformTo(child); |
1148 | final Rect bounds = MatrixUtils.transformRect(transform, rect); |
1149 | final Rect targetRect = bounds.translate(0.0, (size.height - itemExtent) / 2); |
1150 | |
1151 | return RevealedOffset(offset: targetOffset, rect: targetRect); |
1152 | } |
1153 | |
1154 | @override |
1155 | void showOnScreen({ |
1156 | RenderObject? descendant, |
1157 | Rect? rect, |
1158 | Duration duration = Duration.zero, |
1159 | Curve curve = Curves.ease, |
1160 | }) { |
1161 | if (descendant != null) { |
1162 | // Shows the descendant in the selected/center position. |
1163 | final RevealedOffset revealedOffset = getOffsetToReveal(descendant, 0.5, rect: rect); |
1164 | if (duration == Duration.zero) { |
1165 | offset.jumpTo(revealedOffset.offset); |
1166 | } else { |
1167 | offset.animateTo(revealedOffset.offset, duration: duration, curve: curve); |
1168 | } |
1169 | rect = revealedOffset.rect; |
1170 | } |
1171 | |
1172 | super.showOnScreen( |
1173 | rect: rect, |
1174 | duration: duration, |
1175 | curve: curve, |
1176 | ); |
1177 | } |
1178 | } |
1179 | |