blob: 59b178ca2082049149bae4176e8783d1dcc28fd5 [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:flute/animation.dart';
import 'package:flute/foundation.dart';
/// The direction of a scroll, relative to the positive scroll offset axis given
/// by an [AxisDirection] and a [GrowthDirection].
///
/// This contrasts to [GrowthDirection] in that it has a third value, [idle],
/// for the case where no scroll is occurring.
///
/// This is used by [RenderSliverFloatingPersistentHeader] to only expand when
/// the user is scrolling in the same direction as the detected scroll offset
/// change.
enum ScrollDirection {
/// No scrolling is underway.
idle,
/// Scrolling is happening in the positive scroll offset direction.
///
/// For example, for the [GrowthDirection.forward] part of a vertical
/// [AxisDirection.down] list, this means the content is moving up, exposing
/// lower content.
forward,
/// Scrolling is happening in the negative scroll offset direction.
///
/// For example, for the [GrowthDirection.forward] part of a vertical
/// [AxisDirection.down] list, this means the content is moving down, exposing
/// earlier content.
reverse,
}
/// Returns the opposite of the given [ScrollDirection].
///
/// Specifically, returns [ScrollDirection.reverse] for [ScrollDirection.forward]
/// (and vice versa) and returns [ScrollDirection.idle] for
/// [ScrollDirection.idle].
ScrollDirection flipScrollDirection(ScrollDirection direction) {
switch (direction) {
case ScrollDirection.idle:
return ScrollDirection.idle;
case ScrollDirection.forward:
return ScrollDirection.reverse;
case ScrollDirection.reverse:
return ScrollDirection.forward;
}
}
/// Which part of the content inside the viewport should be visible.
///
/// The [pixels] value determines the scroll offset that the viewport uses to
/// select which part of its content to display. As the user scrolls the
/// viewport, this value changes, which changes the content that is displayed.
///
/// This object is a [Listenable] that notifies its listeners when [pixels]
/// changes.
///
/// See also:
///
/// * [ScrollPosition], which is a commonly used concrete subclass.
/// * [RenderViewportBase], which is a render object that uses viewport
/// offsets.
abstract class ViewportOffset extends ChangeNotifier {
/// Default constructor.
///
/// Allows subclasses to construct this object directly.
ViewportOffset();
/// Creates a viewport offset with the given [pixels] value.
///
/// The [pixels] value does not change unless the viewport issues a
/// correction.
factory ViewportOffset.fixed(double value) = _FixedViewportOffset;
/// Creates a viewport offset with a [pixels] value of 0.0.
///
/// The [pixels] value does not change unless the viewport issues a
/// correction.
factory ViewportOffset.zero() = _FixedViewportOffset.zero;
/// The number of pixels to offset the children in the opposite of the axis direction.
///
/// For example, if the axis direction is down, then the pixel value
/// represents the number of logical pixels to move the children _up_ the
/// screen. Similarly, if the axis direction is left, then the pixels value
/// represents the number of logical pixels to move the children to _right_.
///
/// This object notifies its listeners when this value changes (except when
/// the value changes due to [correctBy]).
double get pixels;
/// Whether the [pixels] property is available.
bool get hasPixels;
/// Called when the viewport's extents are established.
///
/// The argument is the dimension of the [RenderViewport] in the main axis
/// (e.g. the height, for a vertical viewport).
///
/// This may be called redundantly, with the same value, each frame. This is
/// called during layout for the [RenderViewport]. If the viewport is
/// configured to shrink-wrap its contents, it may be called several times,
/// since the layout is repeated each time the scroll offset is corrected.
///
/// If this is called, it is called before [applyContentDimensions]. If this
/// is called, [applyContentDimensions] will be called soon afterwards in the
/// same layout phase. If the viewport is not configured to shrink-wrap its
/// contents, then this will only be called when the viewport recomputes its
/// size (i.e. when its parent lays out), and not during normal scrolling.
///
/// If applying the viewport dimensions changes the scroll offset, return
/// false. Otherwise, return true. If you return false, the [RenderViewport]
/// will be laid out again with the new scroll offset. This is expensive. (The
/// return value is answering the question "did you accept these viewport
/// dimensions unconditionally?"; if the new dimensions change the
/// [ViewportOffset]'s actual [pixels] value, then the viewport will need to
/// be laid out again.)
bool applyViewportDimension(double viewportDimension);
/// Called when the viewport's content extents are established.
///
/// The arguments are the minimum and maximum scroll extents respectively. The
/// minimum will be equal to or less than the maximum. In the case of slivers,
/// the minimum will be equal to or less than zero, the maximum will be equal
/// to or greater than zero.
///
/// The maximum scroll extent has the viewport dimension subtracted from it.
/// For instance, if there is 100.0 pixels of scrollable content, and the
/// viewport is 80.0 pixels high, then the minimum scroll extent will
/// typically be 0.0 and the maximum scroll extent will typically be 20.0,
/// because there's only 20.0 pixels of actual scroll slack.
///
/// If applying the content dimensions changes the scroll offset, return
/// false. Otherwise, return true. If you return false, the [RenderViewport]
/// will be laid out again with the new scroll offset. This is expensive. (The
/// return value is answering the question "did you accept these content
/// dimensions unconditionally?"; if the new dimensions change the
/// [ViewportOffset]'s actual [pixels] value, then the viewport will need to
/// be laid out again.)
///
/// This is called at least once each time the [RenderViewport] is laid out,
/// even if the values have not changed. It may be called many times if the
/// scroll offset is corrected (if this returns false). This is always called
/// after [applyViewportDimension], if that method is called.
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent);
/// Apply a layout-time correction to the scroll offset.
///
/// This method should change the [pixels] value by `correction`, but without
/// calling [notifyListeners]. It is called during layout by the
/// [RenderViewport], before [applyContentDimensions]. After this method is
/// called, the layout will be recomputed and that may result in this method
/// being called again, though this should be very rare.
///
/// See also:
///
/// * [jumpTo], for also changing the scroll position when not in layout.
/// [jumpTo] applies the change immediately and notifies its listeners.
void correctBy(double correction);
/// Jumps [pixels] from its current value to the given value,
/// without animation, and without checking if the new value is in range.
///
/// See also:
///
/// * [correctBy], for changing the current offset in the middle of layout
/// and that defers the notification of its listeners until after layout.
void jumpTo(double pixels);
/// Animates [pixels] from its current value to the given value.
///
/// The returned [Future] will complete when the animation ends, whether it
/// completed successfully or whether it was interrupted prematurely.
///
/// The duration must not be zero. To jump to a particular value without an
/// animation, use [jumpTo].
Future<void> animateTo(
double to, {
required Duration duration,
required Curve curve,
});
/// Calls [jumpTo] if duration is null or [Duration.zero], otherwise
/// [animateTo] is called.
///
/// If [animateTo] is called then [curve] defaults to [Curves.ease]. The
/// [clamp] parameter is ignored by this stub implementation but subclasses
/// like [ScrollPosition] handle it by adjusting [to] to prevent over or
/// underscroll.
Future<void> moveTo(
double to, {
Duration? duration,
Curve? curve,
bool? clamp,
}) {
if (duration == null || duration == Duration.zero) {
jumpTo(to);
return Future<void>.value();
} else {
return animateTo(to, duration: duration, curve: curve ?? Curves.ease);
}
}
/// The direction in which the user is trying to change [pixels], relative to
/// the viewport's [RenderViewportBase.axisDirection].
///
/// If the _user_ is not scrolling, this will return [ScrollDirection.idle]
/// even if there is (for example) a [ScrollActivity] currently animating the
/// position.
///
/// This is exposed in [SliverConstraints.userScrollDirection], which is used
/// by some slivers to determine how to react to a change in scroll offset.
/// For example, [RenderSliverFloatingPersistentHeader] will only expand a
/// floating app bar when the [userScrollDirection] is in the positive scroll
/// offset direction.
ScrollDirection get userScrollDirection;
/// Whether a viewport is allowed to change [pixels] implicitly to respond to
/// a call to [RenderObject.showOnScreen].
///
/// [RenderObject.showOnScreen] is for example used to bring a text field
/// fully on screen after it has received focus. This property controls
/// whether the viewport associated with this offset is allowed to change the
/// offset's [pixels] value to fulfill such a request.
bool get allowImplicitScrolling;
@override
String toString() {
final List<String> description = <String>[];
debugFillDescription(description);
return '${describeIdentity(this)}(${description.join(", ")})';
}
/// Add additional information to the given description for use by [toString].
///
/// This method makes it easier for subclasses to coordinate to provide a
/// high-quality [toString] implementation. The [toString] implementation on
/// the [State] base class calls [debugFillDescription] to collect useful
/// information from subclasses to incorporate into its return value.
///
/// Implementations of this method should start with a call to the inherited
/// method, as in `super.debugFillDescription(description)`.
@mustCallSuper
void debugFillDescription(List<String> description) {
if (hasPixels) {
description.add('offset: ${pixels.toStringAsFixed(1)}');
}
}
}
class _FixedViewportOffset extends ViewportOffset {
_FixedViewportOffset(this._pixels);
_FixedViewportOffset.zero() : _pixels = 0.0;
double _pixels;
@override
double get pixels => _pixels;
@override
bool get hasPixels => true;
@override
bool applyViewportDimension(double viewportDimension) => true;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) => true;
@override
void correctBy(double correction) {
_pixels += correction;
}
@override
void jumpTo(double pixels) {
// Do nothing, viewport is fixed.
}
@override
Future<void> animateTo(
double to, {
required Duration duration,
required Curve curve,
}) async { }
@override
ScrollDirection get userScrollDirection => ScrollDirection.idle;
@override
bool get allowImplicitScrolling => false;
}