| // 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/gestures.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/semantics.dart'; |
| import 'package:vector_math/vector_math_64.dart'; |
| |
| import 'box.dart'; |
| import 'object.dart'; |
| import 'sliver.dart'; |
| import 'viewport.dart'; |
| import 'viewport_offset.dart'; |
| |
| /// 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; |
| } |
| |
| /// 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; |
| } |
| |
| 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; |
| } |
| return null; |
| } |
| |
| 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 && childMainAxisPosition(child) == 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); |
| } |
| } |
| |
| /// Whether the [SemanticsNode]s associated with this [RenderSliver] should |
| /// be excluded from the semantic scrolling area. |
| /// |
| /// [RenderSliver]s that stay on the screen even though the user has scrolled |
| /// past them (e.g. a pinned app bar) should set this to true. |
| @protected |
| bool get excludeFromSemanticsScrolling => _excludeFromSemanticsScrolling; |
| bool _excludeFromSemanticsScrolling = false; |
| set excludeFromSemanticsScrolling(bool value) { |
| if (_excludeFromSemanticsScrolling == value) |
| return; |
| _excludeFromSemanticsScrolling = value; |
| markNeedsSemanticsUpdate(); |
| } |
| |
| @override |
| void describeSemanticsConfiguration(SemanticsConfiguration config) { |
| super.describeSemanticsConfiguration(config); |
| |
| if (_excludeFromSemanticsScrolling) |
| 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({ |
| RenderBox child, |
| OverScrollHeaderStretchConfiguration stretchConfiguration, |
| }) : super( |
| child: child, |
| stretchConfiguration: 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 && _childPosition == 0.0) { |
| 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: paintExtent.clamp(0.0, constraints.remainingPaintExtent) as double, |
| 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: paintExtent.clamp(0.0, constraints.remainingPaintExtent) as double, |
| maxPaintExtent: maxExtent, |
| hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
| ); |
| _childPosition = updateGeometry(); |
| } |
| |
| @override |
| double childMainAxisPosition(RenderBox child) { |
| assert(child == this.child); |
| 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({ |
| RenderBox child, |
| OverScrollHeaderStretchConfiguration stretchConfiguration, |
| }) : super( |
| child: child, |
| stretchConfiguration: stretchConfiguration, |
| ); |
| |
| @override |
| void performLayout() { |
| final SliverConstraints constraints = this.constraints; |
| final double maxExtent = this.maxExtent; |
| final bool overlapsContent = constraints.overlap > 0.0; |
| excludeFromSemanticsScrolling = overlapsContent || (constraints.scrollOffset > maxExtent - minExtent); |
| layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: overlapsContent); |
| final double effectiveRemainingPaintExtent = math.max(0, constraints.remainingPaintExtent - constraints.overlap); |
| final double layoutExtent = (maxExtent - constraints.scrollOffset).clamp(0.0, effectiveRemainingPaintExtent) as double; |
| 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; |
| } |
| |
| /// 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({ |
| @required this.vsync, |
| this.curve = Curves.ease, |
| this.duration = const Duration(milliseconds: 300), |
| }) : assert(vsync != null), |
| assert(curve != null), |
| assert(duration != null); |
| |
| /// The [TickerProvider] for the [AnimationController] that causes a |
| /// floating header to snap in or out of view. |
| final TickerProvider vsync; |
| |
| /// 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({ |
| RenderBox child, |
| FloatingHeaderSnapConfiguration snapConfiguration, |
| OverScrollHeaderStretchConfiguration stretchConfiguration, |
| }) : _snapConfiguration = snapConfiguration, |
| super( |
| child: child, |
| stretchConfiguration: stretchConfiguration, |
| ); |
| |
| AnimationController _controller; |
| Animation<double> _animation; |
| double _lastActualScrollOffset; |
| double _effectiveScrollOffset; |
| |
| // 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(); |
| } |
| |
| /// 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 get snapConfiguration => _snapConfiguration; |
| FloatingHeaderSnapConfiguration _snapConfiguration; |
| set snapConfiguration(FloatingHeaderSnapConfiguration value) { |
| if (value == _snapConfiguration) |
| return; |
| if (value == null) { |
| _controller?.dispose(); |
| _controller = null; |
| } else { |
| if (_snapConfiguration != null && value.vsync != _snapConfiguration.vsync) |
| _controller?.resync(value.vsync); |
| } |
| _snapConfiguration = value; |
| } |
| |
| /// Updates [geometry], and returns the new value for [childMainAxisPosition]. |
| /// |
| /// This is used by [performLayout]. |
| @protected |
| double updateGeometry() { |
| double stretchOffset = 0.0; |
| if (stretchConfiguration != null && _childPosition == 0.0) { |
| 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: paintExtent.clamp(0.0, constraints.remainingPaintExtent) as double, |
| layoutExtent: layoutExtent.clamp(0.0, constraints.remainingPaintExtent) as double, |
| 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); |
| } |
| |
| /// If the header isn't already fully exposed, then scroll it into view. |
| void maybeStartSnapAnimation(ScrollDirection direction) { |
| if (snapConfiguration == null) |
| return; |
| if (direction == ScrollDirection.forward && _effectiveScrollOffset <= 0.0) |
| return; |
| if (direction == ScrollDirection.reverse && _effectiveScrollOffset >= maxExtent) |
| return; |
| |
| final TickerProvider vsync = snapConfiguration.vsync; |
| final Duration duration = snapConfiguration.duration; |
| _controller ??= AnimationController(vsync: vsync, duration: duration) |
| ..addListener(() { |
| if (_effectiveScrollOffset == _animation.value) |
| return; |
| _effectiveScrollOffset = _animation.value; |
| markNeedsLayout(); |
| }); |
| |
| _animation = _controller.drive( |
| Tween<double>( |
| begin: _effectiveScrollOffset, |
| end: direction == ScrollDirection.forward ? 0.0 : maxExtent, |
| ).chain(CurveTween( |
| curve: snapConfiguration.curve, |
| )), |
| ); |
| |
| _controller.forward(from: 0.0); |
| } |
| |
| /// If a header snap 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; |
| if (allowFloatingExpansion) { |
| if (_effectiveScrollOffset > maxExtent) // We're scrolled off-screen, but should reveal, so |
| _effectiveScrollOffset = maxExtent; // pretend we're just at the limit. |
| } else { |
| if (delta > 0.0) // If we are trying to expand when allowFloatingExpansion is false, |
| delta = 0.0; // disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.) |
| } |
| _effectiveScrollOffset = (_effectiveScrollOffset - delta).clamp(0.0, constraints.scrollOffset) as double; |
| } else { |
| _effectiveScrollOffset = constraints.scrollOffset; |
| } |
| excludeFromSemanticsScrolling = _effectiveScrollOffset <= constraints.scrollOffset; |
| final bool overlapsContent = _effectiveScrollOffset < constraints.scrollOffset; |
| |
| layoutChild( |
| _effectiveScrollOffset, |
| maxExtent, |
| overlapsContent: overlapsContent, |
| ); |
| _childPosition = updateGeometry(); |
| _lastActualScrollOffset = constraints.scrollOffset; |
| } |
| |
| @override |
| double childMainAxisPosition(RenderBox child) { |
| assert(child == this.child); |
| return _childPosition; |
| } |
| |
| @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({ |
| RenderBox child, |
| FloatingHeaderSnapConfiguration snapConfiguration, |
| OverScrollHeaderStretchConfiguration stretchConfiguration, |
| }) : super( |
| child: child, |
| snapConfiguration: snapConfiguration, |
| stretchConfiguration: stretchConfiguration, |
| ); |
| |
| @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 = paintExtent.clamp( |
| minAllowedExtent, |
| constraints.remainingPaintExtent, |
| ) as double; |
| 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: layoutExtent.clamp(0.0, clampedPaintExtent) as double, |
| maxPaintExtent: maxExtent + stretchOffset, |
| maxScrollObstructionExtent: maxExtent, |
| hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
| ); |
| return 0.0; |
| } |
| } |