blob: f548e9bf114ea75ca2c7c4ff3c0658bba1515af2 [file] [log] [blame]
// 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,
);
}
@override
void updateRenderObject(BuildContext context, covariant _RenderSliverScrollingPersistentHeaderForWidgets renderObject) {
renderObject.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,
);
}
@override
void updateRenderObject(BuildContext context, covariant _RenderSliverPinnedPersistentHeaderForWidgets renderObject) {
renderObject
..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,
});
}