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/foundation.dart'; |
8 | import 'package:flutter/gestures.dart'; |
9 | |
10 | import 'box.dart'; |
11 | import 'debug.dart'; |
12 | import 'object.dart'; |
13 | import 'viewport.dart'; |
14 | import 'viewport_offset.dart'; |
15 | |
16 | // CORE TYPES FOR SLIVERS |
17 | // The RenderSliver base class and its helper types. |
18 | |
19 | /// Called to get the item extent by the index of item. |
20 | /// |
21 | /// Used by [ListView.itemExtentBuilder] and [SliverVariedExtentList.itemExtentBuilder]. |
22 | typedef ItemExtentBuilder = double Function(int index, SliverLayoutDimensions dimensions); |
23 | |
24 | /// Relates the dimensions of the [RenderSliver] during layout. |
25 | /// |
26 | /// Used by [ListView.itemExtentBuilder] and [SliverVariedExtentList.itemExtentBuilder]. |
27 | @immutable |
28 | class SliverLayoutDimensions { |
29 | /// Constructs a [SliverLayoutDimensions] with the specified parameters. |
30 | const SliverLayoutDimensions({ |
31 | required this.scrollOffset, |
32 | required this.precedingScrollExtent, |
33 | required this.viewportMainAxisExtent, |
34 | required this.crossAxisExtent |
35 | }); |
36 | |
37 | /// {@macro flutter.rendering.SliverConstraints.scrollOffset} |
38 | final double scrollOffset; |
39 | |
40 | /// {@macro flutter.rendering.SliverConstraints.precedingScrollExtent} |
41 | final double precedingScrollExtent; |
42 | |
43 | /// The number of pixels the viewport can display in the main axis. |
44 | /// |
45 | /// For a vertical list, this is the height of the viewport. |
46 | final double viewportMainAxisExtent; |
47 | |
48 | /// The number of pixels in the cross-axis. |
49 | /// |
50 | /// For a vertical list, this is the width of the sliver. |
51 | final double crossAxisExtent; |
52 | |
53 | @override |
54 | bool operator ==(Object other) { |
55 | if (identical(this, other)) { |
56 | return true; |
57 | } |
58 | if (other is! SliverLayoutDimensions) { |
59 | return false; |
60 | } |
61 | return other.scrollOffset == scrollOffset && |
62 | other.precedingScrollExtent == precedingScrollExtent && |
63 | other.viewportMainAxisExtent == viewportMainAxisExtent && |
64 | other.crossAxisExtent == crossAxisExtent; |
65 | } |
66 | |
67 | @override |
68 | String toString() { |
69 | return 'scrollOffset: $scrollOffset' |
70 | ' precedingScrollExtent: $precedingScrollExtent' |
71 | ' viewportMainAxisExtent: $viewportMainAxisExtent' |
72 | ' crossAxisExtent: $crossAxisExtent' ; |
73 | } |
74 | |
75 | @override |
76 | int get hashCode => Object.hash( |
77 | scrollOffset, |
78 | precedingScrollExtent, |
79 | viewportMainAxisExtent, |
80 | viewportMainAxisExtent |
81 | ); |
82 | } |
83 | |
84 | /// The direction in which a sliver's contents are ordered, relative to the |
85 | /// scroll offset axis. |
86 | /// |
87 | /// For example, a vertical alphabetical list that is going [AxisDirection.down] |
88 | /// with a [GrowthDirection.forward] would have the A at the top and the Z at |
89 | /// the bottom, with the A adjacent to the origin, as would such a list going |
90 | /// [AxisDirection.up] with a [GrowthDirection.reverse]. On the other hand, a |
91 | /// vertical alphabetical list that is going [AxisDirection.down] with a |
92 | /// [GrowthDirection.reverse] would have the Z at the top (at scroll offset |
93 | /// zero) and the A below it. |
94 | /// |
95 | /// {@template flutter.rendering.GrowthDirection.sample} |
96 | /// Most scroll views by default are ordered [GrowthDirection.forward]. |
97 | /// Changing the default values of [ScrollView.anchor], |
98 | /// [ScrollView.center], or both, can configure a scroll view for |
99 | /// [GrowthDirection.reverse]. |
100 | /// |
101 | /// {@tool dartpad} |
102 | /// This sample shows a [CustomScrollView], with [Radio] buttons in the |
103 | /// [AppBar.bottom] that change the [AxisDirection] to illustrate different |
104 | /// configurations. The [CustomScrollView.anchor] and [CustomScrollView.center] |
105 | /// properties are also set to have the 0 scroll offset positioned in the middle |
106 | /// of the viewport, with [GrowthDirection.forward] and [GrowthDirection.reverse] |
107 | /// illustrated on either side. The sliver that shares the |
108 | /// [CustomScrollView.center] key is positioned at the [CustomScrollView.anchor]. |
109 | /// |
110 | /// ** See code in examples/api/lib/rendering/growth_direction/growth_direction.0.dart ** |
111 | /// {@end-tool} |
112 | /// {@endtemplate} |
113 | /// |
114 | /// See also: |
115 | /// |
116 | /// * [applyGrowthDirectionToAxisDirection], which returns the direction in |
117 | /// which the scroll offset increases. |
118 | enum GrowthDirection { |
119 | /// This sliver's contents are ordered in the same direction as the |
120 | /// [AxisDirection]. For example, a vertical alphabetical list that is going |
121 | /// [AxisDirection.down] with a [GrowthDirection.forward] would have the A at |
122 | /// the top and the Z at the bottom, with the A adjacent to the origin. |
123 | /// |
124 | /// See also: |
125 | /// |
126 | /// * [applyGrowthDirectionToAxisDirection], which returns the direction in |
127 | /// which the scroll offset increases. |
128 | forward, |
129 | |
130 | /// This sliver's contents are ordered in the opposite direction of the |
131 | /// [AxisDirection]. |
132 | /// |
133 | /// See also: |
134 | /// |
135 | /// * [applyGrowthDirectionToAxisDirection], which returns the direction in |
136 | /// which the scroll offset increases. |
137 | reverse, |
138 | } |
139 | |
140 | /// Flips the [AxisDirection] if the [GrowthDirection] is [GrowthDirection.reverse]. |
141 | /// |
142 | /// Specifically, returns `axisDirection` if `growthDirection` is |
143 | /// [GrowthDirection.forward], otherwise returns [flipAxisDirection] applied to |
144 | /// `axisDirection`. |
145 | /// |
146 | /// This function is useful in [RenderSliver] subclasses that are given both an |
147 | /// [AxisDirection] and a [GrowthDirection] and wish to compute the |
148 | /// [AxisDirection] in which growth will occur. |
149 | AxisDirection applyGrowthDirectionToAxisDirection(AxisDirection axisDirection, GrowthDirection growthDirection) { |
150 | switch (growthDirection) { |
151 | case GrowthDirection.forward: |
152 | return axisDirection; |
153 | case GrowthDirection.reverse: |
154 | return flipAxisDirection(axisDirection); |
155 | } |
156 | } |
157 | |
158 | /// Flips the [ScrollDirection] if the [GrowthDirection] is |
159 | /// [GrowthDirection.reverse]. |
160 | /// |
161 | /// Specifically, returns `scrollDirection` if `scrollDirection` is |
162 | /// [GrowthDirection.forward], otherwise returns [flipScrollDirection] applied |
163 | /// to `scrollDirection`. |
164 | /// |
165 | /// This function is useful in [RenderSliver] subclasses that are given both an |
166 | /// [ScrollDirection] and a [GrowthDirection] and wish to compute the |
167 | /// [ScrollDirection] in which growth will occur. |
168 | ScrollDirection applyGrowthDirectionToScrollDirection(ScrollDirection scrollDirection, GrowthDirection growthDirection) { |
169 | switch (growthDirection) { |
170 | case GrowthDirection.forward: |
171 | return scrollDirection; |
172 | case GrowthDirection.reverse: |
173 | return flipScrollDirection(scrollDirection); |
174 | } |
175 | } |
176 | |
177 | /// Immutable layout constraints for [RenderSliver] layout. |
178 | /// |
179 | /// The [SliverConstraints] describe the current scroll state of the viewport |
180 | /// from the point of view of the sliver receiving the constraints. For example, |
181 | /// a [scrollOffset] of zero means that the leading edge of the sliver is |
182 | /// visible in the viewport, not that the viewport itself has a zero scroll |
183 | /// offset. |
184 | class SliverConstraints extends Constraints { |
185 | /// Creates sliver constraints with the given information. |
186 | const SliverConstraints({ |
187 | required this.axisDirection, |
188 | required this.growthDirection, |
189 | required this.userScrollDirection, |
190 | required this.scrollOffset, |
191 | required this.precedingScrollExtent, |
192 | required this.overlap, |
193 | required this.remainingPaintExtent, |
194 | required this.crossAxisExtent, |
195 | required this.crossAxisDirection, |
196 | required this.viewportMainAxisExtent, |
197 | required this.remainingCacheExtent, |
198 | required this.cacheOrigin, |
199 | }); |
200 | |
201 | /// Creates a copy of this object but with the given fields replaced with the |
202 | /// new values. |
203 | SliverConstraints copyWith({ |
204 | AxisDirection? axisDirection, |
205 | GrowthDirection? growthDirection, |
206 | ScrollDirection? userScrollDirection, |
207 | double? scrollOffset, |
208 | double? precedingScrollExtent, |
209 | double? overlap, |
210 | double? remainingPaintExtent, |
211 | double? crossAxisExtent, |
212 | AxisDirection? crossAxisDirection, |
213 | double? viewportMainAxisExtent, |
214 | double? remainingCacheExtent, |
215 | double? cacheOrigin, |
216 | }) { |
217 | return SliverConstraints( |
218 | axisDirection: axisDirection ?? this.axisDirection, |
219 | growthDirection: growthDirection ?? this.growthDirection, |
220 | userScrollDirection: userScrollDirection ?? this.userScrollDirection, |
221 | scrollOffset: scrollOffset ?? this.scrollOffset, |
222 | precedingScrollExtent: precedingScrollExtent ?? this.precedingScrollExtent, |
223 | overlap: overlap ?? this.overlap, |
224 | remainingPaintExtent: remainingPaintExtent ?? this.remainingPaintExtent, |
225 | crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent, |
226 | crossAxisDirection: crossAxisDirection ?? this.crossAxisDirection, |
227 | viewportMainAxisExtent: viewportMainAxisExtent ?? this.viewportMainAxisExtent, |
228 | remainingCacheExtent: remainingCacheExtent ?? this.remainingCacheExtent, |
229 | cacheOrigin: cacheOrigin ?? this.cacheOrigin, |
230 | ); |
231 | } |
232 | |
233 | /// The direction in which the [scrollOffset] and [remainingPaintExtent] |
234 | /// increase. |
235 | /// |
236 | /// {@tool dartpad} |
237 | /// This sample shows a [CustomScrollView], with [Radio] buttons in the |
238 | /// [AppBar.bottom] that change the [AxisDirection] to illustrate different |
239 | /// configurations. |
240 | /// |
241 | /// ** See code in examples/api/lib/painting/axis_direction/axis_direction.0.dart ** |
242 | /// {@end-tool} |
243 | final AxisDirection axisDirection; |
244 | |
245 | /// The direction in which the contents of slivers are ordered, relative to |
246 | /// the [axisDirection]. |
247 | /// |
248 | /// For example, if the [axisDirection] is [AxisDirection.up], and the |
249 | /// [growthDirection] is [GrowthDirection.forward], then an alphabetical list |
250 | /// will have A at the bottom, then B, then C, and so forth, with Z at the |
251 | /// top, with the bottom of the A at scroll offset zero, and the top of the Z |
252 | /// at the highest scroll offset. |
253 | /// |
254 | /// If a viewport has an overall [AxisDirection] of [AxisDirection.down], then |
255 | /// slivers above the absolute zero offset will have an axis of |
256 | /// [AxisDirection.up] and a growth direction of [GrowthDirection.reverse], |
257 | /// while slivers below the absolute zero offset will have the same axis |
258 | /// direction as the viewport and a growth direction of |
259 | /// [GrowthDirection.forward]. (The slivers with a reverse growth direction |
260 | /// still see only positive scroll offsets; the scroll offsets are reversed as |
261 | /// well, with zero at the absolute zero point, and positive numbers going |
262 | /// away from there.) |
263 | /// |
264 | /// Normally, the absolute zero offset is determined by the viewport's |
265 | /// [RenderViewport.center] and [RenderViewport.anchor] properties. |
266 | /// |
267 | /// {@macro flutter.rendering.GrowthDirection.sample} |
268 | final GrowthDirection growthDirection; |
269 | |
270 | /// The direction in which the user is attempting to scroll, relative to the |
271 | /// [axisDirection] and [growthDirection]. |
272 | /// |
273 | /// For example, if [growthDirection] is [GrowthDirection.forward] and |
274 | /// [axisDirection] is [AxisDirection.down], then a |
275 | /// [ScrollDirection.reverse] means that the user is scrolling down, in the |
276 | /// positive [scrollOffset] direction. |
277 | /// |
278 | /// If the _user_ is not scrolling, this will return [ScrollDirection.idle] |
279 | /// even if there is (for example) a [ScrollActivity] currently animating the |
280 | /// position. |
281 | /// |
282 | /// This is used by some slivers to determine how to react to a change in |
283 | /// scroll offset. For example, [RenderSliverFloatingPersistentHeader] will |
284 | /// only expand a floating app bar when the [userScrollDirection] is in the |
285 | /// positive scroll offset direction. |
286 | /// |
287 | /// {@macro flutter.rendering.ScrollDirection.sample} |
288 | final ScrollDirection userScrollDirection; |
289 | |
290 | /// {@template flutter.rendering.SliverConstraints.scrollOffset} |
291 | /// The scroll offset, in this sliver's coordinate system, that corresponds to |
292 | /// the earliest visible part of this sliver in the [AxisDirection] if |
293 | /// [SliverConstraints.growthDirection] is [GrowthDirection.forward] or in the opposite |
294 | /// [AxisDirection] direction if [SliverConstraints.growthDirection] is [GrowthDirection.reverse]. |
295 | /// |
296 | /// For example, if [AxisDirection] is [AxisDirection.down] and [SliverConstraints.growthDirection] |
297 | /// is [GrowthDirection.forward], then scroll offset is the amount the top of |
298 | /// the sliver has been scrolled past the top of the viewport. |
299 | /// |
300 | /// This value is typically used to compute whether this sliver should still |
301 | /// protrude into the viewport via [SliverGeometry.paintExtent] and |
302 | /// [SliverGeometry.layoutExtent] considering how far the beginning of the |
303 | /// sliver is above the beginning of the viewport. |
304 | /// |
305 | /// For slivers whose top is not past the top of the viewport, the |
306 | /// [scrollOffset] is `0` when [AxisDirection] is [AxisDirection.down] and |
307 | /// [SliverConstraints.growthDirection] is [GrowthDirection.forward]. The set of slivers with |
308 | /// [scrollOffset] `0` includes all the slivers that are below the bottom of the |
309 | /// viewport. |
310 | /// |
311 | /// [SliverConstraints.remainingPaintExtent] is typically used to accomplish |
312 | /// the same goal of computing whether scrolled out slivers should still |
313 | /// partially 'protrude in' from the bottom of the viewport. |
314 | /// |
315 | /// Whether this corresponds to the beginning or the end of the sliver's |
316 | /// contents depends on the [SliverConstraints.growthDirection]. |
317 | /// {@endtemplate} |
318 | final double scrollOffset; |
319 | |
320 | /// {@template flutter.rendering.SliverConstraints.precedingScrollExtent} |
321 | /// The scroll distance that has been consumed by all [RenderSliver]s that |
322 | /// came before this [RenderSliver]. |
323 | /// |
324 | /// # Edge Cases |
325 | /// |
326 | /// [RenderSliver]s often lazily create their internal content as layout |
327 | /// occurs, e.g., [SliverList]. In this case, when [RenderSliver]s exceed the |
328 | /// viewport, their children are built lazily, and the [RenderSliver] does not |
329 | /// have enough information to estimate its total extent, |
330 | /// [precedingScrollExtent] will be [double.infinity] for all [RenderSliver]s |
331 | /// that appear after the lazily constructed child. This is because a total |
332 | /// [SliverGeometry.scrollExtent] cannot be calculated unless all inner |
333 | /// children have been created and sized, or the number of children and |
334 | /// estimated extents are provided. The infinite [SliverGeometry.scrollExtent] |
335 | /// will become finite as soon as enough information is available to estimate |
336 | /// the overall extent of all children within the given [RenderSliver]. |
337 | /// |
338 | /// [RenderSliver]s may legitimately be infinite, meaning that they can scroll |
339 | /// content forever without reaching the end. For any [RenderSliver]s that |
340 | /// appear after the infinite [RenderSliver], the [precedingScrollExtent] will |
341 | /// be [double.infinity]. |
342 | /// {@endtemplate} |
343 | final double precedingScrollExtent; |
344 | |
345 | /// The number of pixels from where the pixels corresponding to the |
346 | /// [scrollOffset] will be painted up to the first pixel that has not yet been |
347 | /// painted on by an earlier sliver, in the [axisDirection]. |
348 | /// |
349 | /// For example, if the previous sliver had a [SliverGeometry.paintExtent] of |
350 | /// 100.0 pixels but a [SliverGeometry.layoutExtent] of only 50.0 pixels, |
351 | /// then the [overlap] of this sliver will be 50.0. |
352 | /// |
353 | /// This is typically ignored unless the sliver is itself going to be pinned |
354 | /// or floating and wants to avoid doing so under the previous sliver. |
355 | final double overlap; |
356 | |
357 | /// The number of pixels of content that the sliver should consider providing. |
358 | /// (Providing more pixels than this is inefficient.) |
359 | /// |
360 | /// The actual number of pixels provided should be specified in the |
361 | /// [RenderSliver.geometry] as [SliverGeometry.paintExtent]. |
362 | /// |
363 | /// This value may be infinite, for example if the viewport is an |
364 | /// unconstrained [RenderShrinkWrappingViewport]. |
365 | /// |
366 | /// This value may be 0.0, for example if the sliver is scrolled off the |
367 | /// bottom of a downwards vertical viewport. |
368 | final double remainingPaintExtent; |
369 | |
370 | /// The number of pixels in the cross-axis. |
371 | /// |
372 | /// For a vertical list, this is the width of the sliver. |
373 | final double crossAxisExtent; |
374 | |
375 | /// The direction in which children should be placed in the cross axis. |
376 | /// |
377 | /// Typically used in vertical lists to describe whether the ambient |
378 | /// [TextDirection] is [TextDirection.rtl] or [TextDirection.ltr]. |
379 | final AxisDirection crossAxisDirection; |
380 | |
381 | /// The number of pixels the viewport can display in the main axis. |
382 | /// |
383 | /// For a vertical list, this is the height of the viewport. |
384 | final double viewportMainAxisExtent; |
385 | |
386 | /// Where the cache area starts relative to the [scrollOffset]. |
387 | /// |
388 | /// Slivers that fall into the cache area located before the leading edge and |
389 | /// after the trailing edge of the viewport should still render content |
390 | /// because they are about to become visible when the user scrolls. |
391 | /// |
392 | /// The [cacheOrigin] describes where the [remainingCacheExtent] starts relative |
393 | /// to the [scrollOffset]. A cache origin of 0 means that the sliver does not |
394 | /// have to provide any content before the current [scrollOffset]. A |
395 | /// [cacheOrigin] of -250.0 means that even though the first visible part of |
396 | /// the sliver will be at the provided [scrollOffset], the sliver should |
397 | /// render content starting 250.0 before the [scrollOffset] to fill the |
398 | /// cache area of the viewport. |
399 | /// |
400 | /// The [cacheOrigin] is always negative or zero and will never exceed |
401 | /// -[scrollOffset]. In other words, a sliver is never asked to provide |
402 | /// content before its zero [scrollOffset]. |
403 | /// |
404 | /// See also: |
405 | /// |
406 | /// * [RenderViewport.cacheExtent] for a description of a viewport's cache area. |
407 | final double cacheOrigin; |
408 | |
409 | |
410 | /// Describes how much content the sliver should provide starting from the |
411 | /// [cacheOrigin]. |
412 | /// |
413 | /// Not all content in the [remainingCacheExtent] will be visible as some |
414 | /// of it might fall into the cache area of the viewport. |
415 | /// |
416 | /// Each sliver should start laying out content at the [cacheOrigin] and |
417 | /// try to provide as much content as the [remainingCacheExtent] allows. |
418 | /// |
419 | /// The [remainingCacheExtent] is always larger or equal to the |
420 | /// [remainingPaintExtent]. Content, that falls in the [remainingCacheExtent], |
421 | /// but is outside of the [remainingPaintExtent] is currently not visible |
422 | /// in the viewport. |
423 | /// |
424 | /// See also: |
425 | /// |
426 | /// * [RenderViewport.cacheExtent] for a description of a viewport's cache area. |
427 | final double remainingCacheExtent; |
428 | |
429 | /// The axis along which the [scrollOffset] and [remainingPaintExtent] are measured. |
430 | Axis get axis => axisDirectionToAxis(axisDirection); |
431 | |
432 | /// Return what the [growthDirection] would be if the [axisDirection] was |
433 | /// either [AxisDirection.down] or [AxisDirection.right]. |
434 | /// |
435 | /// This is the same as [growthDirection] unless the [axisDirection] is either |
436 | /// [AxisDirection.up] or [AxisDirection.left], in which case it is the |
437 | /// opposite growth direction. |
438 | /// |
439 | /// This can be useful in combination with [axis] to view the [axisDirection] |
440 | /// and [growthDirection] in different terms. |
441 | GrowthDirection get normalizedGrowthDirection { |
442 | switch (axisDirection) { |
443 | case AxisDirection.down: |
444 | case AxisDirection.right: |
445 | return growthDirection; |
446 | case AxisDirection.up: |
447 | case AxisDirection.left: |
448 | switch (growthDirection) { |
449 | case GrowthDirection.forward: |
450 | return GrowthDirection.reverse; |
451 | case GrowthDirection.reverse: |
452 | return GrowthDirection.forward; |
453 | } |
454 | } |
455 | } |
456 | |
457 | @override |
458 | bool get isTight => false; |
459 | |
460 | @override |
461 | bool get isNormalized { |
462 | return scrollOffset >= 0.0 |
463 | && crossAxisExtent >= 0.0 |
464 | && axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection) |
465 | && viewportMainAxisExtent >= 0.0 |
466 | && remainingPaintExtent >= 0.0; |
467 | } |
468 | |
469 | /// Returns [BoxConstraints] that reflects the sliver constraints. |
470 | /// |
471 | /// The `minExtent` and `maxExtent` are used as the constraints in the main |
472 | /// axis. If non-null, the given `crossAxisExtent` is used as a tight |
473 | /// constraint in the cross axis. Otherwise, the [crossAxisExtent] from this |
474 | /// object is used as a constraint in the cross axis. |
475 | /// |
476 | /// Useful for slivers that have [RenderBox] children. |
477 | BoxConstraints asBoxConstraints({ |
478 | double minExtent = 0.0, |
479 | double maxExtent = double.infinity, |
480 | double? crossAxisExtent, |
481 | }) { |
482 | crossAxisExtent ??= this.crossAxisExtent; |
483 | switch (axis) { |
484 | case Axis.horizontal: |
485 | return BoxConstraints( |
486 | minHeight: crossAxisExtent, |
487 | maxHeight: crossAxisExtent, |
488 | minWidth: minExtent, |
489 | maxWidth: maxExtent, |
490 | ); |
491 | case Axis.vertical: |
492 | return BoxConstraints( |
493 | minWidth: crossAxisExtent, |
494 | maxWidth: crossAxisExtent, |
495 | minHeight: minExtent, |
496 | maxHeight: maxExtent, |
497 | ); |
498 | } |
499 | } |
500 | |
501 | @override |
502 | bool debugAssertIsValid({ |
503 | bool isAppliedConstraint = false, |
504 | InformationCollector? informationCollector, |
505 | }) { |
506 | assert(() { |
507 | bool hasErrors = false; |
508 | final StringBuffer errorMessage = StringBuffer('\n' ); |
509 | void verify(bool check, String message) { |
510 | if (check) { |
511 | return; |
512 | } |
513 | hasErrors = true; |
514 | errorMessage.writeln(' $message' ); |
515 | } |
516 | void verifyDouble(double property, String name, {bool mustBePositive = false, bool mustBeNegative = false}) { |
517 | if (property.isNaN) { |
518 | String additional = '.' ; |
519 | if (mustBePositive) { |
520 | additional = ', expected greater than or equal to zero.' ; |
521 | } else if (mustBeNegative) { |
522 | additional = ', expected less than or equal to zero.' ; |
523 | } |
524 | verify(false, 'The " $name" is NaN $additional' ); |
525 | } else if (mustBePositive) { |
526 | verify(property >= 0.0, 'The " $name" is negative.' ); |
527 | } else if (mustBeNegative) { |
528 | verify(property <= 0.0, 'The " $name" is positive.' ); |
529 | } |
530 | } |
531 | verifyDouble(scrollOffset, 'scrollOffset' ); |
532 | verifyDouble(overlap, 'overlap' ); |
533 | verifyDouble(crossAxisExtent, 'crossAxisExtent' ); |
534 | verifyDouble(scrollOffset, 'scrollOffset' , mustBePositive: true); |
535 | verify(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection), 'The "axisDirection" and the "crossAxisDirection" are along the same axis.' ); |
536 | verifyDouble(viewportMainAxisExtent, 'viewportMainAxisExtent' , mustBePositive: true); |
537 | verifyDouble(remainingPaintExtent, 'remainingPaintExtent' , mustBePositive: true); |
538 | verifyDouble(remainingCacheExtent, 'remainingCacheExtent' , mustBePositive: true); |
539 | verifyDouble(cacheOrigin, 'cacheOrigin' , mustBeNegative: true); |
540 | verifyDouble(precedingScrollExtent, 'precedingScrollExtent' , mustBePositive: true); |
541 | verify(isNormalized, 'The constraints are not normalized.' ); // should be redundant with earlier checks |
542 | if (hasErrors) { |
543 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
544 | ErrorSummary(' $runtimeType is not valid: $errorMessage' ), |
545 | if (informationCollector != null) |
546 | ...informationCollector(), |
547 | DiagnosticsProperty<SliverConstraints>('The offending constraints were' , this, style: DiagnosticsTreeStyle.errorProperty), |
548 | ]); |
549 | } |
550 | return true; |
551 | }()); |
552 | return true; |
553 | } |
554 | |
555 | @override |
556 | bool operator ==(Object other) { |
557 | if (identical(this, other)) { |
558 | return true; |
559 | } |
560 | if (other is! SliverConstraints) { |
561 | return false; |
562 | } |
563 | assert(other.debugAssertIsValid()); |
564 | return other.axisDirection == axisDirection |
565 | && other.growthDirection == growthDirection |
566 | && other.scrollOffset == scrollOffset |
567 | && other.overlap == overlap |
568 | && other.remainingPaintExtent == remainingPaintExtent |
569 | && other.crossAxisExtent == crossAxisExtent |
570 | && other.crossAxisDirection == crossAxisDirection |
571 | && other.viewportMainAxisExtent == viewportMainAxisExtent |
572 | && other.remainingCacheExtent == remainingCacheExtent |
573 | && other.cacheOrigin == cacheOrigin; |
574 | } |
575 | |
576 | @override |
577 | int get hashCode => Object.hash( |
578 | axisDirection, |
579 | growthDirection, |
580 | scrollOffset, |
581 | overlap, |
582 | remainingPaintExtent, |
583 | crossAxisExtent, |
584 | crossAxisDirection, |
585 | viewportMainAxisExtent, |
586 | remainingCacheExtent, |
587 | cacheOrigin, |
588 | ); |
589 | |
590 | @override |
591 | String toString() { |
592 | final List<String> properties = <String>[ |
593 | ' $axisDirection' , |
594 | ' $growthDirection' , |
595 | ' $userScrollDirection' , |
596 | 'scrollOffset: ${scrollOffset.toStringAsFixed(1)}' , |
597 | 'remainingPaintExtent: ${remainingPaintExtent.toStringAsFixed(1)}' , |
598 | if (overlap != 0.0) 'overlap: ${overlap.toStringAsFixed(1)}' , |
599 | 'crossAxisExtent: ${crossAxisExtent.toStringAsFixed(1)}' , |
600 | 'crossAxisDirection: $crossAxisDirection' , |
601 | 'viewportMainAxisExtent: ${viewportMainAxisExtent.toStringAsFixed(1)}' , |
602 | 'remainingCacheExtent: ${remainingCacheExtent.toStringAsFixed(1)}' , |
603 | 'cacheOrigin: ${cacheOrigin.toStringAsFixed(1)}' , |
604 | ]; |
605 | return 'SliverConstraints( ${properties.join(', ' )})' ; |
606 | } |
607 | } |
608 | |
609 | /// Describes the amount of space occupied by a [RenderSliver]. |
610 | /// |
611 | /// A sliver can occupy space in several different ways, which is why this class |
612 | /// contains multiple values. |
613 | @immutable |
614 | class SliverGeometry with Diagnosticable { |
615 | /// Creates an object that describes the amount of space occupied by a sliver. |
616 | /// |
617 | /// If the [layoutExtent] argument is null, [layoutExtent] defaults to the |
618 | /// [paintExtent]. If the [hitTestExtent] argument is null, [hitTestExtent] |
619 | /// defaults to the [paintExtent]. If [visible] is null, [visible] defaults to |
620 | /// whether [paintExtent] is greater than zero. |
621 | const SliverGeometry({ |
622 | this.scrollExtent = 0.0, |
623 | this.paintExtent = 0.0, |
624 | this.paintOrigin = 0.0, |
625 | double? layoutExtent, |
626 | this.maxPaintExtent = 0.0, |
627 | this.maxScrollObstructionExtent = 0.0, |
628 | this.crossAxisExtent, |
629 | double? hitTestExtent, |
630 | bool? visible, |
631 | this.hasVisualOverflow = false, |
632 | this.scrollOffsetCorrection, |
633 | double? cacheExtent, |
634 | }) : assert(scrollOffsetCorrection != 0.0), |
635 | layoutExtent = layoutExtent ?? paintExtent, |
636 | hitTestExtent = hitTestExtent ?? paintExtent, |
637 | cacheExtent = cacheExtent ?? layoutExtent ?? paintExtent, |
638 | visible = visible ?? paintExtent > 0.0; |
639 | |
640 | /// Creates a copy of this object but with the given fields replaced with the |
641 | /// new values. |
642 | SliverGeometry copyWith({ |
643 | double? scrollExtent, |
644 | double? paintExtent, |
645 | double? paintOrigin, |
646 | double? layoutExtent, |
647 | double? maxPaintExtent, |
648 | double? maxScrollObstructionExtent, |
649 | double? crossAxisExtent, |
650 | double? hitTestExtent, |
651 | bool? visible, |
652 | bool? hasVisualOverflow, |
653 | double? cacheExtent, |
654 | }) { |
655 | return SliverGeometry( |
656 | scrollExtent: scrollExtent ?? this.scrollExtent, |
657 | paintExtent: paintExtent ?? this.paintExtent, |
658 | paintOrigin: paintOrigin ?? this.paintOrigin, |
659 | layoutExtent: layoutExtent ?? this.layoutExtent, |
660 | maxPaintExtent: maxPaintExtent ?? this.maxPaintExtent, |
661 | maxScrollObstructionExtent: maxScrollObstructionExtent ?? this.maxScrollObstructionExtent, |
662 | crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent, |
663 | hitTestExtent: hitTestExtent ?? this.hitTestExtent, |
664 | visible: visible ?? this.visible, |
665 | hasVisualOverflow: hasVisualOverflow ?? this.hasVisualOverflow, |
666 | cacheExtent: cacheExtent ?? this.cacheExtent, |
667 | ); |
668 | } |
669 | |
670 | /// A sliver that occupies no space at all. |
671 | static const SliverGeometry zero = SliverGeometry(); |
672 | |
673 | /// The (estimated) total scrollable extent that this sliver has content for. |
674 | /// |
675 | /// This is the amount of scrolling the user needs to do to get from the |
676 | /// beginning of this sliver to the end of this sliver. |
677 | /// |
678 | /// The value is used to calculate the [SliverConstraints.scrollOffset] of |
679 | /// all slivers in the scrollable and thus should be provided whether the |
680 | /// sliver is currently in the viewport or not. |
681 | /// |
682 | /// In a typical scrolling scenario, the [scrollExtent] is constant for a |
683 | /// sliver throughout the scrolling while [paintExtent] and [layoutExtent] |
684 | /// will progress from `0` when offscreen to between `0` and [scrollExtent] |
685 | /// as the sliver scrolls partially into and out of the screen and is |
686 | /// equal to [scrollExtent] while the sliver is entirely on screen. However, |
687 | /// these relationships can be customized to achieve more special effects. |
688 | /// |
689 | /// This value must be accurate if the [paintExtent] is less than the |
690 | /// [SliverConstraints.remainingPaintExtent] provided during layout. |
691 | final double scrollExtent; |
692 | |
693 | /// The visual location of the first visible part of this sliver relative to |
694 | /// its layout position. |
695 | /// |
696 | /// For example, if the sliver wishes to paint visually before its layout |
697 | /// position, the [paintOrigin] is negative. The coordinate system this sliver |
698 | /// uses for painting is relative to this [paintOrigin]. In other words, |
699 | /// when [RenderSliver.paint] is called, the (0, 0) position of the [Offset] |
700 | /// given to it is at this [paintOrigin]. |
701 | /// |
702 | /// The coordinate system used for the [paintOrigin] itself is relative |
703 | /// to the start of this sliver's layout position rather than relative to |
704 | /// its current position on the viewport. In other words, in a typical |
705 | /// scrolling scenario, [paintOrigin] remains constant at 0.0 rather than |
706 | /// tracking from 0.0 to [SliverConstraints.viewportMainAxisExtent] as the |
707 | /// sliver scrolls past the viewport. |
708 | /// |
709 | /// This value does not affect the layout of subsequent slivers. The next |
710 | /// sliver is still placed at [layoutExtent] after this sliver's layout |
711 | /// position. This value does affect where the [paintExtent] extent is |
712 | /// measured from when computing the [SliverConstraints.overlap] for the next |
713 | /// sliver. |
714 | /// |
715 | /// Defaults to 0.0, which means slivers start painting at their layout |
716 | /// position by default. |
717 | final double paintOrigin; |
718 | |
719 | /// The amount of currently visible visual space that was taken by the sliver |
720 | /// to render the subset of the sliver that covers all or part of the |
721 | /// [SliverConstraints.remainingPaintExtent] in the current viewport. |
722 | /// |
723 | /// This value does not affect how the next sliver is positioned. In other |
724 | /// words, if this value was 100 and [layoutExtent] was 0, typical slivers |
725 | /// placed after it would end up drawing in the same 100 pixel space while |
726 | /// painting. |
727 | /// |
728 | /// This must be between zero and [SliverConstraints.remainingPaintExtent]. |
729 | /// |
730 | /// This value is typically 0 when outside of the viewport and grows or |
731 | /// shrinks from 0 or to 0 as the sliver is being scrolled into and out of the |
732 | /// viewport unless the sliver wants to achieve a special effect and paint |
733 | /// even when scrolled away. |
734 | /// |
735 | /// This contributes to the calculation for the next sliver's |
736 | /// [SliverConstraints.overlap]. |
737 | final double paintExtent; |
738 | |
739 | /// The distance from the first visible part of this sliver to the first |
740 | /// visible part of the next sliver, assuming the next sliver's |
741 | /// [SliverConstraints.scrollOffset] is zero. |
742 | /// |
743 | /// This must be between zero and [paintExtent]. It defaults to [paintExtent]. |
744 | /// |
745 | /// This value is typically 0 when outside of the viewport and grows or |
746 | /// shrinks from 0 or to 0 as the sliver is being scrolled into and out of the |
747 | /// viewport unless the sliver wants to achieve a special effect and push |
748 | /// down the layout start position of subsequent slivers before the sliver is |
749 | /// even scrolled into the viewport. |
750 | final double layoutExtent; |
751 | |
752 | /// The (estimated) total paint extent that this sliver would be able to |
753 | /// provide if the [SliverConstraints.remainingPaintExtent] was infinite. |
754 | /// |
755 | /// This is used by viewports that implement shrink-wrapping. |
756 | /// |
757 | /// By definition, this cannot be less than [paintExtent]. |
758 | final double maxPaintExtent; |
759 | |
760 | /// The maximum extent by which this sliver can reduce the area in which |
761 | /// content can scroll if the sliver were pinned at the edge. |
762 | /// |
763 | /// Slivers that never get pinned at the edge, should return zero. |
764 | /// |
765 | /// A pinned app bar is an example for a sliver that would use this setting: |
766 | /// When the app bar is pinned to the top, the area in which content can |
767 | /// actually scroll is reduced by the height of the app bar. |
768 | final double maxScrollObstructionExtent; |
769 | |
770 | /// The distance from where this sliver started painting to the bottom of |
771 | /// where it should accept hits. |
772 | /// |
773 | /// This must be between zero and [paintExtent]. It defaults to [paintExtent]. |
774 | final double hitTestExtent; |
775 | |
776 | /// Whether this sliver should be painted. |
777 | /// |
778 | /// By default, this is true if [paintExtent] is greater than zero, and |
779 | /// false if [paintExtent] is zero. |
780 | final bool visible; |
781 | |
782 | /// Whether this sliver has visual overflow. |
783 | /// |
784 | /// By default, this is false, which means the viewport does not need to clip |
785 | /// its children. If any slivers have visual overflow, the viewport will apply |
786 | /// a clip to its children. |
787 | final bool hasVisualOverflow; |
788 | |
789 | /// If this is non-zero after [RenderSliver.performLayout] returns, the scroll |
790 | /// offset will be adjusted by the parent and then the entire layout of the |
791 | /// parent will be rerun. |
792 | /// |
793 | /// When the value is non-zero, the [RenderSliver] does not need to compute |
794 | /// the rest of the values when constructing the [SliverGeometry] or call |
795 | /// [RenderObject.layout] on its children since [RenderSliver.performLayout] |
796 | /// will be called again on this sliver in the same frame after the |
797 | /// [SliverConstraints.scrollOffset] correction has been applied, when the |
798 | /// proper [SliverGeometry] and layout of its children can be computed. |
799 | /// |
800 | /// If the parent is also a [RenderSliver], it must propagate this value |
801 | /// in its own [RenderSliver.geometry] property until a viewport which adjusts |
802 | /// its offset based on this value. |
803 | final double? scrollOffsetCorrection; |
804 | |
805 | /// How many pixels the sliver has consumed in the |
806 | /// [SliverConstraints.remainingCacheExtent]. |
807 | /// |
808 | /// This value should be equal to or larger than the [layoutExtent] because |
809 | /// the sliver always consumes at least the [layoutExtent] from the |
810 | /// [SliverConstraints.remainingCacheExtent] and possibly more if it falls |
811 | /// into the cache area of the viewport. |
812 | /// |
813 | /// See also: |
814 | /// |
815 | /// * [RenderViewport.cacheExtent] for a description of a viewport's cache area. |
816 | final double cacheExtent; |
817 | |
818 | /// The amount of space allocated to the cross axis. |
819 | /// |
820 | /// This value will be typically null unless it is different from |
821 | /// [SliverConstraints.crossAxisExtent]. If null, then the cross axis extent of |
822 | /// the sliver is assumed to be the same as the [SliverConstraints.crossAxisExtent]. |
823 | /// This is because slivers typically consume all of the extent that is available |
824 | /// in the cross axis. |
825 | /// |
826 | /// See also: |
827 | /// |
828 | /// * [SliverConstrainedCrossAxis] for an example of a sliver which takes up |
829 | /// a smaller cross axis extent than the provided constraint. |
830 | /// * [SliverCrossAxisGroup] for an example of a sliver which makes use of this |
831 | /// [crossAxisExtent] to lay out their children. |
832 | final double? crossAxisExtent; |
833 | |
834 | /// Asserts that this geometry is internally consistent. |
835 | /// |
836 | /// Does nothing if asserts are disabled. Always returns true. |
837 | bool debugAssertIsValid({ |
838 | InformationCollector? informationCollector, |
839 | }) { |
840 | assert(() { |
841 | void verify(bool check, String summary, {List<DiagnosticsNode>? details}) { |
842 | if (check) { |
843 | return; |
844 | } |
845 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
846 | ErrorSummary(' ${objectRuntimeType(this, 'SliverGeometry' )} is not valid: $summary' ), |
847 | ...?details, |
848 | if (informationCollector != null) |
849 | ...informationCollector(), |
850 | ]); |
851 | } |
852 | |
853 | verify(scrollExtent >= 0.0, 'The "scrollExtent" is negative.' ); |
854 | verify(paintExtent >= 0.0, 'The "paintExtent" is negative.' ); |
855 | verify(layoutExtent >= 0.0, 'The "layoutExtent" is negative.' ); |
856 | verify(cacheExtent >= 0.0, 'The "cacheExtent" is negative.' ); |
857 | if (layoutExtent > paintExtent) { |
858 | verify(false, |
859 | 'The "layoutExtent" exceeds the "paintExtent".' , |
860 | details: _debugCompareFloats('paintExtent' , paintExtent, 'layoutExtent' , layoutExtent), |
861 | ); |
862 | } |
863 | // If the paintExtent is slightly more than the maxPaintExtent, but the difference is still less |
864 | // than precisionErrorTolerance, we will not throw the assert below. |
865 | if (paintExtent - maxPaintExtent > precisionErrorTolerance) { |
866 | verify(false, |
867 | 'The "maxPaintExtent" is less than the "paintExtent".' , |
868 | details: |
869 | _debugCompareFloats('maxPaintExtent' , maxPaintExtent, 'paintExtent' , paintExtent) |
870 | ..add(ErrorDescription("By definition, a sliver can't paint more than the maximum that it can paint!" )), |
871 | ); |
872 | } |
873 | verify(hitTestExtent >= 0.0, 'The "hitTestExtent" is negative.' ); |
874 | verify(scrollOffsetCorrection != 0.0, 'The "scrollOffsetCorrection" is zero.' ); |
875 | return true; |
876 | }()); |
877 | return true; |
878 | } |
879 | |
880 | @override |
881 | String toStringShort() => objectRuntimeType(this, 'SliverGeometry' ); |
882 | |
883 | @override |
884 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
885 | super.debugFillProperties(properties); |
886 | properties.add(DoubleProperty('scrollExtent' , scrollExtent)); |
887 | if (paintExtent > 0.0) { |
888 | properties.add(DoubleProperty('paintExtent' , paintExtent, unit : visible ? null : ' but not painting' )); |
889 | } else if (paintExtent == 0.0) { |
890 | if (visible) { |
891 | properties.add(DoubleProperty('paintExtent' , paintExtent, unit: visible ? null : ' but visible' )); |
892 | } |
893 | properties.add(FlagProperty('visible' , value: visible, ifFalse: 'hidden' )); |
894 | } else { |
895 | // Negative paintExtent! |
896 | properties.add(DoubleProperty('paintExtent' , paintExtent, tooltip: '!' )); |
897 | } |
898 | properties.add(DoubleProperty('paintOrigin' , paintOrigin, defaultValue: 0.0)); |
899 | properties.add(DoubleProperty('layoutExtent' , layoutExtent, defaultValue: paintExtent)); |
900 | properties.add(DoubleProperty('maxPaintExtent' , maxPaintExtent)); |
901 | properties.add(DoubleProperty('hitTestExtent' , hitTestExtent, defaultValue: paintExtent)); |
902 | properties.add(DiagnosticsProperty<bool>('hasVisualOverflow' , hasVisualOverflow, defaultValue: false)); |
903 | properties.add(DoubleProperty('scrollOffsetCorrection' , scrollOffsetCorrection, defaultValue: null)); |
904 | properties.add(DoubleProperty('cacheExtent' , cacheExtent, defaultValue: 0.0)); |
905 | } |
906 | } |
907 | |
908 | /// Method signature for hit testing a [RenderSliver]. |
909 | /// |
910 | /// Used by [SliverHitTestResult.addWithAxisOffset] to hit test [RenderSliver] |
911 | /// children. |
912 | /// |
913 | /// See also: |
914 | /// |
915 | /// * [RenderSliver.hitTest], which documents more details around hit testing |
916 | /// [RenderSliver]s. |
917 | typedef SliverHitTest = bool Function(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }); |
918 | |
919 | /// The result of performing a hit test on [RenderSliver]s. |
920 | /// |
921 | /// An instance of this class is provided to [RenderSliver.hitTest] to record |
922 | /// the result of the hit test. |
923 | class SliverHitTestResult extends HitTestResult { |
924 | /// Creates an empty hit test result for hit testing on [RenderSliver]. |
925 | SliverHitTestResult() : super(); |
926 | |
927 | /// Wraps `result` to create a [HitTestResult] that implements the |
928 | /// [SliverHitTestResult] protocol for hit testing on [RenderSliver]s. |
929 | /// |
930 | /// This method is used by [RenderObject]s that adapt between the |
931 | /// [RenderSliver]-world and the non-[RenderSliver]-world to convert a |
932 | /// (subtype of) [HitTestResult] to a [SliverHitTestResult] for hit testing on |
933 | /// [RenderSliver]s. |
934 | /// |
935 | /// The [HitTestEntry] instances added to the returned [SliverHitTestResult] |
936 | /// are also added to the wrapped `result` (both share the same underlying |
937 | /// data structure to store [HitTestEntry] instances). |
938 | /// |
939 | /// See also: |
940 | /// |
941 | /// * [HitTestResult.wrap], which turns a [SliverHitTestResult] back into a |
942 | /// generic [HitTestResult]. |
943 | /// * [BoxHitTestResult.wrap], which turns a [SliverHitTestResult] into a |
944 | /// [BoxHitTestResult] for hit testing on [RenderBox] children. |
945 | SliverHitTestResult.wrap(super.result) : super.wrap(); |
946 | |
947 | /// Transforms `mainAxisPosition` and `crossAxisPosition` to the local |
948 | /// coordinate system of a child for hit-testing the child. |
949 | /// |
950 | /// The actual hit testing of the child needs to be implemented in the |
951 | /// provided `hitTest` callback, which is invoked with the transformed |
952 | /// `position` as argument. |
953 | /// |
954 | /// For the transform `mainAxisOffset` is subtracted from `mainAxisPosition` |
955 | /// and `crossAxisOffset` is subtracted from `crossAxisPosition`. |
956 | /// |
957 | /// The `paintOffset` describes how the paint position of a point painted at |
958 | /// the provided `mainAxisPosition` and `crossAxisPosition` would change after |
959 | /// `mainAxisOffset` and `crossAxisOffset` have been applied. This |
960 | /// `paintOffset` is used to properly convert [PointerEvent]s to the local |
961 | /// coordinate system of the event receiver. |
962 | /// |
963 | /// The `paintOffset` may be null if `mainAxisOffset` and `crossAxisOffset` are |
964 | /// both zero. |
965 | /// |
966 | /// The function returns the return value of `hitTest`. |
967 | bool addWithAxisOffset({ |
968 | required Offset? paintOffset, |
969 | required double mainAxisOffset, |
970 | required double crossAxisOffset, |
971 | required double mainAxisPosition, |
972 | required double crossAxisPosition, |
973 | required SliverHitTest hitTest, |
974 | }) { |
975 | if (paintOffset != null) { |
976 | pushOffset(-paintOffset); |
977 | } |
978 | final bool isHit = hitTest( |
979 | this, |
980 | mainAxisPosition: mainAxisPosition - mainAxisOffset, |
981 | crossAxisPosition: crossAxisPosition - crossAxisOffset, |
982 | ); |
983 | if (paintOffset != null) { |
984 | popTransform(); |
985 | } |
986 | return isHit; |
987 | } |
988 | } |
989 | |
990 | /// A hit test entry used by [RenderSliver]. |
991 | /// |
992 | /// The coordinate system used by this hit test entry is relative to the |
993 | /// [AxisDirection] of the target sliver. |
994 | class SliverHitTestEntry extends HitTestEntry<RenderSliver> { |
995 | /// Creates a sliver hit test entry. |
996 | SliverHitTestEntry( |
997 | super.target, { |
998 | required this.mainAxisPosition, |
999 | required this.crossAxisPosition, |
1000 | }); |
1001 | |
1002 | /// The distance in the [AxisDirection] from the edge of the sliver's painted |
1003 | /// area (as given by the [SliverConstraints.scrollOffset]) to the hit point. |
1004 | /// This can be an unusual direction, for example in the [AxisDirection.up] |
1005 | /// case this is a distance from the _bottom_ of the sliver's painted area. |
1006 | final double mainAxisPosition; |
1007 | |
1008 | /// The distance to the hit point in the axis opposite the |
1009 | /// [SliverConstraints.axis]. |
1010 | /// |
1011 | /// If the cross axis is horizontal (i.e. the |
1012 | /// [SliverConstraints.axisDirection] is either [AxisDirection.down] or |
1013 | /// [AxisDirection.up]), then the [crossAxisPosition] is a distance from the |
1014 | /// left edge of the sliver. If the cross axis is vertical (i.e. the |
1015 | /// [SliverConstraints.axisDirection] is either [AxisDirection.right] or |
1016 | /// [AxisDirection.left]), then the [crossAxisPosition] is a distance from the |
1017 | /// top edge of the sliver. |
1018 | /// |
1019 | /// This is always a distance from the left or top of the parent, never a |
1020 | /// distance from the right or bottom. |
1021 | final double crossAxisPosition; |
1022 | |
1023 | @override |
1024 | String toString() => ' ${target.runtimeType}@(mainAxis: $mainAxisPosition, crossAxis: $crossAxisPosition)' ; |
1025 | } |
1026 | |
1027 | /// Parent data structure used by parents of slivers that position their |
1028 | /// children using layout offsets. |
1029 | /// |
1030 | /// This data structure is optimized for fast layout. It is best used by parents |
1031 | /// that expect to have many children whose relative positions don't change even |
1032 | /// when the scroll offset does. |
1033 | class SliverLogicalParentData extends ParentData { |
1034 | /// The position of the child relative to the zero scroll offset. |
1035 | /// |
1036 | /// The number of pixels from the zero scroll offset of the parent sliver |
1037 | /// (the line at which its [SliverConstraints.scrollOffset] is zero) to the |
1038 | /// side of the child closest to that offset. A [layoutOffset] can be null |
1039 | /// when it cannot be determined. The value will be set after layout. |
1040 | /// |
1041 | /// In a typical list, this does not change as the parent is scrolled. |
1042 | /// |
1043 | /// Defaults to null. |
1044 | double? layoutOffset; |
1045 | |
1046 | @override |
1047 | String toString() => 'layoutOffset= ${layoutOffset == null ? 'None' : layoutOffset!.toStringAsFixed(1)}' ; |
1048 | } |
1049 | |
1050 | /// Parent data for slivers that have multiple children and that position their |
1051 | /// children using layout offsets. |
1052 | class SliverLogicalContainerParentData extends SliverLogicalParentData with ContainerParentDataMixin<RenderSliver> { } |
1053 | |
1054 | /// Parent data structure used by parents of slivers that position their |
1055 | /// children using absolute coordinates. |
1056 | /// |
1057 | /// For example, used by [RenderViewport]. |
1058 | /// |
1059 | /// This data structure is optimized for fast painting, at the cost of requiring |
1060 | /// additional work during layout when the children change their offsets. It is |
1061 | /// best used by parents that expect to have few children, especially if those |
1062 | /// children will themselves be very tall relative to the parent. |
1063 | class SliverPhysicalParentData extends ParentData { |
1064 | /// The position of the child relative to the parent. |
1065 | /// |
1066 | /// This is the distance from the top left visible corner of the parent to the |
1067 | /// top left visible corner of the sliver. |
1068 | Offset paintOffset = Offset.zero; |
1069 | |
1070 | /// The [crossAxisFlex] factor to use for this sliver child. |
1071 | /// |
1072 | /// If used outside of a [SliverCrossAxisGroup] widget, this value has no meaning. |
1073 | /// |
1074 | /// If null or zero, the child is inflexible and determines its own size in the cross axis. |
1075 | /// If non-zero, the amount of space the child can occupy in the cross axis is |
1076 | /// determined by dividing the free space (after placing the inflexible children) |
1077 | /// according to the flex factors of the flexible children. |
1078 | /// |
1079 | /// This value is only used by the [SliverCrossAxisGroup] widget to determine |
1080 | /// how to allocate its [SliverConstraints.crossAxisExtent] to its children. |
1081 | /// |
1082 | /// See also: |
1083 | /// |
1084 | /// * [SliverCrossAxisGroup], which lays out multiple slivers along the |
1085 | /// cross axis direction. |
1086 | int? crossAxisFlex; |
1087 | |
1088 | /// Apply the [paintOffset] to the given [transform]. |
1089 | /// |
1090 | /// Used to implement [RenderObject.applyPaintTransform] by slivers that use |
1091 | /// [SliverPhysicalParentData]. |
1092 | void applyPaintTransform(Matrix4 transform) { |
1093 | // Hit test logic relies on this always providing an invertible matrix. |
1094 | transform.translate(paintOffset.dx, paintOffset.dy); |
1095 | } |
1096 | |
1097 | @override |
1098 | String toString() => 'paintOffset= $paintOffset' ; |
1099 | } |
1100 | |
1101 | /// Parent data for slivers that have multiple children and that position their |
1102 | /// children using absolute coordinates. |
1103 | class SliverPhysicalContainerParentData extends SliverPhysicalParentData with ContainerParentDataMixin<RenderSliver> { } |
1104 | |
1105 | List<DiagnosticsNode> _debugCompareFloats(String labelA, double valueA, String labelB, double valueB) { |
1106 | return <DiagnosticsNode>[ |
1107 | if (valueA.toStringAsFixed(1) != valueB.toStringAsFixed(1)) |
1108 | ErrorDescription( |
1109 | 'The $labelA is ${valueA.toStringAsFixed(1)}, but ' |
1110 | 'the $labelB is ${valueB.toStringAsFixed(1)}.' , |
1111 | ) |
1112 | else ...<DiagnosticsNode>[ |
1113 | ErrorDescription('The $labelA is $valueA, but the $labelB is $valueB.' ), |
1114 | ErrorHint( |
1115 | 'Maybe you have fallen prey to floating point rounding errors, and should explicitly ' |
1116 | 'apply the min() or max() functions, or the clamp() method, to the $labelB?' , |
1117 | ), |
1118 | ], |
1119 | ]; |
1120 | } |
1121 | |
1122 | /// Base class for the render objects that implement scroll effects in viewports. |
1123 | /// |
1124 | /// A [RenderViewport] has a list of child slivers. Each sliver — literally a |
1125 | /// slice of the viewport's contents — is laid out in turn, covering the |
1126 | /// viewport in the process. (Every sliver is laid out each time, including |
1127 | /// those that have zero extent because they are "scrolled off" or are beyond |
1128 | /// the end of the viewport.) |
1129 | /// |
1130 | /// Slivers participate in the _sliver protocol_, wherein during [layout] each |
1131 | /// sliver receives a [SliverConstraints] object and computes a corresponding |
1132 | /// [SliverGeometry] that describes where it fits in the viewport. This is |
1133 | /// analogous to the box protocol used by [RenderBox], which gets a |
1134 | /// [BoxConstraints] as input and computes a [Size]. |
1135 | /// |
1136 | /// Slivers have a leading edge, which is where the position described by |
1137 | /// [SliverConstraints.scrollOffset] for this sliver begins. Slivers have |
1138 | /// several dimensions, the primary of which is [SliverGeometry.paintExtent], |
1139 | /// which describes the extent of the sliver along the main axis, starting from |
1140 | /// the leading edge, reaching either the end of the viewport or the end of the |
1141 | /// sliver, whichever comes first. |
1142 | /// |
1143 | /// Slivers can change dimensions based on the changing constraints in a |
1144 | /// non-linear fashion, to achieve various scroll effects. For example, the |
1145 | /// various [RenderSliverPersistentHeader] subclasses, on which [SliverAppBar] |
1146 | /// is based, achieve effects such as staying visible despite the scroll offset, |
1147 | /// or reappearing at different offsets based on the user's scroll direction |
1148 | /// ([SliverConstraints.userScrollDirection]). |
1149 | /// |
1150 | /// {@youtube 560 315 https://www.youtube.com/watch?v=Mz3kHQxBjGg} |
1151 | /// |
1152 | /// ## Writing a RenderSliver subclass |
1153 | /// |
1154 | /// Slivers can have sliver children, or children from another coordinate |
1155 | /// system, typically box children. (For details on the box protocol, see |
1156 | /// [RenderBox].) Slivers can also have different child models, typically having |
1157 | /// either one child, or a list of children. |
1158 | /// |
1159 | /// ### Examples of slivers |
1160 | /// |
1161 | /// A good example of a sliver with a single child that is also itself a sliver |
1162 | /// is [RenderSliverPadding], which indents its child. A sliver-to-sliver render |
1163 | /// object such as this must construct a [SliverConstraints] object for its |
1164 | /// child, then must take its child's [SliverGeometry] and use it to form its |
1165 | /// own [geometry]. |
1166 | /// |
1167 | /// The other common kind of one-child sliver is a sliver that has a single |
1168 | /// [RenderBox] child. An example of that would be [RenderSliverToBoxAdapter], |
1169 | /// which lays out a single box and sizes itself around the box. Such a sliver |
1170 | /// must use its [SliverConstraints] to create a [BoxConstraints] for the |
1171 | /// child, lay the child out (using the child's [layout] method), and then use |
1172 | /// the child's [RenderBox.size] to generate the sliver's [SliverGeometry]. |
1173 | /// |
1174 | /// The most common kind of sliver though is one with multiple children. The |
1175 | /// most straight-forward example of this is [RenderSliverList], which arranges |
1176 | /// its children one after the other in the main axis direction. As with the |
1177 | /// one-box-child sliver case, it uses its [constraints] to create a |
1178 | /// [BoxConstraints] for the children, and then it uses the aggregate |
1179 | /// information from all its children to generate its [geometry]. Unlike the |
1180 | /// one-child cases, however, it is judicious in which children it actually lays |
1181 | /// out (and later paints). If the scroll offset is 1000 pixels, and it |
1182 | /// previously determined that the first three children are each 400 pixels |
1183 | /// tall, then it will skip the first two and start the layout with its third |
1184 | /// child. |
1185 | /// |
1186 | /// ### Layout |
1187 | /// |
1188 | /// As they are laid out, slivers decide their [geometry], which includes their |
1189 | /// size ([SliverGeometry.paintExtent]) and the position of the next sliver |
1190 | /// ([SliverGeometry.layoutExtent]), as well as the position of each of their |
1191 | /// children, based on the input [constraints] from the viewport such as the |
1192 | /// scroll offset ([SliverConstraints.scrollOffset]). |
1193 | /// |
1194 | /// For example, a sliver that just paints a box 100 pixels high would say its |
1195 | /// [SliverGeometry.paintExtent] was 100 pixels when the scroll offset was zero, |
1196 | /// but would say its [SliverGeometry.paintExtent] was 25 pixels when the scroll |
1197 | /// offset was 75 pixels, and would say it was zero when the scroll offset was |
1198 | /// 100 pixels or more. (This is assuming that |
1199 | /// [SliverConstraints.remainingPaintExtent] was more than 100 pixels.) |
1200 | /// |
1201 | /// The various dimensions that are provided as input to this system are in the |
1202 | /// [constraints]. They are described in detail in the documentation for the |
1203 | /// [SliverConstraints] class. |
1204 | /// |
1205 | /// The [performLayout] function must take these [constraints] and create a |
1206 | /// [SliverGeometry] object that it must then assign to the [geometry] property. |
1207 | /// The different dimensions of the geometry that can be configured are |
1208 | /// described in detail in the documentation for the [SliverGeometry] class. |
1209 | /// |
1210 | /// ### Painting |
1211 | /// |
1212 | /// In addition to implementing layout, a sliver must also implement painting. |
1213 | /// This is achieved by overriding the [paint] method. |
1214 | /// |
1215 | /// The [paint] method is called with an [Offset] from the [Canvas] origin to |
1216 | /// the top-left corner of the sliver, _regardless of the axis direction_. |
1217 | /// |
1218 | /// Subclasses should also override [applyPaintTransform] to provide the |
1219 | /// [Matrix4] describing the position of each child relative to the sliver. |
1220 | /// (This is used by, among other things, the accessibility layer, to determine |
1221 | /// the bounds of the child.) |
1222 | /// |
1223 | /// ### Hit testing |
1224 | /// |
1225 | /// To implement hit testing, either override the [hitTestSelf] and |
1226 | /// [hitTestChildren] methods, or, for more complex cases, instead override the |
1227 | /// [hitTest] method directly. |
1228 | /// |
1229 | /// To actually react to pointer events, the [handleEvent] method may be |
1230 | /// implemented. By default it does nothing. (Typically gestures are handled by |
1231 | /// widgets in the box protocol, not by slivers directly.) |
1232 | /// |
1233 | /// ### Helper methods |
1234 | /// |
1235 | /// There are a number of methods that a sliver should implement which will make |
1236 | /// the other methods easier to implement. Each method listed below has detailed |
1237 | /// documentation. In addition, the [RenderSliverHelpers] class can be used to |
1238 | /// mix in some helpful methods. |
1239 | /// |
1240 | /// #### childScrollOffset |
1241 | /// |
1242 | /// If the subclass positions children anywhere other than at scroll offset |
1243 | /// zero, it should override [childScrollOffset]. For example, |
1244 | /// [RenderSliverList] and [RenderSliverGrid] override this method, but |
1245 | /// [RenderSliverToBoxAdapter] does not. |
1246 | /// |
1247 | /// This is used by, among other things, [Scrollable.ensureVisible]. |
1248 | /// |
1249 | /// #### childMainAxisPosition |
1250 | /// |
1251 | /// Subclasses should implement [childMainAxisPosition] to describe where their |
1252 | /// children are positioned. |
1253 | /// |
1254 | /// #### childCrossAxisPosition |
1255 | /// |
1256 | /// If the subclass positions children in the cross-axis at a position other |
1257 | /// than zero, then it should override [childCrossAxisPosition]. For example |
1258 | /// [RenderSliverGrid] overrides this method. |
1259 | abstract class RenderSliver extends RenderObject { |
1260 | // layout input |
1261 | @override |
1262 | SliverConstraints get constraints => super.constraints as SliverConstraints; |
1263 | |
1264 | /// The amount of space this sliver occupies. |
1265 | /// |
1266 | /// This value is stale whenever this object is marked as needing layout. |
1267 | /// During [performLayout], do not read the [geometry] of a child unless you |
1268 | /// pass true for parentUsesSize when calling the child's [layout] function. |
1269 | /// |
1270 | /// The geometry of a sliver should be set only during the sliver's |
1271 | /// [performLayout] or [performResize] functions. If you wish to change the |
1272 | /// geometry of a sliver outside of those functions, call [markNeedsLayout] |
1273 | /// instead to schedule a layout of the sliver. |
1274 | SliverGeometry? get geometry => _geometry; |
1275 | SliverGeometry? _geometry; |
1276 | set geometry(SliverGeometry? value) { |
1277 | assert(!(debugDoingThisResize && debugDoingThisLayout)); |
1278 | assert(sizedByParent || !debugDoingThisResize); |
1279 | assert(() { |
1280 | if ((sizedByParent && debugDoingThisResize) || |
1281 | (!sizedByParent && debugDoingThisLayout)) { |
1282 | return true; |
1283 | } |
1284 | assert(!debugDoingThisResize); |
1285 | DiagnosticsNode? contract, violation, hint; |
1286 | if (debugDoingThisLayout) { |
1287 | assert(sizedByParent); |
1288 | violation = ErrorDescription('It appears that the geometry setter was called from performLayout().' ); |
1289 | } else { |
1290 | violation = ErrorDescription('The geometry setter was called from outside layout (neither performResize() nor performLayout() were being run for this object).' ); |
1291 | if (owner != null && owner!.debugDoingLayout) { |
1292 | hint = ErrorDescription('Only the object itself can set its geometry. It is a contract violation for other objects to set it.' ); |
1293 | } |
1294 | } |
1295 | if (sizedByParent) { |
1296 | contract = ErrorDescription('Because this RenderSliver has sizedByParent set to true, it must set its geometry in performResize().' ); |
1297 | } else { |
1298 | contract = ErrorDescription('Because this RenderSliver has sizedByParent set to false, it must set its geometry in performLayout().' ); |
1299 | } |
1300 | |
1301 | final List<DiagnosticsNode> information = <DiagnosticsNode>[ |
1302 | ErrorSummary('RenderSliver geometry setter called incorrectly.' ), |
1303 | violation, |
1304 | if (hint != null) hint, |
1305 | contract, |
1306 | describeForError('The RenderSliver in question is' ), |
1307 | ]; |
1308 | throw FlutterError.fromParts(information); |
1309 | }()); |
1310 | _geometry = value; |
1311 | } |
1312 | |
1313 | @override |
1314 | Rect get semanticBounds => paintBounds; |
1315 | |
1316 | @override |
1317 | Rect get paintBounds { |
1318 | switch (constraints.axis) { |
1319 | case Axis.horizontal: |
1320 | return Rect.fromLTWH( |
1321 | 0.0, 0.0, |
1322 | geometry!.paintExtent, |
1323 | constraints.crossAxisExtent, |
1324 | ); |
1325 | case Axis.vertical: |
1326 | return Rect.fromLTWH( |
1327 | 0.0, 0.0, |
1328 | constraints.crossAxisExtent, |
1329 | geometry!.paintExtent, |
1330 | ); |
1331 | } |
1332 | } |
1333 | |
1334 | @override |
1335 | void debugResetSize() { } |
1336 | |
1337 | @override |
1338 | void debugAssertDoesMeetConstraints() { |
1339 | assert(geometry!.debugAssertIsValid( |
1340 | informationCollector: () => <DiagnosticsNode>[ |
1341 | describeForError('The RenderSliver that returned the offending geometry was' ), |
1342 | ], |
1343 | )); |
1344 | assert(() { |
1345 | if (geometry!.paintOrigin + geometry!.paintExtent > constraints.remainingPaintExtent) { |
1346 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
1347 | ErrorSummary('SliverGeometry has a paintOffset that exceeds the remainingPaintExtent from the constraints.' ), |
1348 | describeForError('The render object whose geometry violates the constraints is the following' ), |
1349 | ..._debugCompareFloats( |
1350 | 'remainingPaintExtent' , constraints.remainingPaintExtent, |
1351 | 'paintOrigin + paintExtent' , geometry!.paintOrigin + geometry!.paintExtent, |
1352 | ), |
1353 | ErrorDescription( |
1354 | 'The paintOrigin and paintExtent must cause the child sliver to paint ' |
1355 | 'within the viewport, and so cannot exceed the remainingPaintExtent.' , |
1356 | ), |
1357 | ]); |
1358 | } |
1359 | return true; |
1360 | }()); |
1361 | } |
1362 | |
1363 | @override |
1364 | void performResize() { |
1365 | assert(false); |
1366 | } |
1367 | |
1368 | /// For a center sliver, the distance before the absolute zero scroll offset |
1369 | /// that this sliver can cover. |
1370 | /// |
1371 | /// For example, if an [AxisDirection.down] viewport with an |
1372 | /// [RenderViewport.anchor] of 0.5 has a single sliver with a height of 100.0 |
1373 | /// and its [centerOffsetAdjustment] returns 50.0, then the sliver will be |
1374 | /// centered in the viewport when the scroll offset is 0.0. |
1375 | /// |
1376 | /// The distance here is in the opposite direction of the |
1377 | /// [RenderViewport.axisDirection], so values will typically be positive. |
1378 | double get centerOffsetAdjustment => 0.0; |
1379 | |
1380 | /// Determines the set of render objects located at the given position. |
1381 | /// |
1382 | /// Returns true if the given point is contained in this render object or one |
1383 | /// of its descendants. Adds any render objects that contain the point to the |
1384 | /// given hit test result. |
1385 | /// |
1386 | /// The caller is responsible for providing the position in the local |
1387 | /// coordinate space of the callee. The callee is responsible for checking |
1388 | /// whether the given position is within its bounds. |
1389 | /// |
1390 | /// Hit testing requires layout to be up-to-date but does not require painting |
1391 | /// to be up-to-date. That means a render object can rely upon [performLayout] |
1392 | /// having been called in [hitTest] but cannot rely upon [paint] having been |
1393 | /// called. For example, a render object might be a child of a [RenderOpacity] |
1394 | /// object, which calls [hitTest] on its children when its opacity is zero |
1395 | /// even through it does not [paint] its children. |
1396 | /// |
1397 | /// ## Coordinates for RenderSliver objects |
1398 | /// |
1399 | /// The `mainAxisPosition` is the distance in the [AxisDirection] (after |
1400 | /// applying the [GrowthDirection]) from the edge of the sliver's painted |
1401 | /// area. This can be an unusual direction, for example in the |
1402 | /// [AxisDirection.up] case this is a distance from the _bottom_ of the |
1403 | /// sliver's painted area. |
1404 | /// |
1405 | /// The `crossAxisPosition` is the distance in the other axis. If the cross |
1406 | /// axis is horizontal (i.e. the [SliverConstraints.axisDirection] is either |
1407 | /// [AxisDirection.down] or [AxisDirection.up]), then the `crossAxisPosition` |
1408 | /// is a distance from the left edge of the sliver. If the cross axis is |
1409 | /// vertical (i.e. the [SliverConstraints.axisDirection] is either |
1410 | /// [AxisDirection.right] or [AxisDirection.left]), then the |
1411 | /// `crossAxisPosition` is a distance from the top edge of the sliver. |
1412 | /// |
1413 | /// ## Implementing hit testing for slivers |
1414 | /// |
1415 | /// The most straight-forward way to implement hit testing for a new sliver |
1416 | /// render object is to override its [hitTestSelf] and [hitTestChildren] |
1417 | /// methods. |
1418 | bool hitTest(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) { |
1419 | if (mainAxisPosition >= 0.0 && mainAxisPosition < geometry!.hitTestExtent && |
1420 | crossAxisPosition >= 0.0 && crossAxisPosition < constraints.crossAxisExtent) { |
1421 | if (hitTestChildren(result, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition) || |
1422 | hitTestSelf(mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition)) { |
1423 | result.add(SliverHitTestEntry( |
1424 | this, |
1425 | mainAxisPosition: mainAxisPosition, |
1426 | crossAxisPosition: crossAxisPosition, |
1427 | )); |
1428 | return true; |
1429 | } |
1430 | } |
1431 | return false; |
1432 | } |
1433 | |
1434 | /// Override this method if this render object can be hit even if its |
1435 | /// children were not hit. |
1436 | /// |
1437 | /// Used by [hitTest]. If you override [hitTest] and do not call this |
1438 | /// function, then you don't need to implement this function. |
1439 | /// |
1440 | /// For a discussion of the semantics of the arguments, see [hitTest]. |
1441 | @protected |
1442 | bool hitTestSelf({ required double mainAxisPosition, required double crossAxisPosition }) => false; |
1443 | |
1444 | /// Override this method to check whether any children are located at the |
1445 | /// given position. |
1446 | /// |
1447 | /// Typically children should be hit-tested in reverse paint order so that |
1448 | /// hit tests at locations where children overlap hit the child that is |
1449 | /// visually "on top" (i.e., paints later). |
1450 | /// |
1451 | /// Used by [hitTest]. If you override [hitTest] and do not call this |
1452 | /// function, then you don't need to implement this function. |
1453 | /// |
1454 | /// For a discussion of the semantics of the arguments, see [hitTest]. |
1455 | @protected |
1456 | bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) => false; |
1457 | |
1458 | /// Computes the portion of the region from `from` to `to` that is visible, |
1459 | /// assuming that only the region from the [SliverConstraints.scrollOffset] |
1460 | /// that is [SliverConstraints.remainingPaintExtent] high is visible, and that |
1461 | /// the relationship between scroll offsets and paint offsets is linear. |
1462 | /// |
1463 | /// For example, if the constraints have a scroll offset of 100 and a |
1464 | /// remaining paint extent of 100, and the arguments to this method describe |
1465 | /// the region 50..150, then the returned value would be 50 (from scroll |
1466 | /// offset 100 to scroll offset 150). |
1467 | /// |
1468 | /// This method is not useful if there is not a 1:1 relationship between |
1469 | /// consumed scroll offset and consumed paint extent. For example, if the |
1470 | /// sliver always paints the same amount but consumes a scroll offset extent |
1471 | /// that is proportional to the [SliverConstraints.scrollOffset], then this |
1472 | /// function's results will not be consistent. |
1473 | // This could be a static method but isn't, because it would be less convenient |
1474 | // to call it from subclasses if it was. |
1475 | double calculatePaintOffset(SliverConstraints constraints, { required double from, required double to }) { |
1476 | assert(from <= to); |
1477 | final double a = constraints.scrollOffset; |
1478 | final double b = constraints.scrollOffset + constraints.remainingPaintExtent; |
1479 | // the clamp on the next line is to avoid floating point rounding errors |
1480 | return clampDouble(clampDouble(to, a, b) - clampDouble(from, a, b), 0.0, constraints.remainingPaintExtent); |
1481 | } |
1482 | |
1483 | /// Computes the portion of the region from `from` to `to` that is within |
1484 | /// the cache extent of the viewport, assuming that only the region from the |
1485 | /// [SliverConstraints.cacheOrigin] that is |
1486 | /// [SliverConstraints.remainingCacheExtent] high is visible, and that |
1487 | /// the relationship between scroll offsets and paint offsets is linear. |
1488 | /// |
1489 | /// This method is not useful if there is not a 1:1 relationship between |
1490 | /// consumed scroll offset and consumed cache extent. |
1491 | double calculateCacheOffset(SliverConstraints constraints, { required double from, required double to }) { |
1492 | assert(from <= to); |
1493 | final double a = constraints.scrollOffset + constraints.cacheOrigin; |
1494 | final double b = constraints.scrollOffset + constraints.remainingCacheExtent; |
1495 | // the clamp on the next line is to avoid floating point rounding errors |
1496 | return clampDouble(clampDouble(to, a, b) - clampDouble(from, a, b), 0.0, constraints.remainingCacheExtent); |
1497 | } |
1498 | |
1499 | /// Returns the distance from the leading _visible_ edge of the sliver to the |
1500 | /// side of the given child closest to that edge. |
1501 | /// |
1502 | /// For example, if the [constraints] describe this sliver as having an axis |
1503 | /// direction of [AxisDirection.down], then this is the distance from the top |
1504 | /// of the visible portion of the sliver to the top of the child. On the other |
1505 | /// hand, if the [constraints] describe this sliver as having an axis |
1506 | /// direction of [AxisDirection.up], then this is the distance from the bottom |
1507 | /// of the visible portion of the sliver to the bottom of the child. In both |
1508 | /// cases, this is the direction of increasing |
1509 | /// [SliverConstraints.scrollOffset] and |
1510 | /// [SliverLogicalParentData.layoutOffset]. |
1511 | /// |
1512 | /// For children that are [RenderSliver]s, the leading edge of the _child_ |
1513 | /// will be the leading _visible_ edge of the child, not the part of the child |
1514 | /// that would locally be a scroll offset 0.0. For children that are not |
1515 | /// [RenderSliver]s, for example a [RenderBox] child, it's the actual distance |
1516 | /// to the edge of the box, since those boxes do not know how to handle being |
1517 | /// scrolled. |
1518 | /// |
1519 | /// This method differs from [childScrollOffset] in that |
1520 | /// [childMainAxisPosition] gives the distance from the leading _visible_ edge |
1521 | /// of the sliver whereas [childScrollOffset] gives the distance from the |
1522 | /// sliver's zero scroll offset. |
1523 | /// |
1524 | /// Calling this for a child that is not visible is not valid. |
1525 | @protected |
1526 | double childMainAxisPosition(covariant RenderObject child) { |
1527 | assert(() { |
1528 | throw FlutterError(' ${objectRuntimeType(this, 'RenderSliver' )} does not implement childPosition.' ); |
1529 | }()); |
1530 | return 0.0; |
1531 | } |
1532 | |
1533 | /// Returns the distance along the cross axis from the zero of the cross axis |
1534 | /// in this sliver's [paint] coordinate space to the nearest side of the given |
1535 | /// child. |
1536 | /// |
1537 | /// For example, if the [constraints] describe this sliver as having an axis |
1538 | /// direction of [AxisDirection.down], then this is the distance from the left |
1539 | /// of the sliver to the left of the child. Similarly, if the [constraints] |
1540 | /// describe this sliver as having an axis direction of [AxisDirection.up], |
1541 | /// then this is value is the same. If the axis direction is |
1542 | /// [AxisDirection.left] or [AxisDirection.right], then it is the distance |
1543 | /// from the top of the sliver to the top of the child. |
1544 | /// |
1545 | /// Calling this for a child that is not visible is not valid. |
1546 | @protected |
1547 | double childCrossAxisPosition(covariant RenderObject child) => 0.0; |
1548 | |
1549 | /// Returns the scroll offset for the leading edge of the given child. |
1550 | /// |
1551 | /// The `child` must be a child of this sliver. |
1552 | /// |
1553 | /// This method differs from [childMainAxisPosition] in that |
1554 | /// [childMainAxisPosition] gives the distance from the leading _visible_ edge |
1555 | /// of the sliver whereas [childScrollOffset] gives the distance from sliver's |
1556 | /// zero scroll offset. |
1557 | double? childScrollOffset(covariant RenderObject child) { |
1558 | assert(child.parent == this); |
1559 | return 0.0; |
1560 | } |
1561 | |
1562 | @override |
1563 | void applyPaintTransform(RenderObject child, Matrix4 transform) { |
1564 | assert(() { |
1565 | throw FlutterError(' ${objectRuntimeType(this, 'RenderSliver' )} does not implement applyPaintTransform.' ); |
1566 | }()); |
1567 | } |
1568 | |
1569 | /// This returns a [Size] with dimensions relative to the leading edge of the |
1570 | /// sliver, specifically the same offset that is given to the [paint] method. |
1571 | /// This means that the dimensions may be negative. |
1572 | /// |
1573 | /// This is only valid after [layout] has completed. |
1574 | /// |
1575 | /// See also: |
1576 | /// |
1577 | /// * [getAbsoluteSize], which returns absolute size. |
1578 | @protected |
1579 | Size getAbsoluteSizeRelativeToOrigin() { |
1580 | assert(geometry != null); |
1581 | assert(!debugNeedsLayout); |
1582 | switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
1583 | case AxisDirection.up: |
1584 | return Size(constraints.crossAxisExtent, -geometry!.paintExtent); |
1585 | case AxisDirection.right: |
1586 | return Size(geometry!.paintExtent, constraints.crossAxisExtent); |
1587 | case AxisDirection.down: |
1588 | return Size(constraints.crossAxisExtent, geometry!.paintExtent); |
1589 | case AxisDirection.left: |
1590 | return Size(-geometry!.paintExtent, constraints.crossAxisExtent); |
1591 | } |
1592 | } |
1593 | |
1594 | /// This returns the absolute [Size] of the sliver. |
1595 | /// |
1596 | /// The dimensions are always positive and calling this is only valid after |
1597 | /// [layout] has completed. |
1598 | /// |
1599 | /// See also: |
1600 | /// |
1601 | /// * [getAbsoluteSizeRelativeToOrigin], which returns the size relative to |
1602 | /// the leading edge of the sliver. |
1603 | @protected |
1604 | Size getAbsoluteSize() { |
1605 | assert(geometry != null); |
1606 | assert(!debugNeedsLayout); |
1607 | switch (constraints.axisDirection) { |
1608 | case AxisDirection.up: |
1609 | case AxisDirection.down: |
1610 | return Size(constraints.crossAxisExtent, geometry!.paintExtent); |
1611 | case AxisDirection.right: |
1612 | case AxisDirection.left: |
1613 | return Size(geometry!.paintExtent, constraints.crossAxisExtent); |
1614 | } |
1615 | } |
1616 | |
1617 | void _debugDrawArrow(Canvas canvas, Paint paint, Offset p0, Offset p1, GrowthDirection direction) { |
1618 | assert(() { |
1619 | if (p0 == p1) { |
1620 | return true; |
1621 | } |
1622 | assert(p0.dx == p1.dx || p0.dy == p1.dy); // must be axis-aligned |
1623 | final double d = (p1 - p0).distance * 0.2; |
1624 | final Offset temp; |
1625 | double dx1, dx2, dy1, dy2; |
1626 | switch (direction) { |
1627 | case GrowthDirection.forward: |
1628 | dx1 = dx2 = dy1 = dy2 = d; |
1629 | case GrowthDirection.reverse: |
1630 | temp = p0; |
1631 | p0 = p1; |
1632 | p1 = temp; |
1633 | dx1 = dx2 = dy1 = dy2 = -d; |
1634 | } |
1635 | if (p0.dx == p1.dx) { |
1636 | dx2 = -dx2; |
1637 | } else { |
1638 | dy2 = -dy2; |
1639 | } |
1640 | canvas.drawPath( |
1641 | Path() |
1642 | ..moveTo(p0.dx, p0.dy) |
1643 | ..lineTo(p1.dx, p1.dy) |
1644 | ..moveTo(p1.dx - dx1, p1.dy - dy1) |
1645 | ..lineTo(p1.dx, p1.dy) |
1646 | ..lineTo(p1.dx - dx2, p1.dy - dy2), |
1647 | paint, |
1648 | ); |
1649 | return true; |
1650 | }()); |
1651 | } |
1652 | |
1653 | @override |
1654 | void debugPaint(PaintingContext context, Offset offset) { |
1655 | assert(() { |
1656 | if (debugPaintSizeEnabled) { |
1657 | final double strokeWidth = math.min(4.0, geometry!.paintExtent / 30.0); |
1658 | final Paint paint = Paint() |
1659 | ..color = const Color(0xFF33CC33) |
1660 | ..strokeWidth = strokeWidth |
1661 | ..style = PaintingStyle.stroke |
1662 | ..maskFilter = MaskFilter.blur(BlurStyle.solid, strokeWidth); |
1663 | final double arrowExtent = geometry!.paintExtent; |
1664 | final double padding = math.max(2.0, strokeWidth); |
1665 | final Canvas canvas = context.canvas; |
1666 | canvas.drawCircle( |
1667 | offset.translate(padding, padding), |
1668 | padding * 0.5, |
1669 | paint, |
1670 | ); |
1671 | switch (constraints.axis) { |
1672 | case Axis.vertical: |
1673 | canvas.drawLine( |
1674 | offset, |
1675 | offset.translate(constraints.crossAxisExtent, 0.0), |
1676 | paint, |
1677 | ); |
1678 | _debugDrawArrow( |
1679 | canvas, |
1680 | paint, |
1681 | offset.translate(constraints.crossAxisExtent * 1.0 / 4.0, padding), |
1682 | offset.translate(constraints.crossAxisExtent * 1.0 / 4.0, arrowExtent - padding), |
1683 | constraints.normalizedGrowthDirection, |
1684 | ); |
1685 | _debugDrawArrow( |
1686 | canvas, |
1687 | paint, |
1688 | offset.translate(constraints.crossAxisExtent * 3.0 / 4.0, padding), |
1689 | offset.translate(constraints.crossAxisExtent * 3.0 / 4.0, arrowExtent - padding), |
1690 | constraints.normalizedGrowthDirection, |
1691 | ); |
1692 | case Axis.horizontal: |
1693 | canvas.drawLine( |
1694 | offset, |
1695 | offset.translate(0.0, constraints.crossAxisExtent), |
1696 | paint, |
1697 | ); |
1698 | _debugDrawArrow( |
1699 | canvas, |
1700 | paint, |
1701 | offset.translate(padding, constraints.crossAxisExtent * 1.0 / 4.0), |
1702 | offset.translate(arrowExtent - padding, constraints.crossAxisExtent * 1.0 / 4.0), |
1703 | constraints.normalizedGrowthDirection, |
1704 | ); |
1705 | _debugDrawArrow( |
1706 | canvas, |
1707 | paint, |
1708 | offset.translate(padding, constraints.crossAxisExtent * 3.0 / 4.0), |
1709 | offset.translate(arrowExtent - padding, constraints.crossAxisExtent * 3.0 / 4.0), |
1710 | constraints.normalizedGrowthDirection, |
1711 | ); |
1712 | } |
1713 | } |
1714 | return true; |
1715 | }()); |
1716 | } |
1717 | |
1718 | // This override exists only to change the type of the second argument. |
1719 | @override |
1720 | void handleEvent(PointerEvent event, SliverHitTestEntry entry) { } |
1721 | |
1722 | @override |
1723 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1724 | super.debugFillProperties(properties); |
1725 | properties.add(DiagnosticsProperty<SliverGeometry>('geometry' , geometry)); |
1726 | } |
1727 | } |
1728 | |
1729 | /// Mixin for [RenderSliver] subclasses that provides some utility functions. |
1730 | mixin RenderSliverHelpers implements RenderSliver { |
1731 | bool _getRightWayUp(SliverConstraints constraints) { |
1732 | bool rightWayUp; |
1733 | switch (constraints.axisDirection) { |
1734 | case AxisDirection.up: |
1735 | case AxisDirection.left: |
1736 | rightWayUp = false; |
1737 | case AxisDirection.down: |
1738 | case AxisDirection.right: |
1739 | rightWayUp = true; |
1740 | } |
1741 | switch (constraints.growthDirection) { |
1742 | case GrowthDirection.forward: |
1743 | break; |
1744 | case GrowthDirection.reverse: |
1745 | rightWayUp = !rightWayUp; |
1746 | } |
1747 | return rightWayUp; |
1748 | } |
1749 | |
1750 | /// Utility function for [hitTestChildren] for use when the children are |
1751 | /// [RenderBox] widgets. |
1752 | /// |
1753 | /// This function takes care of converting the position from the sliver |
1754 | /// coordinate system to the Cartesian coordinate system used by [RenderBox]. |
1755 | /// |
1756 | /// This function relies on [childMainAxisPosition] to determine the position of |
1757 | /// child in question. |
1758 | /// |
1759 | /// Calling this for a child that is not visible is not valid. |
1760 | @protected |
1761 | bool hitTestBoxChild(BoxHitTestResult result, RenderBox child, { required double mainAxisPosition, required double crossAxisPosition }) { |
1762 | final bool rightWayUp = _getRightWayUp(constraints); |
1763 | double delta = childMainAxisPosition(child); |
1764 | final double crossAxisDelta = childCrossAxisPosition(child); |
1765 | double absolutePosition = mainAxisPosition - delta; |
1766 | final double absoluteCrossAxisPosition = crossAxisPosition - crossAxisDelta; |
1767 | Offset paintOffset, transformedPosition; |
1768 | switch (constraints.axis) { |
1769 | case Axis.horizontal: |
1770 | if (!rightWayUp) { |
1771 | absolutePosition = child.size.width - absolutePosition; |
1772 | delta = geometry!.paintExtent - child.size.width - delta; |
1773 | } |
1774 | paintOffset = Offset(delta, crossAxisDelta); |
1775 | transformedPosition = Offset(absolutePosition, absoluteCrossAxisPosition); |
1776 | case Axis.vertical: |
1777 | if (!rightWayUp) { |
1778 | absolutePosition = child.size.height - absolutePosition; |
1779 | delta = geometry!.paintExtent - child.size.height - delta; |
1780 | } |
1781 | paintOffset = Offset(crossAxisDelta, delta); |
1782 | transformedPosition = Offset(absoluteCrossAxisPosition, absolutePosition); |
1783 | } |
1784 | return result.addWithOutOfBandPosition( |
1785 | paintOffset: paintOffset, |
1786 | hitTest: (BoxHitTestResult result) { |
1787 | return child.hitTest(result, position: transformedPosition); |
1788 | }, |
1789 | ); |
1790 | } |
1791 | |
1792 | /// Utility function for [applyPaintTransform] for use when the children are |
1793 | /// [RenderBox] widgets. |
1794 | /// |
1795 | /// This function turns the value returned by [childMainAxisPosition] and |
1796 | /// [childCrossAxisPosition]for the child in question into a translation that |
1797 | /// it then applies to the given matrix. |
1798 | /// |
1799 | /// Calling this for a child that is not visible is not valid. |
1800 | @protected |
1801 | void applyPaintTransformForBoxChild(RenderBox child, Matrix4 transform) { |
1802 | final bool rightWayUp = _getRightWayUp(constraints); |
1803 | double delta = childMainAxisPosition(child); |
1804 | final double crossAxisDelta = childCrossAxisPosition(child); |
1805 | switch (constraints.axis) { |
1806 | case Axis.horizontal: |
1807 | if (!rightWayUp) { |
1808 | delta = geometry!.paintExtent - child.size.width - delta; |
1809 | } |
1810 | transform.translate(delta, crossAxisDelta); |
1811 | case Axis.vertical: |
1812 | if (!rightWayUp) { |
1813 | delta = geometry!.paintExtent - child.size.height - delta; |
1814 | } |
1815 | transform.translate(crossAxisDelta, delta); |
1816 | } |
1817 | } |
1818 | } |
1819 | |
1820 | // ADAPTER FOR RENDER BOXES INSIDE SLIVERS |
1821 | // Transitions from the RenderSliver world to the RenderBox world. |
1822 | |
1823 | /// An abstract class for [RenderSliver]s that contains a single [RenderBox]. |
1824 | /// |
1825 | /// See also: |
1826 | /// |
1827 | /// * [RenderSliver], which explains more about the Sliver protocol. |
1828 | /// * [RenderBox], which explains more about the Box protocol. |
1829 | /// * [RenderSliverToBoxAdapter], which extends this class to size the child |
1830 | /// according to its preferred size. |
1831 | /// * [RenderSliverFillRemaining], which extends this class to size the child |
1832 | /// to fill the remaining space in the viewport. |
1833 | abstract class RenderSliverSingleBoxAdapter extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers { |
1834 | /// Creates a [RenderSliver] that wraps a [RenderBox]. |
1835 | RenderSliverSingleBoxAdapter({ |
1836 | RenderBox? child, |
1837 | }) { |
1838 | this.child = child; |
1839 | } |
1840 | |
1841 | @override |
1842 | void setupParentData(RenderObject child) { |
1843 | if (child.parentData is! SliverPhysicalParentData) { |
1844 | child.parentData = SliverPhysicalParentData(); |
1845 | } |
1846 | } |
1847 | |
1848 | /// Sets the [SliverPhysicalParentData.paintOffset] for the given child |
1849 | /// according to the [SliverConstraints.axisDirection] and |
1850 | /// [SliverConstraints.growthDirection] and the given geometry. |
1851 | @protected |
1852 | void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) { |
1853 | final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; |
1854 | switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
1855 | case AxisDirection.up: |
1856 | childParentData.paintOffset = Offset(0.0, -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset))); |
1857 | case AxisDirection.right: |
1858 | childParentData.paintOffset = Offset(-constraints.scrollOffset, 0.0); |
1859 | case AxisDirection.down: |
1860 | childParentData.paintOffset = Offset(0.0, -constraints.scrollOffset); |
1861 | case AxisDirection.left: |
1862 | childParentData.paintOffset = Offset(-(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)), 0.0); |
1863 | } |
1864 | } |
1865 | |
1866 | @override |
1867 | bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) { |
1868 | assert(geometry!.hitTestExtent > 0.0); |
1869 | if (child != null) { |
1870 | return hitTestBoxChild(BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition); |
1871 | } |
1872 | return false; |
1873 | } |
1874 | |
1875 | @override |
1876 | double childMainAxisPosition(RenderBox child) { |
1877 | return -constraints.scrollOffset; |
1878 | } |
1879 | |
1880 | @override |
1881 | void applyPaintTransform(RenderObject child, Matrix4 transform) { |
1882 | assert(child == this.child); |
1883 | final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; |
1884 | childParentData.applyPaintTransform(transform); |
1885 | } |
1886 | |
1887 | @override |
1888 | void paint(PaintingContext context, Offset offset) { |
1889 | if (child != null && geometry!.visible) { |
1890 | final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData; |
1891 | context.paintChild(child!, offset + childParentData.paintOffset); |
1892 | } |
1893 | } |
1894 | } |
1895 | |
1896 | /// A [RenderSliver] that contains a single [RenderBox]. |
1897 | /// |
1898 | /// The child will not be laid out if it is not visible. It is sized according |
1899 | /// to the child's preferences in the main axis, and with a tight constraint |
1900 | /// forcing it to the dimensions of the viewport in the cross axis. |
1901 | /// |
1902 | /// See also: |
1903 | /// |
1904 | /// * [RenderSliver], which explains more about the Sliver protocol. |
1905 | /// * [RenderBox], which explains more about the Box protocol. |
1906 | /// * [RenderViewport], which allows [RenderSliver] objects to be placed inside |
1907 | /// a [RenderBox] (the opposite of this class). |
1908 | class RenderSliverToBoxAdapter extends RenderSliverSingleBoxAdapter { |
1909 | /// Creates a [RenderSliver] that wraps a [RenderBox]. |
1910 | RenderSliverToBoxAdapter({ |
1911 | super.child, |
1912 | }); |
1913 | |
1914 | @override |
1915 | void performLayout() { |
1916 | if (child == null) { |
1917 | geometry = SliverGeometry.zero; |
1918 | return; |
1919 | } |
1920 | final SliverConstraints constraints = this.constraints; |
1921 | child!.layout(constraints.asBoxConstraints(), parentUsesSize: true); |
1922 | final double childExtent; |
1923 | switch (constraints.axis) { |
1924 | case Axis.horizontal: |
1925 | childExtent = child!.size.width; |
1926 | case Axis.vertical: |
1927 | childExtent = child!.size.height; |
1928 | } |
1929 | final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: childExtent); |
1930 | final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent); |
1931 | |
1932 | assert(paintedChildSize.isFinite); |
1933 | assert(paintedChildSize >= 0.0); |
1934 | geometry = SliverGeometry( |
1935 | scrollExtent: childExtent, |
1936 | paintExtent: paintedChildSize, |
1937 | cacheExtent: cacheExtent, |
1938 | maxPaintExtent: childExtent, |
1939 | hitTestExtent: paintedChildSize, |
1940 | hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0, |
1941 | ); |
1942 | setChildParentData(child!, constraints, geometry!); |
1943 | } |
1944 | } |
1945 | |