1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:math' as math; |
6 | |
7 | import 'package:flutter/animation.dart'; |
8 | import 'package:flutter/foundation.dart'; |
9 | import 'package:flutter/scheduler.dart'; |
10 | import 'package:flutter/semantics.dart'; |
11 | |
12 | import 'box.dart'; |
13 | import 'object.dart'; |
14 | import 'sliver.dart'; |
15 | import 'viewport.dart'; |
16 | import 'viewport_offset.dart'; |
17 | |
18 | // Trims the specified edges of the given `Rect` [original], so that they do not |
19 | // exceed the given values. |
20 | Rect? _trim( |
21 | Rect? original, { |
22 | double top = -double.infinity, |
23 | double right = double.infinity, |
24 | double bottom = double.infinity, |
25 | double left = -double.infinity, |
26 | }) => original?.intersect(Rect.fromLTRB(left, top, right, bottom)); |
27 | |
28 | /// Specifies how a stretched header is to trigger an [AsyncCallback]. |
29 | /// |
30 | /// See also: |
31 | /// |
32 | /// * [SliverAppBar], which creates a header that can be stretched into an |
33 | /// overscroll area and trigger a callback function. |
34 | class OverScrollHeaderStretchConfiguration { |
35 | /// Creates an object that specifies how a stretched header may activate an |
36 | /// [AsyncCallback]. |
37 | OverScrollHeaderStretchConfiguration({ |
38 | this.stretchTriggerOffset = 100.0, |
39 | this.onStretchTrigger, |
40 | }); |
41 | |
42 | /// The offset of overscroll required to trigger the [onStretchTrigger]. |
43 | final double stretchTriggerOffset; |
44 | |
45 | /// The callback function to be executed when a user over-scrolls to the |
46 | /// offset specified by [stretchTriggerOffset]. |
47 | final AsyncCallback? onStretchTrigger; |
48 | } |
49 | |
50 | /// {@template flutter.rendering.PersistentHeaderShowOnScreenConfiguration} |
51 | /// Specifies how a pinned header or a floating header should react to |
52 | /// [RenderObject.showOnScreen] calls. |
53 | /// {@endtemplate} |
54 | @immutable |
55 | class PersistentHeaderShowOnScreenConfiguration { |
56 | /// Creates an object that specifies how a pinned or floating persistent header |
57 | /// should behave in response to [RenderObject.showOnScreen] calls. |
58 | const PersistentHeaderShowOnScreenConfiguration({ |
59 | this.minShowOnScreenExtent = double.negativeInfinity, |
60 | this.maxShowOnScreenExtent = double.infinity, |
61 | }) : assert(minShowOnScreenExtent <= maxShowOnScreenExtent); |
62 | |
63 | /// The smallest the floating header can expand to in the main axis direction, |
64 | /// in response to a [RenderObject.showOnScreen] call, in addition to its |
65 | /// [RenderSliverPersistentHeader.minExtent]. |
66 | /// |
67 | /// When a floating persistent header is told to show a [Rect] on screen, it |
68 | /// may expand itself to accommodate the [Rect]. The minimum extent that is |
69 | /// allowed for such expansion is either |
70 | /// [RenderSliverPersistentHeader.minExtent] or [minShowOnScreenExtent], |
71 | /// whichever is larger. If the persistent header's current extent is already |
72 | /// larger than that maximum extent, it will remain unchanged. |
73 | /// |
74 | /// This parameter can be set to the persistent header's `maxExtent` (or |
75 | /// `double.infinity`) so the persistent header will always try to expand when |
76 | /// [RenderObject.showOnScreen] is called on it. |
77 | /// |
78 | /// Defaults to [double.negativeInfinity], must be less than or equal to |
79 | /// [maxShowOnScreenExtent]. Has no effect unless the persistent header is a |
80 | /// floating header. |
81 | final double minShowOnScreenExtent; |
82 | |
83 | /// The biggest the floating header can expand to in the main axis direction, |
84 | /// in response to a [RenderObject.showOnScreen] call, in addition to its |
85 | /// [RenderSliverPersistentHeader.maxExtent]. |
86 | /// |
87 | /// When a floating persistent header is told to show a [Rect] on screen, it |
88 | /// may expand itself to accommodate the [Rect]. The maximum extent that is |
89 | /// allowed for such expansion is either |
90 | /// [RenderSliverPersistentHeader.maxExtent] or [maxShowOnScreenExtent], |
91 | /// whichever is smaller. If the persistent header's current extent is already |
92 | /// larger than that maximum extent, it will remain unchanged. |
93 | /// |
94 | /// This parameter can be set to the persistent header's `minExtent` (or |
95 | /// `double.negativeInfinity`) so the persistent header will never try to |
96 | /// expand when [RenderObject.showOnScreen] is called on it. |
97 | /// |
98 | /// Defaults to [double.infinity], must be greater than or equal to |
99 | /// [minShowOnScreenExtent]. Has no effect unless the persistent header is a |
100 | /// floating header. |
101 | final double maxShowOnScreenExtent; |
102 | } |
103 | |
104 | /// A base class for slivers that have a [RenderBox] child which scrolls |
105 | /// normally, except that when it hits the leading edge (typically the top) of |
106 | /// the viewport, it shrinks to a minimum size ([minExtent]). |
107 | /// |
108 | /// This class primarily provides helpers for managing the child, in particular: |
109 | /// |
110 | /// * [layoutChild], which applies min and max extents and a scroll offset to |
111 | /// lay out the child. This is normally called from [performLayout]. |
112 | /// |
113 | /// * [childExtent], to convert the child's box layout dimensions to the sliver |
114 | /// geometry model. |
115 | /// |
116 | /// * hit testing, painting, and other details of the sliver protocol. |
117 | /// |
118 | /// Subclasses must implement [performLayout], [minExtent], and [maxExtent], and |
119 | /// typically also will implement [updateChild]. |
120 | abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers { |
121 | /// Creates a sliver that changes its size when scrolled to the start of the |
122 | /// viewport. |
123 | /// |
124 | /// This is an abstract class; this constructor only initializes the [child]. |
125 | RenderSliverPersistentHeader({ |
126 | RenderBox? child, |
127 | this.stretchConfiguration, |
128 | }) { |
129 | this.child = child; |
130 | } |
131 | |
132 | late double _lastStretchOffset; |
133 | |
134 | /// The biggest that this render object can become, in the main axis direction. |
135 | /// |
136 | /// This value should not be based on the child. If it changes, call |
137 | /// [markNeedsLayout]. |
138 | double get maxExtent; |
139 | |
140 | /// The smallest that this render object can become, in the main axis direction. |
141 | /// |
142 | /// If this is based on the intrinsic dimensions of the child, the child |
143 | /// should be measured during [updateChild] and the value cached and returned |
144 | /// here. The [updateChild] method will automatically be invoked any time the |
145 | /// child changes its intrinsic dimensions. |
146 | double get minExtent; |
147 | |
148 | /// The dimension of the child in the main axis. |
149 | @protected |
150 | double get childExtent { |
151 | if (child == null) { |
152 | return 0.0; |
153 | } |
154 | assert(child!.hasSize); |
155 | switch (constraints.axis) { |
156 | case Axis.vertical: |
157 | return child!.size.height; |
158 | case Axis.horizontal: |
159 | return child!.size.width; |
160 | } |
161 | } |
162 | |
163 | bool _needsUpdateChild = true; |
164 | double _lastShrinkOffset = 0.0; |
165 | bool _lastOverlapsContent = false; |
166 | |
167 | /// Defines the parameters used to execute an [AsyncCallback] when a |
168 | /// stretching header over-scrolls. |
169 | /// |
170 | /// If [stretchConfiguration] is null then callback is not triggered. |
171 | /// |
172 | /// See also: |
173 | /// |
174 | /// * [SliverAppBar], which creates a header that can stretched into an |
175 | /// overscroll area and trigger a callback function. |
176 | OverScrollHeaderStretchConfiguration? stretchConfiguration; |
177 | |
178 | /// Update the child render object if necessary. |
179 | /// |
180 | /// Called before the first layout, any time [markNeedsLayout] is called, and |
181 | /// any time the scroll offset changes. The `shrinkOffset` is the difference |
182 | /// between the [maxExtent] and the current size. Zero means the header is |
183 | /// fully expanded, any greater number up to [maxExtent] means that the header |
184 | /// has been scrolled by that much. The `overlapsContent` argument is true if |
185 | /// the sliver's leading edge is beyond its normal place in the viewport |
186 | /// contents, and false otherwise. It may still paint beyond its normal place |
187 | /// if the [minExtent] after this call is greater than the amount of space that |
188 | /// would normally be left. |
189 | /// |
190 | /// The render object will size itself to the larger of (a) the [maxExtent] |
191 | /// minus the child's intrinsic height and (b) the [maxExtent] minus the |
192 | /// shrink offset. |
193 | /// |
194 | /// When this method is called by [layoutChild], the [child] can be set, |
195 | /// mutated, or replaced. (It should not be called outside [layoutChild].) |
196 | /// |
197 | /// Any time this method would mutate the child, call [markNeedsLayout]. |
198 | @protected |
199 | void updateChild(double shrinkOffset, bool overlapsContent) { } |
200 | |
201 | @override |
202 | void markNeedsLayout() { |
203 | // This is automatically called whenever the child's intrinsic dimensions |
204 | // change, at which point we should remeasure them during the next layout. |
205 | _needsUpdateChild = true; |
206 | super.markNeedsLayout(); |
207 | } |
208 | |
209 | /// Lays out the [child]. |
210 | /// |
211 | /// This is called by [performLayout]. It applies the given `scrollOffset` |
212 | /// (which need not match the offset given by the [constraints]) and the |
213 | /// `maxExtent` (which need not match the value returned by the [maxExtent] |
214 | /// getter). |
215 | /// |
216 | /// The `overlapsContent` argument is passed to [updateChild]. |
217 | @protected |
218 | void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent = false }) { |
219 | final double shrinkOffset = math.min(scrollOffset, maxExtent); |
220 | if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) { |
221 | invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { |
222 | assert(constraints == this.constraints); |
223 | updateChild(shrinkOffset, overlapsContent); |
224 | }); |
225 | _lastShrinkOffset = shrinkOffset; |
226 | _lastOverlapsContent = overlapsContent; |
227 | _needsUpdateChild = false; |
228 | } |
229 | assert(() { |
230 | if (minExtent <= maxExtent) { |
231 | return true; |
232 | } |
233 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
234 | ErrorSummary('The maxExtent for this $runtimeType is less than its minExtent.' ), |
235 | DoubleProperty('The specified maxExtent was' , maxExtent), |
236 | DoubleProperty('The specified minExtent was' , minExtent), |
237 | ]); |
238 | }()); |
239 | double stretchOffset = 0.0; |
240 | if (stretchConfiguration != null && constraints.scrollOffset == 0.0) { |
241 | stretchOffset += constraints.overlap.abs(); |
242 | } |
243 | |
244 | child?.layout( |
245 | constraints.asBoxConstraints( |
246 | maxExtent: math.max(minExtent, maxExtent - shrinkOffset) + stretchOffset, |
247 | ), |
248 | parentUsesSize: true, |
249 | ); |
250 | |
251 | if (stretchConfiguration != null && |
252 | stretchConfiguration!.onStretchTrigger != null && |
253 | stretchOffset >= stretchConfiguration!.stretchTriggerOffset && |
254 | _lastStretchOffset <= stretchConfiguration!.stretchTriggerOffset) { |
255 | stretchConfiguration!.onStretchTrigger!(); |
256 | } |
257 | _lastStretchOffset = stretchOffset; |
258 | } |
259 | |
260 | /// Returns the distance from the leading _visible_ edge of the sliver to the |
261 | /// side of the child closest to that edge, in the scroll axis direction. |
262 | /// |
263 | /// For example, if the [constraints] describe this sliver as having an axis |
264 | /// direction of [AxisDirection.down], then this is the distance from the top |
265 | /// of the visible portion of the sliver to the top of the child. If the child |
266 | /// is scrolled partially off the top of the viewport, then this will be |
267 | /// negative. On the other hand, if the [constraints] describe this sliver as |
268 | /// having an axis direction of [AxisDirection.up], then this is the distance |
269 | /// from the bottom of the visible portion of the sliver to the bottom of the |
270 | /// child. In both cases, this is the direction of increasing |
271 | /// [SliverConstraints.scrollOffset]. |
272 | /// |
273 | /// Calling this when the child is not visible is not valid. |
274 | /// |
275 | /// The argument must be the value of the [child] property. |
276 | /// |
277 | /// This must be implemented by [RenderSliverPersistentHeader] subclasses. |
278 | /// |
279 | /// If there is no child, this should return 0.0. |
280 | @override |
281 | double childMainAxisPosition(covariant RenderObject child) => super.childMainAxisPosition(child); |
282 | |
283 | @override |
284 | bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) { |
285 | assert(geometry!.hitTestExtent > 0.0); |
286 | if (child != null) { |
287 | return hitTestBoxChild(BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition); |
288 | } |
289 | return false; |
290 | } |
291 | |
292 | @override |
293 | void applyPaintTransform(RenderObject child, Matrix4 transform) { |
294 | assert(child == this.child); |
295 | applyPaintTransformForBoxChild(child as RenderBox, transform); |
296 | } |
297 | |
298 | @override |
299 | void paint(PaintingContext context, Offset offset) { |
300 | if (child != null && geometry!.visible) { |
301 | switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
302 | case AxisDirection.up: |
303 | offset += Offset(0.0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent); |
304 | case AxisDirection.down: |
305 | offset += Offset(0.0, childMainAxisPosition(child!)); |
306 | case AxisDirection.left: |
307 | offset += Offset(geometry!.paintExtent - childMainAxisPosition(child!) - childExtent, 0.0); |
308 | case AxisDirection.right: |
309 | offset += Offset(childMainAxisPosition(child!), 0.0); |
310 | } |
311 | context.paintChild(child!, offset); |
312 | } |
313 | } |
314 | |
315 | @override |
316 | void describeSemanticsConfiguration(SemanticsConfiguration config) { |
317 | super.describeSemanticsConfiguration(config); |
318 | config.addTagForChildren(RenderViewport.excludeFromScrolling); |
319 | } |
320 | |
321 | @override |
322 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
323 | super.debugFillProperties(properties); |
324 | properties.add(DoubleProperty.lazy('maxExtent' , () => maxExtent)); |
325 | properties.add(DoubleProperty.lazy('child position' , () => childMainAxisPosition(child!))); |
326 | } |
327 | } |
328 | |
329 | /// A sliver with a [RenderBox] child which scrolls normally, except that when |
330 | /// it hits the leading edge (typically the top) of the viewport, it shrinks to |
331 | /// a minimum size before continuing to scroll. |
332 | /// |
333 | /// This sliver makes no effort to avoid overlapping other content. |
334 | abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader { |
335 | /// Creates a sliver that shrinks when it hits the start of the viewport, then |
336 | /// scrolls off. |
337 | RenderSliverScrollingPersistentHeader({ |
338 | super.child, |
339 | super.stretchConfiguration, |
340 | }); |
341 | |
342 | // Distance from our leading edge to the child's leading edge, in the axis |
343 | // direction. Negative if we're scrolled off the top. |
344 | double? _childPosition; |
345 | |
346 | /// Updates [geometry], and returns the new value for [childMainAxisPosition]. |
347 | /// |
348 | /// This is used by [performLayout]. |
349 | @protected |
350 | double updateGeometry() { |
351 | double stretchOffset = 0.0; |
352 | if (stretchConfiguration != null) { |
353 | stretchOffset += constraints.overlap.abs(); |
354 | } |
355 | final double maxExtent = this.maxExtent; |
356 | final double paintExtent = maxExtent - constraints.scrollOffset; |
357 | geometry = SliverGeometry( |
358 | scrollExtent: maxExtent, |
359 | paintOrigin: math.min(constraints.overlap, 0.0), |
360 | paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent), |
361 | maxPaintExtent: maxExtent + stretchOffset, |
362 | hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
363 | ); |
364 | return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent); |
365 | } |
366 | |
367 | |
368 | @override |
369 | void performLayout() { |
370 | final SliverConstraints constraints = this.constraints; |
371 | final double maxExtent = this.maxExtent; |
372 | layoutChild(constraints.scrollOffset, maxExtent); |
373 | final double paintExtent = maxExtent - constraints.scrollOffset; |
374 | geometry = SliverGeometry( |
375 | scrollExtent: maxExtent, |
376 | paintOrigin: math.min(constraints.overlap, 0.0), |
377 | paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent), |
378 | maxPaintExtent: maxExtent, |
379 | hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
380 | ); |
381 | _childPosition = updateGeometry(); |
382 | } |
383 | |
384 | @override |
385 | double childMainAxisPosition(RenderBox child) { |
386 | assert(child == this.child); |
387 | assert(_childPosition != null); |
388 | return _childPosition!; |
389 | } |
390 | } |
391 | |
392 | /// A sliver with a [RenderBox] child which never scrolls off the viewport in |
393 | /// the positive scroll direction, and which first scrolls on at a full size but |
394 | /// then shrinks as the viewport continues to scroll. |
395 | /// |
396 | /// This sliver avoids overlapping other earlier slivers where possible. |
397 | abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader { |
398 | /// Creates a sliver that shrinks when it hits the start of the viewport, then |
399 | /// stays pinned there. |
400 | RenderSliverPinnedPersistentHeader({ |
401 | super.child, |
402 | super.stretchConfiguration, |
403 | this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(), |
404 | }); |
405 | |
406 | /// Specifies the persistent header's behavior when `showOnScreen` is called. |
407 | /// |
408 | /// If set to null, the persistent header will delegate the `showOnScreen` call |
409 | /// to it's parent [RenderObject]. |
410 | PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration; |
411 | |
412 | @override |
413 | void performLayout() { |
414 | final SliverConstraints constraints = this.constraints; |
415 | final double maxExtent = this.maxExtent; |
416 | final bool overlapsContent = constraints.overlap > 0.0; |
417 | layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: overlapsContent); |
418 | final double effectiveRemainingPaintExtent = math.max(0, constraints.remainingPaintExtent - constraints.overlap); |
419 | final double layoutExtent = clampDouble(maxExtent - constraints.scrollOffset, 0.0, effectiveRemainingPaintExtent); |
420 | final double stretchOffset = stretchConfiguration != null ? |
421 | constraints.overlap.abs() : |
422 | 0.0; |
423 | geometry = SliverGeometry( |
424 | scrollExtent: maxExtent, |
425 | paintOrigin: constraints.overlap, |
426 | paintExtent: math.min(childExtent, effectiveRemainingPaintExtent), |
427 | layoutExtent: layoutExtent, |
428 | maxPaintExtent: maxExtent + stretchOffset, |
429 | maxScrollObstructionExtent: minExtent, |
430 | cacheExtent: layoutExtent > 0.0 ? -constraints.cacheOrigin + layoutExtent : layoutExtent, |
431 | hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
432 | ); |
433 | } |
434 | |
435 | @override |
436 | double childMainAxisPosition(RenderBox child) => 0.0; |
437 | |
438 | @override |
439 | void showOnScreen({ |
440 | RenderObject? descendant, |
441 | Rect? rect, |
442 | Duration duration = Duration.zero, |
443 | Curve curve = Curves.ease, |
444 | }) { |
445 | final Rect? localBounds = descendant != null |
446 | ? MatrixUtils.transformRect(descendant.getTransformTo(this), rect ?? descendant.paintBounds) |
447 | : rect; |
448 | |
449 | Rect? newRect; |
450 | switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
451 | case AxisDirection.up: |
452 | newRect = _trim(localBounds, bottom: childExtent); |
453 | case AxisDirection.right: |
454 | newRect = _trim(localBounds, left: 0); |
455 | case AxisDirection.down: |
456 | newRect = _trim(localBounds, top: 0); |
457 | case AxisDirection.left: |
458 | newRect = _trim(localBounds, right: childExtent); |
459 | } |
460 | |
461 | super.showOnScreen( |
462 | descendant: this, |
463 | rect: newRect, |
464 | duration: duration, |
465 | curve: curve, |
466 | ); |
467 | } |
468 | } |
469 | |
470 | /// Specifies how a floating header is to be "snapped" (animated) into or out |
471 | /// of view. |
472 | /// |
473 | /// See also: |
474 | /// |
475 | /// * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and |
476 | /// [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which |
477 | /// start or stop the floating header's animation. |
478 | /// * [SliverAppBar], which creates a header that can be pinned, floating, |
479 | /// and snapped into view via the corresponding parameters. |
480 | class FloatingHeaderSnapConfiguration { |
481 | /// Creates an object that specifies how a floating header is to be "snapped" |
482 | /// (animated) into or out of view. |
483 | FloatingHeaderSnapConfiguration({ |
484 | this.curve = Curves.ease, |
485 | this.duration = const Duration(milliseconds: 300), |
486 | }); |
487 | |
488 | /// The snap animation curve. |
489 | final Curve curve; |
490 | |
491 | /// The snap animation's duration. |
492 | final Duration duration; |
493 | } |
494 | |
495 | /// A sliver with a [RenderBox] child which shrinks and scrolls like a |
496 | /// [RenderSliverScrollingPersistentHeader], but immediately comes back when the |
497 | /// user scrolls in the reverse direction. |
498 | /// |
499 | /// See also: |
500 | /// |
501 | /// * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks |
502 | /// to the start of the viewport rather than scrolling off. |
503 | abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader { |
504 | /// Creates a sliver that shrinks when it hits the start of the viewport, then |
505 | /// scrolls off, and comes back immediately when the user reverses the scroll |
506 | /// direction. |
507 | RenderSliverFloatingPersistentHeader({ |
508 | super.child, |
509 | TickerProvider? vsync, |
510 | this.snapConfiguration, |
511 | super.stretchConfiguration, |
512 | required this.showOnScreenConfiguration, |
513 | }) : _vsync = vsync; |
514 | |
515 | AnimationController? _controller; |
516 | late Animation<double> _animation; |
517 | double? _lastActualScrollOffset; |
518 | double? _effectiveScrollOffset; |
519 | // Important for pointer scrolling, which does not have the same concept of |
520 | // a hold and release scroll movement, like dragging. |
521 | // This keeps track of the last ScrollDirection when scrolling started. |
522 | ScrollDirection? _lastStartedScrollDirection; |
523 | |
524 | // Distance from our leading edge to the child's leading edge, in the axis |
525 | // direction. Negative if we're scrolled off the top. |
526 | double? _childPosition; |
527 | |
528 | @override |
529 | void detach() { |
530 | _controller?.dispose(); |
531 | _controller = null; // lazily recreated if we're reattached. |
532 | super.detach(); |
533 | } |
534 | |
535 | |
536 | /// A [TickerProvider] to use when animating the scroll position. |
537 | TickerProvider? get vsync => _vsync; |
538 | TickerProvider? _vsync; |
539 | set vsync(TickerProvider? value) { |
540 | if (value == _vsync) { |
541 | return; |
542 | } |
543 | _vsync = value; |
544 | if (value == null) { |
545 | _controller?.dispose(); |
546 | _controller = null; |
547 | } else { |
548 | _controller?.resync(value); |
549 | } |
550 | } |
551 | |
552 | /// Defines the parameters used to snap (animate) the floating header in and |
553 | /// out of view. |
554 | /// |
555 | /// If [snapConfiguration] is null then the floating header does not snap. |
556 | /// |
557 | /// See also: |
558 | /// |
559 | /// * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and |
560 | /// [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which |
561 | /// start or stop the floating header's animation. |
562 | /// * [SliverAppBar], which creates a header that can be pinned, floating, |
563 | /// and snapped into view via the corresponding parameters. |
564 | FloatingHeaderSnapConfiguration? snapConfiguration; |
565 | |
566 | /// {@macro flutter.rendering.PersistentHeaderShowOnScreenConfiguration} |
567 | /// |
568 | /// If set to null, the persistent header will delegate the `showOnScreen` call |
569 | /// to it's parent [RenderObject]. |
570 | PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration; |
571 | |
572 | /// Updates [geometry], and returns the new value for [childMainAxisPosition]. |
573 | /// |
574 | /// This is used by [performLayout]. |
575 | @protected |
576 | double updateGeometry() { |
577 | double stretchOffset = 0.0; |
578 | if (stretchConfiguration != null) { |
579 | stretchOffset += constraints.overlap.abs(); |
580 | } |
581 | final double maxExtent = this.maxExtent; |
582 | final double paintExtent = maxExtent - _effectiveScrollOffset!; |
583 | final double layoutExtent = maxExtent - constraints.scrollOffset; |
584 | geometry = SliverGeometry( |
585 | scrollExtent: maxExtent, |
586 | paintOrigin: math.min(constraints.overlap, 0.0), |
587 | paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent), |
588 | layoutExtent: clampDouble(layoutExtent, 0.0, constraints.remainingPaintExtent), |
589 | maxPaintExtent: maxExtent + stretchOffset, |
590 | hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
591 | ); |
592 | return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent); |
593 | } |
594 | |
595 | void _updateAnimation(Duration duration, double endValue, Curve curve) { |
596 | assert( |
597 | vsync != null, |
598 | 'vsync must not be null if the floating header changes size animatedly.' , |
599 | ); |
600 | |
601 | final AnimationController effectiveController = |
602 | _controller ??= AnimationController(vsync: vsync!, duration: duration) |
603 | ..addListener(() { |
604 | if (_effectiveScrollOffset == _animation.value) { |
605 | return; |
606 | } |
607 | _effectiveScrollOffset = _animation.value; |
608 | markNeedsLayout(); |
609 | }); |
610 | |
611 | _animation = effectiveController.drive( |
612 | Tween<double>( |
613 | begin: _effectiveScrollOffset, |
614 | end: endValue, |
615 | ).chain(CurveTween(curve: curve)), |
616 | ); |
617 | } |
618 | |
619 | /// Update the last known ScrollDirection when scrolling began. |
620 | // ignore: use_setters_to_change_properties, (API predates enforcing the lint) |
621 | void updateScrollStartDirection(ScrollDirection direction) { |
622 | _lastStartedScrollDirection = direction; |
623 | } |
624 | |
625 | /// If the header isn't already fully exposed, then scroll it into view. |
626 | void maybeStartSnapAnimation(ScrollDirection direction) { |
627 | final FloatingHeaderSnapConfiguration? snap = snapConfiguration; |
628 | if (snap == null) { |
629 | return; |
630 | } |
631 | if (direction == ScrollDirection.forward && _effectiveScrollOffset! <= 0.0) { |
632 | return; |
633 | } |
634 | if (direction == ScrollDirection.reverse && _effectiveScrollOffset! >= maxExtent) { |
635 | return; |
636 | } |
637 | |
638 | _updateAnimation( |
639 | snap.duration, |
640 | direction == ScrollDirection.forward ? 0.0 : maxExtent, |
641 | snap.curve, |
642 | ); |
643 | _controller?.forward(from: 0.0); |
644 | } |
645 | |
646 | /// If a header snap animation or a [showOnScreen] expand animation is underway |
647 | /// then stop it. |
648 | void maybeStopSnapAnimation(ScrollDirection direction) { |
649 | _controller?.stop(); |
650 | } |
651 | |
652 | @override |
653 | void performLayout() { |
654 | final SliverConstraints constraints = this.constraints; |
655 | final double maxExtent = this.maxExtent; |
656 | if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either |
657 | ((constraints.scrollOffset < _lastActualScrollOffset!) || // we are scrolling back, so should reveal, or |
658 | (_effectiveScrollOffset! < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate. |
659 | double delta = _lastActualScrollOffset! - constraints.scrollOffset; |
660 | |
661 | final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward |
662 | || (_lastStartedScrollDirection != null && _lastStartedScrollDirection == ScrollDirection.forward); |
663 | if (allowFloatingExpansion) { |
664 | if (_effectiveScrollOffset! > maxExtent) { |
665 | // We're scrolled off-screen, but should reveal, so pretend we're just at the limit. |
666 | _effectiveScrollOffset = maxExtent; |
667 | } |
668 | } else { |
669 | if (delta > 0.0) { |
670 | // Disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.) |
671 | delta = 0.0; |
672 | } |
673 | } |
674 | _effectiveScrollOffset = clampDouble(_effectiveScrollOffset! - delta, 0.0, constraints.scrollOffset); |
675 | } else { |
676 | _effectiveScrollOffset = constraints.scrollOffset; |
677 | } |
678 | final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset; |
679 | |
680 | layoutChild( |
681 | _effectiveScrollOffset!, |
682 | maxExtent, |
683 | overlapsContent: overlapsContent, |
684 | ); |
685 | _childPosition = updateGeometry(); |
686 | _lastActualScrollOffset = constraints.scrollOffset; |
687 | } |
688 | |
689 | @override |
690 | void showOnScreen({ |
691 | RenderObject? descendant, |
692 | Rect? rect, |
693 | Duration duration = Duration.zero, |
694 | Curve curve = Curves.ease, |
695 | }) { |
696 | final PersistentHeaderShowOnScreenConfiguration? showOnScreen = showOnScreenConfiguration; |
697 | if (showOnScreen == null) { |
698 | return super.showOnScreen(descendant: descendant, rect: rect, duration: duration, curve: curve); |
699 | } |
700 | |
701 | assert(child != null || descendant == null); |
702 | // We prefer the child's coordinate space (instead of the sliver's) because |
703 | // it's easier for us to convert the target rect into target extents: when |
704 | // the sliver is sitting above the leading edge (not possible with pinned |
705 | // headers), the leading edge of the sliver and the leading edge of the child |
706 | // will not be aligned. The only exception is when child is null (and thus |
707 | // descendant == null). |
708 | final Rect? childBounds = descendant != null |
709 | ? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds) |
710 | : rect; |
711 | |
712 | double targetExtent; |
713 | Rect? targetRect; |
714 | switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
715 | case AxisDirection.up: |
716 | targetExtent = childExtent - (childBounds?.top ?? 0); |
717 | targetRect = _trim(childBounds, bottom: childExtent); |
718 | case AxisDirection.right: |
719 | targetExtent = childBounds?.right ?? childExtent; |
720 | targetRect = _trim(childBounds, left: 0); |
721 | case AxisDirection.down: |
722 | targetExtent = childBounds?.bottom ?? childExtent; |
723 | targetRect = _trim(childBounds, top: 0); |
724 | case AxisDirection.left: |
725 | targetExtent = childExtent - (childBounds?.left ?? 0); |
726 | targetRect = _trim(childBounds, right: childExtent); |
727 | } |
728 | |
729 | // A stretch header can have a bigger childExtent than maxExtent. |
730 | final double effectiveMaxExtent = math.max(childExtent, maxExtent); |
731 | |
732 | targetExtent = clampDouble( |
733 | clampDouble( |
734 | targetExtent, |
735 | showOnScreen.minShowOnScreenExtent, |
736 | showOnScreen.maxShowOnScreenExtent, |
737 | ), |
738 | // Clamp the value back to the valid range after applying additional |
739 | // constraints. Contracting is not allowed. |
740 | childExtent, |
741 | effectiveMaxExtent); |
742 | |
743 | // Expands the header if needed, with animation. |
744 | if (targetExtent > childExtent && _controller?.status != AnimationStatus.forward) { |
745 | final double targetScrollOffset = maxExtent - targetExtent; |
746 | assert( |
747 | vsync != null, |
748 | 'vsync must not be null if the floating header changes size animatedly.' , |
749 | ); |
750 | _updateAnimation(duration, targetScrollOffset, curve); |
751 | _controller?.forward(from: 0.0); |
752 | } |
753 | |
754 | super.showOnScreen( |
755 | descendant: descendant == null ? this : child, |
756 | rect: targetRect, |
757 | duration: duration, |
758 | curve: curve, |
759 | ); |
760 | } |
761 | |
762 | @override |
763 | double childMainAxisPosition(RenderBox child) { |
764 | assert(child == this.child); |
765 | return _childPosition ?? 0.0; |
766 | } |
767 | |
768 | @override |
769 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
770 | super.debugFillProperties(properties); |
771 | properties.add(DoubleProperty('effective scroll offset' , _effectiveScrollOffset)); |
772 | } |
773 | } |
774 | |
775 | /// A sliver with a [RenderBox] child which shrinks and then remains pinned to |
776 | /// the start of the viewport like a [RenderSliverPinnedPersistentHeader], but |
777 | /// immediately grows when the user scrolls in the reverse direction. |
778 | /// |
779 | /// See also: |
780 | /// |
781 | /// * [RenderSliverFloatingPersistentHeader], which is similar but scrolls off |
782 | /// the top rather than sticking to it. |
783 | abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader { |
784 | /// Creates a sliver that shrinks when it hits the start of the viewport, then |
785 | /// stays pinned there, and grows immediately when the user reverses the |
786 | /// scroll direction. |
787 | RenderSliverFloatingPinnedPersistentHeader({ |
788 | super.child, |
789 | super.vsync, |
790 | super.snapConfiguration, |
791 | super.stretchConfiguration, |
792 | super.showOnScreenConfiguration, |
793 | }); |
794 | |
795 | @override |
796 | double updateGeometry() { |
797 | final double minExtent = this.minExtent; |
798 | final double minAllowedExtent = constraints.remainingPaintExtent > minExtent ? |
799 | minExtent : |
800 | constraints.remainingPaintExtent; |
801 | final double maxExtent = this.maxExtent; |
802 | final double paintExtent = maxExtent - _effectiveScrollOffset!; |
803 | final double clampedPaintExtent = clampDouble(paintExtent, |
804 | minAllowedExtent, |
805 | constraints.remainingPaintExtent, |
806 | ); |
807 | final double layoutExtent = maxExtent - constraints.scrollOffset; |
808 | final double stretchOffset = stretchConfiguration != null ? |
809 | constraints.overlap.abs() : |
810 | 0.0; |
811 | geometry = SliverGeometry( |
812 | scrollExtent: maxExtent, |
813 | paintOrigin: math.min(constraints.overlap, 0.0), |
814 | paintExtent: clampedPaintExtent, |
815 | layoutExtent: clampDouble(layoutExtent, 0.0, clampedPaintExtent), |
816 | maxPaintExtent: maxExtent + stretchOffset, |
817 | maxScrollObstructionExtent: minExtent, |
818 | hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
819 | ); |
820 | return 0.0; |
821 | } |
822 | } |
823 | |