| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| |
| import 'package:flutter/animation.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/semantics.dart'; |
| |
| import 'box.dart'; |
| import 'object.dart'; |
| import 'sliver.dart'; |
| import 'viewport.dart'; |
| import 'viewport_offset.dart'; |
| |
| // Trims the specified edges of the given `Rect` [original], so that they do not |
| // exceed the given values. |
| Rect? _trim( |
| Rect? original, { |
| double top = -double.infinity, |
| double right = double.infinity, |
| double bottom = double.infinity, |
| double left = -double.infinity, |
| }) => original?.intersect(Rect.fromLTRB(left, top, right, bottom)); |
| |
| /// Specifies how a stretched header is to trigger an [AsyncCallback]. |
| /// |
| /// See also: |
| /// |
| /// * [SliverAppBar], which creates a header that can be stretched into an |
| /// overscroll area and trigger a callback function. |
| class OverScrollHeaderStretchConfiguration { |
| /// Creates an object that specifies how a stretched header may activate an |
| /// [AsyncCallback]. |
| OverScrollHeaderStretchConfiguration({ |
| this.stretchTriggerOffset = 100.0, |
| this.onStretchTrigger, |
| }) : assert(stretchTriggerOffset != null); |
| |
| /// The offset of overscroll required to trigger the [onStretchTrigger]. |
| final double stretchTriggerOffset; |
| |
| /// The callback function to be executed when a user over-scrolls to the |
| /// offset specified by [stretchTriggerOffset]. |
| final AsyncCallback? onStretchTrigger; |
| } |
| |
| /// {@template flutter.rendering.PersistentHeaderShowOnScreenConfiguration} |
| /// Specifies how a pinned header or a floating header should react to |
| /// [RenderObject.showOnScreen] calls. |
| /// {@endtemplate} |
| @immutable |
| class PersistentHeaderShowOnScreenConfiguration { |
| /// Creates an object that specifies how a pinned or floating persistent header |
| /// should behave in response to [RenderObject.showOnScreen] calls. |
| const PersistentHeaderShowOnScreenConfiguration({ |
| this.minShowOnScreenExtent = double.negativeInfinity, |
| this.maxShowOnScreenExtent = double.infinity, |
| }) : assert(minShowOnScreenExtent <= maxShowOnScreenExtent); |
| |
| /// The smallest the floating header can expand to in the main axis direction, |
| /// in response to a [RenderObject.showOnScreen] call, in addition to its |
| /// [RenderSliverPersistentHeader.minExtent]. |
| /// |
| /// When a floating persistent header is told to show a [Rect] on screen, it |
| /// may expand itself to accommodate the [Rect]. The minimum extent that is |
| /// allowed for such expansion is either |
| /// [RenderSliverPersistentHeader.minExtent] or [minShowOnScreenExtent], |
| /// whichever is larger. If the persistent header's current extent is already |
| /// larger than that maximum extent, it will remain unchanged. |
| /// |
| /// This parameter can be set to the persistent header's `maxExtent` (or |
| /// `double.infinity`) so the persistent header will always try to expand when |
| /// [RenderObject.showOnScreen] is called on it. |
| /// |
| /// Defaults to [double.negativeInfinity], must be less than or equal to |
| /// [maxShowOnScreenExtent]. Has no effect unless the persistent header is a |
| /// floating header. |
| final double minShowOnScreenExtent; |
| |
| /// The biggest the floating header can expand to in the main axis direction, |
| /// in response to a [RenderObject.showOnScreen] call, in addition to its |
| /// [RenderSliverPersistentHeader.maxExtent]. |
| /// |
| /// When a floating persistent header is told to show a [Rect] on screen, it |
| /// may expand itself to accommodate the [Rect]. The maximum extent that is |
| /// allowed for such expansion is either |
| /// [RenderSliverPersistentHeader.maxExtent] or [maxShowOnScreenExtent], |
| /// whichever is smaller. If the persistent header's current extent is already |
| /// larger than that maximum extent, it will remain unchanged. |
| /// |
| /// This parameter can be set to the persistent header's `minExtent` (or |
| /// `double.negativeInfinity`) so the persistent header will never try to |
| /// expand when [RenderObject.showOnScreen] is called on it. |
| /// |
| /// Defaults to [double.infinity], must be greater than or equal to |
| /// [minShowOnScreenExtent]. Has no effect unless the persistent header is a |
| /// floating header. |
| final double maxShowOnScreenExtent; |
| } |
| |
| /// A base class for slivers that have a [RenderBox] child which scrolls |
| /// normally, except that when it hits the leading edge (typically the top) of |
| /// the viewport, it shrinks to a minimum size ([minExtent]). |
| /// |
| /// This class primarily provides helpers for managing the child, in particular: |
| /// |
| /// * [layoutChild], which applies min and max extents and a scroll offset to |
| /// lay out the child. This is normally called from [performLayout]. |
| /// |
| /// * [childExtent], to convert the child's box layout dimensions to the sliver |
| /// geometry model. |
| /// |
| /// * hit testing, painting, and other details of the sliver protocol. |
| /// |
| /// Subclasses must implement [performLayout], [minExtent], and [maxExtent], and |
| /// typically also will implement [updateChild]. |
| abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers { |
| /// Creates a sliver that changes its size when scrolled to the start of the |
| /// viewport. |
| /// |
| /// This is an abstract class; this constructor only initializes the [child]. |
| RenderSliverPersistentHeader({ |
| RenderBox? child, |
| this.stretchConfiguration, |
| }) { |
| this.child = child; |
| } |
| |
| late double _lastStretchOffset; |
| |
| /// The biggest that this render object can become, in the main axis direction. |
| /// |
| /// This value should not be based on the child. If it changes, call |
| /// [markNeedsLayout]. |
| double get maxExtent; |
| |
| /// The smallest that this render object can become, in the main axis direction. |
| /// |
| /// If this is based on the intrinsic dimensions of the child, the child |
| /// should be measured during [updateChild] and the value cached and returned |
| /// here. The [updateChild] method will automatically be invoked any time the |
| /// child changes its intrinsic dimensions. |
| double get minExtent; |
| |
| /// The dimension of the child in the main axis. |
| @protected |
| double get childExtent { |
| if (child == null) { |
| return 0.0; |
| } |
| assert(child!.hasSize); |
| assert(constraints.axis != null); |
| switch (constraints.axis) { |
| case Axis.vertical: |
| return child!.size.height; |
| case Axis.horizontal: |
| return child!.size.width; |
| } |
| } |
| |
| bool _needsUpdateChild = true; |
| double _lastShrinkOffset = 0.0; |
| bool _lastOverlapsContent = false; |
| |
| /// Defines the parameters used to execute an [AsyncCallback] when a |
| /// stretching header over-scrolls. |
| /// |
| /// If [stretchConfiguration] is null then callback is not triggered. |
| /// |
| /// See also: |
| /// |
| /// * [SliverAppBar], which creates a header that can stretched into an |
| /// overscroll area and trigger a callback function. |
| OverScrollHeaderStretchConfiguration? stretchConfiguration; |
| |
| /// Update the child render object if necessary. |
| /// |
| /// Called before the first layout, any time [markNeedsLayout] is called, and |
| /// any time the scroll offset changes. The `shrinkOffset` is the difference |
| /// between the [maxExtent] and the current size. Zero means the header is |
| /// fully expanded, any greater number up to [maxExtent] means that the header |
| /// has been scrolled by that much. The `overlapsContent` argument is true if |
| /// the sliver's leading edge is beyond its normal place in the viewport |
| /// contents, and false otherwise. It may still paint beyond its normal place |
| /// if the [minExtent] after this call is greater than the amount of space that |
| /// would normally be left. |
| /// |
| /// The render object will size itself to the larger of (a) the [maxExtent] |
| /// minus the child's intrinsic height and (b) the [maxExtent] minus the |
| /// shrink offset. |
| /// |
| /// When this method is called by [layoutChild], the [child] can be set, |
| /// mutated, or replaced. (It should not be called outside [layoutChild].) |
| /// |
| /// Any time this method would mutate the child, call [markNeedsLayout]. |
| @protected |
| void updateChild(double shrinkOffset, bool overlapsContent) { } |
| |
| @override |
| void markNeedsLayout() { |
| // This is automatically called whenever the child's intrinsic dimensions |
| // change, at which point we should remeasure them during the next layout. |
| _needsUpdateChild = true; |
| super.markNeedsLayout(); |
| } |
| |
| /// Lays out the [child]. |
| /// |
| /// This is called by [performLayout]. It applies the given `scrollOffset` |
| /// (which need not match the offset given by the [constraints]) and the |
| /// `maxExtent` (which need not match the value returned by the [maxExtent] |
| /// getter). |
| /// |
| /// The `overlapsContent` argument is passed to [updateChild]. |
| @protected |
| void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent = false }) { |
| assert(maxExtent != null); |
| final double shrinkOffset = math.min(scrollOffset, maxExtent); |
| if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) { |
| invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { |
| assert(constraints == this.constraints); |
| updateChild(shrinkOffset, overlapsContent); |
| }); |
| _lastShrinkOffset = shrinkOffset; |
| _lastOverlapsContent = overlapsContent; |
| _needsUpdateChild = false; |
| } |
| assert(minExtent != null); |
| assert(() { |
| if (minExtent <= maxExtent) { |
| return true; |
| } |
| throw FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary('The maxExtent for this $runtimeType is less than its minExtent.'), |
| DoubleProperty('The specified maxExtent was', maxExtent), |
| DoubleProperty('The specified minExtent was', minExtent), |
| ]); |
| }()); |
| double stretchOffset = 0.0; |
| if (stretchConfiguration != null && constraints.scrollOffset == 0.0) { |
| stretchOffset += constraints.overlap.abs(); |
| } |
| |
| child?.layout( |
| constraints.asBoxConstraints( |
| maxExtent: math.max(minExtent, maxExtent - shrinkOffset) + stretchOffset, |
| ), |
| parentUsesSize: true, |
| ); |
| |
| if (stretchConfiguration != null && |
| stretchConfiguration!.onStretchTrigger != null && |
| stretchOffset >= stretchConfiguration!.stretchTriggerOffset && |
| _lastStretchOffset <= stretchConfiguration!.stretchTriggerOffset) { |
| stretchConfiguration!.onStretchTrigger!(); |
| } |
| _lastStretchOffset = stretchOffset; |
| } |
| |
| /// Returns the distance from the leading _visible_ edge of the sliver to the |
| /// side of the child closest to that edge, in the scroll axis direction. |
| /// |
| /// For example, if the [constraints] describe this sliver as having an axis |
| /// direction of [AxisDirection.down], then this is the distance from the top |
| /// of the visible portion of the sliver to the top of the child. If the child |
| /// is scrolled partially off the top of the viewport, then this will be |
| /// negative. On the other hand, if the [constraints] describe this sliver as |
| /// having an axis direction of [AxisDirection.up], then this is the distance |
| /// from the bottom of the visible portion of the sliver to the bottom of the |
| /// child. In both cases, this is the direction of increasing |
| /// [SliverConstraints.scrollOffset]. |
| /// |
| /// Calling this when the child is not visible is not valid. |
| /// |
| /// The argument must be the value of the [child] property. |
| /// |
| /// This must be implemented by [RenderSliverPersistentHeader] subclasses. |
| /// |
| /// If there is no child, this should return 0.0. |
| @override |
| double childMainAxisPosition(covariant RenderObject child) => super.childMainAxisPosition(child); |
| |
| @override |
| bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) { |
| assert(geometry!.hitTestExtent > 0.0); |
| if (child != null) { |
| return hitTestBoxChild(BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition); |
| } |
| return false; |
| } |
| |
| @override |
| void applyPaintTransform(RenderObject child, Matrix4 transform) { |
| assert(child != null); |
| assert(child == this.child); |
| applyPaintTransformForBoxChild(child as RenderBox, transform); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (child != null && geometry!.visible) { |
| assert(constraints.axisDirection != null); |
| switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
| case AxisDirection.up: |
| offset += Offset(0.0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent); |
| break; |
| case AxisDirection.down: |
| offset += Offset(0.0, childMainAxisPosition(child!)); |
| break; |
| case AxisDirection.left: |
| offset += Offset(geometry!.paintExtent - childMainAxisPosition(child!) - childExtent, 0.0); |
| break; |
| case AxisDirection.right: |
| offset += Offset(childMainAxisPosition(child!), 0.0); |
| break; |
| } |
| context.paintChild(child!, offset); |
| } |
| } |
| |
| @override |
| void describeSemanticsConfiguration(SemanticsConfiguration config) { |
| super.describeSemanticsConfiguration(config); |
| config.addTagForChildren(RenderViewport.excludeFromScrolling); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DoubleProperty.lazy('maxExtent', () => maxExtent)); |
| properties.add(DoubleProperty.lazy('child position', () => childMainAxisPosition(child!))); |
| } |
| } |
| |
| /// A sliver with a [RenderBox] child which scrolls normally, except that when |
| /// it hits the leading edge (typically the top) of the viewport, it shrinks to |
| /// a minimum size before continuing to scroll. |
| /// |
| /// This sliver makes no effort to avoid overlapping other content. |
| abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader { |
| /// Creates a sliver that shrinks when it hits the start of the viewport, then |
| /// scrolls off. |
| RenderSliverScrollingPersistentHeader({ |
| super.child, |
| super.stretchConfiguration, |
| }); |
| |
| // Distance from our leading edge to the child's leading edge, in the axis |
| // direction. Negative if we're scrolled off the top. |
| double? _childPosition; |
| |
| /// Updates [geometry], and returns the new value for [childMainAxisPosition]. |
| /// |
| /// This is used by [performLayout]. |
| @protected |
| double updateGeometry() { |
| double stretchOffset = 0.0; |
| if (stretchConfiguration != null) { |
| stretchOffset += constraints.overlap.abs(); |
| } |
| final double maxExtent = this.maxExtent; |
| final double paintExtent = maxExtent - constraints.scrollOffset; |
| geometry = SliverGeometry( |
| scrollExtent: maxExtent, |
| paintOrigin: math.min(constraints.overlap, 0.0), |
| paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent), |
| maxPaintExtent: maxExtent + stretchOffset, |
| hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
| ); |
| return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent); |
| } |
| |
| |
| @override |
| void performLayout() { |
| final SliverConstraints constraints = this.constraints; |
| final double maxExtent = this.maxExtent; |
| layoutChild(constraints.scrollOffset, maxExtent); |
| final double paintExtent = maxExtent - constraints.scrollOffset; |
| geometry = SliverGeometry( |
| scrollExtent: maxExtent, |
| paintOrigin: math.min(constraints.overlap, 0.0), |
| paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent), |
| maxPaintExtent: maxExtent, |
| hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
| ); |
| _childPosition = updateGeometry(); |
| } |
| |
| @override |
| double childMainAxisPosition(RenderBox child) { |
| assert(child == this.child); |
| assert(_childPosition != null); |
| return _childPosition!; |
| } |
| } |
| |
| /// A sliver with a [RenderBox] child which never scrolls off the viewport in |
| /// the positive scroll direction, and which first scrolls on at a full size but |
| /// then shrinks as the viewport continues to scroll. |
| /// |
| /// This sliver avoids overlapping other earlier slivers where possible. |
| abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader { |
| /// Creates a sliver that shrinks when it hits the start of the viewport, then |
| /// stays pinned there. |
| RenderSliverPinnedPersistentHeader({ |
| super.child, |
| super.stretchConfiguration, |
| this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(), |
| }); |
| |
| /// Specifies the persistent header's behavior when `showOnScreen` is called. |
| /// |
| /// If set to null, the persistent header will delegate the `showOnScreen` call |
| /// to it's parent [RenderObject]. |
| PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration; |
| |
| @override |
| void performLayout() { |
| final SliverConstraints constraints = this.constraints; |
| final double maxExtent = this.maxExtent; |
| final bool overlapsContent = constraints.overlap > 0.0; |
| layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: overlapsContent); |
| final double effectiveRemainingPaintExtent = math.max(0, constraints.remainingPaintExtent - constraints.overlap); |
| final double layoutExtent = clampDouble(maxExtent - constraints.scrollOffset, 0.0, effectiveRemainingPaintExtent); |
| final double stretchOffset = stretchConfiguration != null ? |
| constraints.overlap.abs() : |
| 0.0; |
| geometry = SliverGeometry( |
| scrollExtent: maxExtent, |
| paintOrigin: constraints.overlap, |
| paintExtent: math.min(childExtent, effectiveRemainingPaintExtent), |
| layoutExtent: layoutExtent, |
| maxPaintExtent: maxExtent + stretchOffset, |
| maxScrollObstructionExtent: minExtent, |
| cacheExtent: layoutExtent > 0.0 ? -constraints.cacheOrigin + layoutExtent : layoutExtent, |
| hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
| ); |
| } |
| |
| @override |
| double childMainAxisPosition(RenderBox child) => 0.0; |
| |
| @override |
| void showOnScreen({ |
| RenderObject? descendant, |
| Rect? rect, |
| Duration duration = Duration.zero, |
| Curve curve = Curves.ease, |
| }) { |
| final Rect? localBounds = descendant != null |
| ? MatrixUtils.transformRect(descendant.getTransformTo(this), rect ?? descendant.paintBounds) |
| : rect; |
| |
| Rect? newRect; |
| switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
| case AxisDirection.up: |
| newRect = _trim(localBounds, bottom: childExtent); |
| break; |
| case AxisDirection.right: |
| newRect = _trim(localBounds, left: 0); |
| break; |
| case AxisDirection.down: |
| newRect = _trim(localBounds, top: 0); |
| break; |
| case AxisDirection.left: |
| newRect = _trim(localBounds, right: childExtent); |
| break; |
| } |
| |
| super.showOnScreen( |
| descendant: this, |
| rect: newRect, |
| duration: duration, |
| curve: curve, |
| ); |
| } |
| } |
| |
| /// Specifies how a floating header is to be "snapped" (animated) into or out |
| /// of view. |
| /// |
| /// See also: |
| /// |
| /// * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and |
| /// [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which |
| /// start or stop the floating header's animation. |
| /// * [SliverAppBar], which creates a header that can be pinned, floating, |
| /// and snapped into view via the corresponding parameters. |
| class FloatingHeaderSnapConfiguration { |
| /// Creates an object that specifies how a floating header is to be "snapped" |
| /// (animated) into or out of view. |
| FloatingHeaderSnapConfiguration({ |
| this.curve = Curves.ease, |
| this.duration = const Duration(milliseconds: 300), |
| }) : assert(curve != null), |
| assert(duration != null); |
| |
| /// The snap animation curve. |
| final Curve curve; |
| |
| /// The snap animation's duration. |
| final Duration duration; |
| } |
| |
| /// A sliver with a [RenderBox] child which shrinks and scrolls like a |
| /// [RenderSliverScrollingPersistentHeader], but immediately comes back when the |
| /// user scrolls in the reverse direction. |
| /// |
| /// See also: |
| /// |
| /// * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks |
| /// to the start of the viewport rather than scrolling off. |
| abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader { |
| /// Creates a sliver that shrinks when it hits the start of the viewport, then |
| /// scrolls off, and comes back immediately when the user reverses the scroll |
| /// direction. |
| RenderSliverFloatingPersistentHeader({ |
| super.child, |
| TickerProvider? vsync, |
| this.snapConfiguration, |
| super.stretchConfiguration, |
| required this.showOnScreenConfiguration, |
| }) : _vsync = vsync; |
| |
| AnimationController? _controller; |
| late Animation<double> _animation; |
| double? _lastActualScrollOffset; |
| double? _effectiveScrollOffset; |
| // Important for pointer scrolling, which does not have the same concept of |
| // a hold and release scroll movement, like dragging. |
| // This keeps track of the last ScrollDirection when scrolling started. |
| ScrollDirection? _lastStartedScrollDirection; |
| |
| // Distance from our leading edge to the child's leading edge, in the axis |
| // direction. Negative if we're scrolled off the top. |
| double? _childPosition; |
| |
| @override |
| void detach() { |
| _controller?.dispose(); |
| _controller = null; // lazily recreated if we're reattached. |
| super.detach(); |
| } |
| |
| |
| /// A [TickerProvider] to use when animating the scroll position. |
| TickerProvider? get vsync => _vsync; |
| TickerProvider? _vsync; |
| set vsync(TickerProvider? value) { |
| if (value == _vsync) { |
| return; |
| } |
| _vsync = value; |
| if (value == null) { |
| _controller?.dispose(); |
| _controller = null; |
| } else { |
| _controller?.resync(value); |
| } |
| } |
| |
| /// Defines the parameters used to snap (animate) the floating header in and |
| /// out of view. |
| /// |
| /// If [snapConfiguration] is null then the floating header does not snap. |
| /// |
| /// See also: |
| /// |
| /// * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and |
| /// [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which |
| /// start or stop the floating header's animation. |
| /// * [SliverAppBar], which creates a header that can be pinned, floating, |
| /// and snapped into view via the corresponding parameters. |
| FloatingHeaderSnapConfiguration? snapConfiguration; |
| |
| /// {@macro flutter.rendering.PersistentHeaderShowOnScreenConfiguration} |
| /// |
| /// If set to null, the persistent header will delegate the `showOnScreen` call |
| /// to it's parent [RenderObject]. |
| PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration; |
| |
| /// Updates [geometry], and returns the new value for [childMainAxisPosition]. |
| /// |
| /// This is used by [performLayout]. |
| @protected |
| double updateGeometry() { |
| double stretchOffset = 0.0; |
| if (stretchConfiguration != null) { |
| stretchOffset += constraints.overlap.abs(); |
| } |
| final double maxExtent = this.maxExtent; |
| final double paintExtent = maxExtent - _effectiveScrollOffset!; |
| final double layoutExtent = maxExtent - constraints.scrollOffset; |
| geometry = SliverGeometry( |
| scrollExtent: maxExtent, |
| paintOrigin: math.min(constraints.overlap, 0.0), |
| paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent), |
| layoutExtent: clampDouble(layoutExtent, 0.0, constraints.remainingPaintExtent), |
| maxPaintExtent: maxExtent + stretchOffset, |
| hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
| ); |
| return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent); |
| } |
| |
| void _updateAnimation(Duration duration, double endValue, Curve curve) { |
| assert(duration != null); |
| assert(endValue != null); |
| assert(curve != null); |
| assert( |
| vsync != null, |
| 'vsync must not be null if the floating header changes size animatedly.', |
| ); |
| |
| final AnimationController effectiveController = |
| _controller ??= AnimationController(vsync: vsync!, duration: duration) |
| ..addListener(() { |
| if (_effectiveScrollOffset == _animation.value) { |
| return; |
| } |
| _effectiveScrollOffset = _animation.value; |
| markNeedsLayout(); |
| }); |
| |
| _animation = effectiveController.drive( |
| Tween<double>( |
| begin: _effectiveScrollOffset, |
| end: endValue, |
| ).chain(CurveTween(curve: curve)), |
| ); |
| } |
| |
| /// Update the last known ScrollDirection when scrolling began. |
| // ignore: use_setters_to_change_properties, (API predates enforcing the lint) |
| void updateScrollStartDirection(ScrollDirection direction) { |
| _lastStartedScrollDirection = direction; |
| } |
| |
| /// If the header isn't already fully exposed, then scroll it into view. |
| void maybeStartSnapAnimation(ScrollDirection direction) { |
| final FloatingHeaderSnapConfiguration? snap = snapConfiguration; |
| if (snap == null) { |
| return; |
| } |
| if (direction == ScrollDirection.forward && _effectiveScrollOffset! <= 0.0) { |
| return; |
| } |
| if (direction == ScrollDirection.reverse && _effectiveScrollOffset! >= maxExtent) { |
| return; |
| } |
| |
| _updateAnimation( |
| snap.duration, |
| direction == ScrollDirection.forward ? 0.0 : maxExtent, |
| snap.curve, |
| ); |
| _controller?.forward(from: 0.0); |
| } |
| |
| /// If a header snap animation or a [showOnScreen] expand animation is underway |
| /// then stop it. |
| void maybeStopSnapAnimation(ScrollDirection direction) { |
| _controller?.stop(); |
| } |
| |
| @override |
| void performLayout() { |
| final SliverConstraints constraints = this.constraints; |
| final double maxExtent = this.maxExtent; |
| if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either |
| ((constraints.scrollOffset < _lastActualScrollOffset!) || // we are scrolling back, so should reveal, or |
| (_effectiveScrollOffset! < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate. |
| double delta = _lastActualScrollOffset! - constraints.scrollOffset; |
| |
| final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward |
| || (_lastStartedScrollDirection != null && _lastStartedScrollDirection == ScrollDirection.forward); |
| if (allowFloatingExpansion) { |
| if (_effectiveScrollOffset! > maxExtent) { |
| // We're scrolled off-screen, but should reveal, so pretend we're just at the limit. |
| _effectiveScrollOffset = maxExtent; |
| } |
| } else { |
| if (delta > 0.0) { |
| // Disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.) |
| delta = 0.0; |
| } |
| } |
| _effectiveScrollOffset = clampDouble(_effectiveScrollOffset! - delta, 0.0, constraints.scrollOffset); |
| } else { |
| _effectiveScrollOffset = constraints.scrollOffset; |
| } |
| final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset; |
| |
| layoutChild( |
| _effectiveScrollOffset!, |
| maxExtent, |
| overlapsContent: overlapsContent, |
| ); |
| _childPosition = updateGeometry(); |
| _lastActualScrollOffset = constraints.scrollOffset; |
| } |
| |
| @override |
| void showOnScreen({ |
| RenderObject? descendant, |
| Rect? rect, |
| Duration duration = Duration.zero, |
| Curve curve = Curves.ease, |
| }) { |
| final PersistentHeaderShowOnScreenConfiguration? showOnScreen = showOnScreenConfiguration; |
| if (showOnScreen == null) { |
| return super.showOnScreen(descendant: descendant, rect: rect, duration: duration, curve: curve); |
| } |
| |
| assert(child != null || descendant == null); |
| // We prefer the child's coordinate space (instead of the sliver's) because |
| // it's easier for us to convert the target rect into target extents: when |
| // the sliver is sitting above the leading edge (not possible with pinned |
| // headers), the leading edge of the sliver and the leading edge of the child |
| // will not be aligned. The only exception is when child is null (and thus |
| // descendant == null). |
| final Rect? childBounds = descendant != null |
| ? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds) |
| : rect; |
| |
| double targetExtent; |
| Rect? targetRect; |
| switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
| case AxisDirection.up: |
| targetExtent = childExtent - (childBounds?.top ?? 0); |
| targetRect = _trim(childBounds, bottom: childExtent); |
| break; |
| case AxisDirection.right: |
| targetExtent = childBounds?.right ?? childExtent; |
| targetRect = _trim(childBounds, left: 0); |
| break; |
| case AxisDirection.down: |
| targetExtent = childBounds?.bottom ?? childExtent; |
| targetRect = _trim(childBounds, top: 0); |
| break; |
| case AxisDirection.left: |
| targetExtent = childExtent - (childBounds?.left ?? 0); |
| targetRect = _trim(childBounds, right: childExtent); |
| break; |
| } |
| |
| // A stretch header can have a bigger childExtent than maxExtent. |
| final double effectiveMaxExtent = math.max(childExtent, maxExtent); |
| |
| targetExtent = clampDouble( |
| clampDouble( |
| targetExtent, |
| showOnScreen.minShowOnScreenExtent, |
| showOnScreen.maxShowOnScreenExtent, |
| ), |
| // Clamp the value back to the valid range after applying additional |
| // constraints. Contracting is not allowed. |
| childExtent, |
| effectiveMaxExtent); |
| |
| // Expands the header if needed, with animation. |
| if (targetExtent > childExtent) { |
| final double targetScrollOffset = maxExtent - targetExtent; |
| assert( |
| vsync != null, |
| 'vsync must not be null if the floating header changes size animatedly.', |
| ); |
| _updateAnimation(duration, targetScrollOffset, curve); |
| _controller?.forward(from: 0.0); |
| } |
| |
| super.showOnScreen( |
| descendant: descendant == null ? this : child, |
| rect: targetRect, |
| duration: duration, |
| curve: curve, |
| ); |
| } |
| |
| @override |
| double childMainAxisPosition(RenderBox child) { |
| assert(child == this.child); |
| return _childPosition ?? 0.0; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DoubleProperty('effective scroll offset', _effectiveScrollOffset)); |
| } |
| } |
| |
| /// A sliver with a [RenderBox] child which shrinks and then remains pinned to |
| /// the start of the viewport like a [RenderSliverPinnedPersistentHeader], but |
| /// immediately grows when the user scrolls in the reverse direction. |
| /// |
| /// See also: |
| /// |
| /// * [RenderSliverFloatingPersistentHeader], which is similar but scrolls off |
| /// the top rather than sticking to it. |
| abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader { |
| /// Creates a sliver that shrinks when it hits the start of the viewport, then |
| /// stays pinned there, and grows immediately when the user reverses the |
| /// scroll direction. |
| RenderSliverFloatingPinnedPersistentHeader({ |
| super.child, |
| super.vsync, |
| super.snapConfiguration, |
| super.stretchConfiguration, |
| super.showOnScreenConfiguration, |
| }); |
| |
| @override |
| double updateGeometry() { |
| final double minExtent = this.minExtent; |
| final double minAllowedExtent = constraints.remainingPaintExtent > minExtent ? |
| minExtent : |
| constraints.remainingPaintExtent; |
| final double maxExtent = this.maxExtent; |
| final double paintExtent = maxExtent - _effectiveScrollOffset!; |
| final double clampedPaintExtent = clampDouble(paintExtent, |
| minAllowedExtent, |
| constraints.remainingPaintExtent, |
| ); |
| final double layoutExtent = maxExtent - constraints.scrollOffset; |
| final double stretchOffset = stretchConfiguration != null ? |
| constraints.overlap.abs() : |
| 0.0; |
| geometry = SliverGeometry( |
| scrollExtent: maxExtent, |
| paintOrigin: math.min(constraints.overlap, 0.0), |
| paintExtent: clampedPaintExtent, |
| layoutExtent: clampDouble(layoutExtent, 0.0, clampedPaintExtent), |
| maxPaintExtent: maxExtent + stretchOffset, |
| maxScrollObstructionExtent: minExtent, |
| hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
| ); |
| return 0.0; |
| } |
| } |