// 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 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart' show TickerProvider; import 'framework.dart'; import 'scroll_position.dart'; import 'scrollable.dart'; /// Delegate for configuring a [SliverPersistentHeader]. abstract class SliverPersistentHeaderDelegate { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const SliverPersistentHeaderDelegate(); /// The widget to place inside the [SliverPersistentHeader]. /// /// The `context` is the [BuildContext] of the sliver. /// /// The `shrinkOffset` is a distance from [maxExtent] towards [minExtent] /// representing the current amount by which the sliver has been shrunk. When /// the `shrinkOffset` is zero, the contents will be rendered with a dimension /// of [maxExtent] in the main axis. When `shrinkOffset` equals the difference /// between [maxExtent] and [minExtent] (a positive number), the contents will /// be rendered with a dimension of [minExtent] in the main axis. The /// `shrinkOffset` will always be a positive number in that range. /// /// The `overlapsContent` argument is true if subsequent slivers (if any) will /// be rendered beneath this one, and false if the sliver will not have any /// contents below it. Typically this is used to decide whether to draw a /// shadow to simulate the sliver being above the contents below it. Typically /// this is true when `shrinkOffset` is at its greatest value and false /// otherwise, but that is not guaranteed. See [NestedScrollView] for an /// example of a case where `overlapsContent`'s value can be unrelated to /// `shrinkOffset`. Widget build(BuildContext context, double shrinkOffset, bool overlapsContent); /// The smallest size to allow the header to reach, when it shrinks at the /// start of the viewport. /// /// This must return a value equal to or less than [maxExtent]. /// /// This value should not change over the lifetime of the delegate. It should /// be based entirely on the constructor arguments passed to the delegate. See /// [shouldRebuild], which must return true if a new delegate would return a /// different value. double get minExtent; /// The size of the header when it is not shrinking at the top of the /// viewport. /// /// This must return a value equal to or greater than [minExtent]. /// /// This value should not change over the lifetime of the delegate. It should /// be based entirely on the constructor arguments passed to the delegate. See /// [shouldRebuild], which must return true if a new delegate would return a /// different value. double get maxExtent; /// A [TickerProvider] to use when animating the header's size changes. /// /// Must not be null if the persistent header is a floating header, and /// [snapConfiguration] or [showOnScreenConfiguration] is not null. TickerProvider? get vsync => null; /// Specifies how floating headers should animate in and out of view. /// /// If the value of this property is null, then floating headers will /// not animate into place. /// /// This is only used for floating headers (those with /// [SliverPersistentHeader.floating] set to true). /// /// Defaults to null. FloatingHeaderSnapConfiguration? get snapConfiguration => null; /// Specifies an [AsyncCallback] and offset for execution. /// /// If the value of this property is null, then callback will not be /// triggered. /// /// This is only used for stretching headers (those with /// [SliverAppBar.stretch] set to true). /// /// Defaults to null. OverScrollHeaderStretchConfiguration? get stretchConfiguration => null; /// Specifies how floating headers and pinned headers should behave in /// response to [RenderObject.showOnScreen] calls. /// /// Defaults to null. PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => null; /// Whether this delegate is meaningfully different from the old delegate. /// /// If this returns false, then the header might not be rebuilt, even though /// the instance of the delegate changed. /// /// This must return true if `oldDelegate` and this object would return /// different values for [minExtent], [maxExtent], [snapConfiguration], or /// would return a meaningfully different widget tree from [build] for the /// same arguments. bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate); } /// A sliver whose size varies when the sliver is scrolled to the edge /// of the viewport opposite the sliver's [GrowthDirection]. /// /// In the normal case of a [CustomScrollView] with no centered sliver, this /// sliver will vary its size when scrolled to the leading edge of the viewport. /// /// This is the layout primitive that [SliverAppBar] uses for its /// shrinking/growing effect. class SliverPersistentHeader extends StatelessWidget { /// Creates a sliver that varies its size when it is scrolled to the start of /// a viewport. /// /// The [delegate], [pinned], and [floating] arguments must not be null. const SliverPersistentHeader({ super.key, required this.delegate, this.pinned = false, this.floating = false, }) : assert(delegate != null), assert(pinned != null), assert(floating != null); /// Configuration for the sliver's layout. /// /// The delegate provides the following information: /// /// * The minimum and maximum dimensions of the sliver. /// /// * The builder for generating the widgets of the sliver. /// /// * The instructions for snapping the scroll offset, if [floating] is true. final SliverPersistentHeaderDelegate delegate; /// Whether to stick the header to the start of the viewport once it has /// reached its minimum size. /// /// If this is false, the header will continue scrolling off the screen after /// it has shrunk to its minimum extent. final bool pinned; /// Whether the header should immediately grow again if the user reverses /// scroll direction. /// /// If this is false, the header only grows again once the user reaches the /// part of the viewport that contains the sliver. /// /// The [delegate]'s [SliverPersistentHeaderDelegate.snapConfiguration] is /// ignored unless [floating] is true. final bool floating; @override Widget build(BuildContext context) { if (floating && pinned) { return _SliverFloatingPinnedPersistentHeader(delegate: delegate); } if (pinned) { return _SliverPinnedPersistentHeader(delegate: delegate); } if (floating) { return _SliverFloatingPersistentHeader(delegate: delegate); } return _SliverScrollingPersistentHeader(delegate: delegate); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add( DiagnosticsProperty<SliverPersistentHeaderDelegate>( 'delegate', delegate, ), ); final List<String> flags = <String>[ if (pinned) 'pinned', if (floating) 'floating', ]; if (flags.isEmpty) { flags.add('normal'); } properties.add(IterableProperty<String>('mode', flags)); } } class _FloatingHeader extends StatefulWidget { const _FloatingHeader({ required this.child }); final Widget child; @override _FloatingHeaderState createState() => _FloatingHeaderState(); } // A wrapper for the widget created by _SliverPersistentHeaderElement that // starts and stops the floating app bar's snap-into-view or snap-out-of-view // animation. It also informs the float when pointer scrolling by updating the // last known ScrollDirection when scrolling began. class _FloatingHeaderState extends State<_FloatingHeader> { ScrollPosition? _position; @override void didChangeDependencies() { super.didChangeDependencies(); if (_position != null) { _position!.isScrollingNotifier.removeListener(_isScrollingListener); } _position = Scrollable.of(context)?.position; if (_position != null) { _position!.isScrollingNotifier.addListener(_isScrollingListener); } } @override void dispose() { if (_position != null) { _position!.isScrollingNotifier.removeListener(_isScrollingListener); } super.dispose(); } RenderSliverFloatingPersistentHeader? _headerRenderer() { return context.findAncestorRenderObjectOfType<RenderSliverFloatingPersistentHeader>(); } void _isScrollingListener() { assert(_position != null); // When a scroll stops, then maybe snap the app bar into view. // Similarly, when a scroll starts, then maybe stop the snap animation. // Update the scrolling direction as well for pointer scrolling updates. final RenderSliverFloatingPersistentHeader? header = _headerRenderer(); if (_position!.isScrollingNotifier.value) { header?.updateScrollStartDirection(_position!.userScrollDirection); // Only SliverAppBars support snapping, headers will not snap. header?.maybeStopSnapAnimation(_position!.userScrollDirection); } else { // Only SliverAppBars support snapping, headers will not snap. header?.maybeStartSnapAnimation(_position!.userScrollDirection); } } @override Widget build(BuildContext context) => widget.child; } class _SliverPersistentHeaderElement extends RenderObjectElement { _SliverPersistentHeaderElement( _SliverPersistentHeaderRenderObjectWidget super.widget, { this.floating = false, }) : assert(floating != null); final bool floating; @override _RenderSliverPersistentHeaderForWidgetsMixin get renderObject => super.renderObject as _RenderSliverPersistentHeaderForWidgetsMixin; @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); renderObject._element = this; } @override void unmount() { renderObject._element = null; super.unmount(); } @override void update(_SliverPersistentHeaderRenderObjectWidget newWidget) { final _SliverPersistentHeaderRenderObjectWidget oldWidget = widget as _SliverPersistentHeaderRenderObjectWidget; super.update(newWidget); final SliverPersistentHeaderDelegate newDelegate = newWidget.delegate; final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate; if (newDelegate != oldDelegate && (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate))) { renderObject.triggerRebuild(); } } @override void performRebuild() { super.performRebuild(); renderObject.triggerRebuild(); } Element? child; void _build(double shrinkOffset, bool overlapsContent) { owner!.buildScope(this, () { final _SliverPersistentHeaderRenderObjectWidget sliverPersistentHeaderRenderObjectWidget = widget as _SliverPersistentHeaderRenderObjectWidget; child = updateChild( child, floating ? _FloatingHeader(child: sliverPersistentHeaderRenderObjectWidget.delegate.build( this, shrinkOffset, overlapsContent )) : sliverPersistentHeaderRenderObjectWidget.delegate.build(this, shrinkOffset, overlapsContent), null, ); }); } @override void forgetChild(Element child) { assert(child == this.child); this.child = null; super.forgetChild(child); } @override void insertRenderObjectChild(covariant RenderBox child, Object? slot) { assert(renderObject.debugValidateChild(child)); renderObject.child = child; } @override void moveRenderObjectChild(covariant RenderObject child, Object? oldSlot, Object? newSlot) { assert(false); } @override void removeRenderObjectChild(covariant RenderObject child, Object? slot) { renderObject.child = null; } @override void visitChildren(ElementVisitor visitor) { if (child != null) { visitor(child!); } } } abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWidget { const _SliverPersistentHeaderRenderObjectWidget({ required this.delegate, this.floating = false, }) : assert(delegate != null), assert(floating != null); final SliverPersistentHeaderDelegate delegate; final bool floating; @override _SliverPersistentHeaderElement createElement() => _SliverPersistentHeaderElement(this, floating: floating); @override _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context); @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description.add( DiagnosticsProperty<SliverPersistentHeaderDelegate>( 'delegate', delegate, ), ); } } mixin _RenderSliverPersistentHeaderForWidgetsMixin on RenderSliverPersistentHeader { _SliverPersistentHeaderElement? _element; @override double get minExtent => (_element!.widget as _SliverPersistentHeaderRenderObjectWidget).delegate.minExtent; @override double get maxExtent => (_element!.widget as _SliverPersistentHeaderRenderObjectWidget).delegate.maxExtent; @override void updateChild(double shrinkOffset, bool overlapsContent) { assert(_element != null); _element!._build(shrinkOffset, overlapsContent); } @protected void triggerRebuild() { markNeedsLayout(); } } class _SliverScrollingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { const _SliverScrollingPersistentHeader({ required super.delegate, }); @override _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { return _RenderSliverScrollingPersistentHeaderForWidgets( stretchConfiguration: delegate.stretchConfiguration, ); } } class _RenderSliverScrollingPersistentHeaderForWidgets extends RenderSliverScrollingPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { _RenderSliverScrollingPersistentHeaderForWidgets({ super.stretchConfiguration, }); } class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { const _SliverPinnedPersistentHeader({ required super.delegate, }); @override _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { return _RenderSliverPinnedPersistentHeaderForWidgets( stretchConfiguration: delegate.stretchConfiguration, showOnScreenConfiguration: delegate.showOnScreenConfiguration, ); } } class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { _RenderSliverPinnedPersistentHeaderForWidgets({ super.stretchConfiguration, super.showOnScreenConfiguration, }); } class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { const _SliverFloatingPersistentHeader({ required super.delegate, }) : super( floating: true, ); @override _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { return _RenderSliverFloatingPersistentHeaderForWidgets( vsync: delegate.vsync, snapConfiguration: delegate.snapConfiguration, stretchConfiguration: delegate.stretchConfiguration, showOnScreenConfiguration: delegate.showOnScreenConfiguration, ); } @override void updateRenderObject(BuildContext context, _RenderSliverFloatingPersistentHeaderForWidgets renderObject) { renderObject.vsync = delegate.vsync; renderObject.snapConfiguration = delegate.snapConfiguration; renderObject.stretchConfiguration = delegate.stretchConfiguration; renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration; } } class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliverFloatingPinnedPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { _RenderSliverFloatingPinnedPersistentHeaderForWidgets({ required super.vsync, super.snapConfiguration, super.stretchConfiguration, super.showOnScreenConfiguration, }); } class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { const _SliverFloatingPinnedPersistentHeader({ required super.delegate, }) : super( floating: true, ); @override _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { return _RenderSliverFloatingPinnedPersistentHeaderForWidgets( vsync: delegate.vsync, snapConfiguration: delegate.snapConfiguration, stretchConfiguration: delegate.stretchConfiguration, showOnScreenConfiguration: delegate.showOnScreenConfiguration, ); } @override void updateRenderObject(BuildContext context, _RenderSliverFloatingPinnedPersistentHeaderForWidgets renderObject) { renderObject.vsync = delegate.vsync; renderObject.snapConfiguration = delegate.snapConfiguration; renderObject.stretchConfiguration = delegate.stretchConfiguration; renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration; } } class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloatingPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { _RenderSliverFloatingPersistentHeaderForWidgets({ required super.vsync, super.snapConfiguration, super.stretchConfiguration, super.showOnScreenConfiguration, }); }