
This widget implements the ability to place slivers side by side in a single ScrollView so that they scroll together. The design document for `SliverCrossAxisGroup` can be found [here](https://docs.google.com/document/d/1e2bdLSYV_Dq2h8aHpF8mda67aOmZocPiMyjCcTTZhTg/edit?resourcekey=0-Xj2X2XA3CAFae22Sv3hAiA). Fixes #56756.
1882 lines
79 KiB
Dart
1882 lines
79 KiB
Dart
// 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/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
|
|
import 'box.dart';
|
|
import 'debug.dart';
|
|
import 'object.dart';
|
|
import 'viewport.dart';
|
|
import 'viewport_offset.dart';
|
|
|
|
// CORE TYPES FOR SLIVERS
|
|
// The RenderSliver base class and its helper types.
|
|
|
|
/// The direction in which a sliver's contents are ordered, relative to the
|
|
/// scroll offset axis.
|
|
///
|
|
/// For example, a vertical alphabetical list that is going [AxisDirection.down]
|
|
/// with a [GrowthDirection.forward] would have the A at the top and the Z at
|
|
/// the bottom, with the A adjacent to the origin, as would such a list going
|
|
/// [AxisDirection.up] with a [GrowthDirection.reverse]. On the other hand, a
|
|
/// vertical alphabetical list that is going [AxisDirection.down] with a
|
|
/// [GrowthDirection.reverse] would have the Z at the top (at scroll offset
|
|
/// zero) and the A below it.
|
|
///
|
|
/// {@template flutter.rendering.GrowthDirection.sample}
|
|
/// Most scroll views by default are ordered [GrowthDirection.forward].
|
|
/// Changing the default values of [ScrollView.anchor],
|
|
/// [ScrollView.center], or both, can configure a scroll view for
|
|
/// [GrowthDirection.reverse].
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample shows a [CustomScrollView], with [Radio] buttons in the
|
|
/// [AppBar.bottom] that change the [AxisDirection] to illustrate different
|
|
/// configurations. The [CustomScrollView.anchor] and [CustomScrollView.center]
|
|
/// properties are also set to have the 0 scroll offset positioned in the middle
|
|
/// of the viewport, with [GrowthDirection.forward] and [GrowthDirection.reverse]
|
|
/// illustrated on either side. The sliver that shares the
|
|
/// [CustomScrollView.center] key is positioned at the [CustomScrollView.anchor].
|
|
///
|
|
/// ** See code in examples/api/lib/rendering/growth_direction/growth_direction.0.dart **
|
|
/// {@end-tool}
|
|
/// {@endtemplate}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [applyGrowthDirectionToAxisDirection], which returns the direction in
|
|
/// which the scroll offset increases.
|
|
enum GrowthDirection {
|
|
/// This sliver's contents are ordered in the same direction as the
|
|
/// [AxisDirection]. For example, a vertical alphabetical list that is going
|
|
/// [AxisDirection.down] with a [GrowthDirection.forward] would have the A at
|
|
/// the top and the Z at the bottom, with the A adjacent to the origin.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [applyGrowthDirectionToAxisDirection], which returns the direction in
|
|
/// which the scroll offset increases.
|
|
forward,
|
|
|
|
/// This sliver's contents are ordered in the opposite direction of the
|
|
/// [AxisDirection].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [applyGrowthDirectionToAxisDirection], which returns the direction in
|
|
/// which the scroll offset increases.
|
|
reverse,
|
|
}
|
|
|
|
/// Flips the [AxisDirection] if the [GrowthDirection] is [GrowthDirection.reverse].
|
|
///
|
|
/// Specifically, returns `axisDirection` if `growthDirection` is
|
|
/// [GrowthDirection.forward], otherwise returns [flipAxisDirection] applied to
|
|
/// `axisDirection`.
|
|
///
|
|
/// This function is useful in [RenderSliver] subclasses that are given both an
|
|
/// [AxisDirection] and a [GrowthDirection] and wish to compute the
|
|
/// [AxisDirection] in which growth will occur.
|
|
AxisDirection applyGrowthDirectionToAxisDirection(AxisDirection axisDirection, GrowthDirection growthDirection) {
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
return axisDirection;
|
|
case GrowthDirection.reverse:
|
|
return flipAxisDirection(axisDirection);
|
|
}
|
|
}
|
|
|
|
/// Flips the [ScrollDirection] if the [GrowthDirection] is
|
|
/// [GrowthDirection.reverse].
|
|
///
|
|
/// Specifically, returns `scrollDirection` if `scrollDirection` is
|
|
/// [GrowthDirection.forward], otherwise returns [flipScrollDirection] applied
|
|
/// to `scrollDirection`.
|
|
///
|
|
/// This function is useful in [RenderSliver] subclasses that are given both an
|
|
/// [ScrollDirection] and a [GrowthDirection] and wish to compute the
|
|
/// [ScrollDirection] in which growth will occur.
|
|
ScrollDirection applyGrowthDirectionToScrollDirection(ScrollDirection scrollDirection, GrowthDirection growthDirection) {
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
return scrollDirection;
|
|
case GrowthDirection.reverse:
|
|
return flipScrollDirection(scrollDirection);
|
|
}
|
|
}
|
|
|
|
/// Immutable layout constraints for [RenderSliver] layout.
|
|
///
|
|
/// The [SliverConstraints] describe the current scroll state of the viewport
|
|
/// from the point of view of the sliver receiving the constraints. For example,
|
|
/// a [scrollOffset] of zero means that the leading edge of the sliver is
|
|
/// visible in the viewport, not that the viewport itself has a zero scroll
|
|
/// offset.
|
|
class SliverConstraints extends Constraints {
|
|
/// Creates sliver constraints with the given information.
|
|
///
|
|
/// All of the argument must not be null.
|
|
const SliverConstraints({
|
|
required this.axisDirection,
|
|
required this.growthDirection,
|
|
required this.userScrollDirection,
|
|
required this.scrollOffset,
|
|
required this.precedingScrollExtent,
|
|
required this.overlap,
|
|
required this.remainingPaintExtent,
|
|
required this.crossAxisExtent,
|
|
required this.crossAxisDirection,
|
|
required this.viewportMainAxisExtent,
|
|
required this.remainingCacheExtent,
|
|
required this.cacheOrigin,
|
|
});
|
|
|
|
/// Creates a copy of this object but with the given fields replaced with the
|
|
/// new values.
|
|
SliverConstraints copyWith({
|
|
AxisDirection? axisDirection,
|
|
GrowthDirection? growthDirection,
|
|
ScrollDirection? userScrollDirection,
|
|
double? scrollOffset,
|
|
double? precedingScrollExtent,
|
|
double? overlap,
|
|
double? remainingPaintExtent,
|
|
double? crossAxisExtent,
|
|
AxisDirection? crossAxisDirection,
|
|
double? viewportMainAxisExtent,
|
|
double? remainingCacheExtent,
|
|
double? cacheOrigin,
|
|
}) {
|
|
return SliverConstraints(
|
|
axisDirection: axisDirection ?? this.axisDirection,
|
|
growthDirection: growthDirection ?? this.growthDirection,
|
|
userScrollDirection: userScrollDirection ?? this.userScrollDirection,
|
|
scrollOffset: scrollOffset ?? this.scrollOffset,
|
|
precedingScrollExtent: precedingScrollExtent ?? this.precedingScrollExtent,
|
|
overlap: overlap ?? this.overlap,
|
|
remainingPaintExtent: remainingPaintExtent ?? this.remainingPaintExtent,
|
|
crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent,
|
|
crossAxisDirection: crossAxisDirection ?? this.crossAxisDirection,
|
|
viewportMainAxisExtent: viewportMainAxisExtent ?? this.viewportMainAxisExtent,
|
|
remainingCacheExtent: remainingCacheExtent ?? this.remainingCacheExtent,
|
|
cacheOrigin: cacheOrigin ?? this.cacheOrigin,
|
|
);
|
|
}
|
|
|
|
/// The direction in which the [scrollOffset] and [remainingPaintExtent]
|
|
/// increase.
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample shows a [CustomScrollView], with [Radio] buttons in the
|
|
/// [AppBar.bottom] that change the [AxisDirection] to illustrate different
|
|
/// configurations.
|
|
///
|
|
/// ** See code in examples/api/lib/painting/axis_direction/axis_direction.0.dart **
|
|
/// {@end-tool}
|
|
final AxisDirection axisDirection;
|
|
|
|
/// The direction in which the contents of slivers are ordered, relative to
|
|
/// the [axisDirection].
|
|
///
|
|
/// For example, if the [axisDirection] is [AxisDirection.up], and the
|
|
/// [growthDirection] is [GrowthDirection.forward], then an alphabetical list
|
|
/// will have A at the bottom, then B, then C, and so forth, with Z at the
|
|
/// top, with the bottom of the A at scroll offset zero, and the top of the Z
|
|
/// at the highest scroll offset.
|
|
///
|
|
/// If a viewport has an overall [AxisDirection] of [AxisDirection.down], then
|
|
/// slivers above the absolute zero offset will have an axis of
|
|
/// [AxisDirection.up] and a growth direction of [GrowthDirection.reverse],
|
|
/// while slivers below the absolute zero offset will have the same axis
|
|
/// direction as the viewport and a growth direction of
|
|
/// [GrowthDirection.forward]. (The slivers with a reverse growth direction
|
|
/// still see only positive scroll offsets; the scroll offsets are reversed as
|
|
/// well, with zero at the absolute zero point, and positive numbers going
|
|
/// away from there.)
|
|
///
|
|
/// Normally, the absolute zero offset is determined by the viewport's
|
|
/// [RenderViewport.center] and [RenderViewport.anchor] properties.
|
|
///
|
|
/// {@macro flutter.rendering.GrowthDirection.sample}
|
|
final GrowthDirection growthDirection;
|
|
|
|
/// The direction in which the user is attempting to scroll, relative to the
|
|
/// [axisDirection] and [growthDirection].
|
|
///
|
|
/// For example, if [growthDirection] is [GrowthDirection.forward] and
|
|
/// [axisDirection] is [AxisDirection.down], then a
|
|
/// [ScrollDirection.reverse] means that the user is scrolling down, in the
|
|
/// positive [scrollOffset] direction.
|
|
///
|
|
/// 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 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.
|
|
///
|
|
/// {@macro flutter.rendering.ScrollDirection.sample}
|
|
final ScrollDirection userScrollDirection;
|
|
|
|
/// The scroll offset, in this sliver's coordinate system, that corresponds to
|
|
/// the earliest visible part of this sliver in the [AxisDirection] if
|
|
/// [growthDirection] is [GrowthDirection.forward] or in the opposite
|
|
/// [AxisDirection] direction if [growthDirection] is [GrowthDirection.reverse].
|
|
///
|
|
/// For example, if [AxisDirection] is [AxisDirection.down] and [growthDirection]
|
|
/// is [GrowthDirection.forward], then scroll offset is the amount the top of
|
|
/// the sliver has been scrolled past the top of the viewport.
|
|
///
|
|
/// This value is typically used to compute whether this sliver should still
|
|
/// protrude into the viewport via [SliverGeometry.paintExtent] and
|
|
/// [SliverGeometry.layoutExtent] considering how far the beginning of the
|
|
/// sliver is above the beginning of the viewport.
|
|
///
|
|
/// For slivers whose top is not past the top of the viewport, the
|
|
/// [scrollOffset] is `0` when [AxisDirection] is [AxisDirection.down] and
|
|
/// [growthDirection] is [GrowthDirection.forward]. The set of slivers with
|
|
/// [scrollOffset] `0` includes all the slivers that are below the bottom of the
|
|
/// viewport.
|
|
///
|
|
/// [SliverConstraints.remainingPaintExtent] is typically used to accomplish
|
|
/// the same goal of computing whether scrolled out slivers should still
|
|
/// partially 'protrude in' from the bottom of the viewport.
|
|
///
|
|
/// Whether this corresponds to the beginning or the end of the sliver's
|
|
/// contents depends on the [growthDirection].
|
|
final double scrollOffset;
|
|
|
|
/// The scroll distance that has been consumed by all [RenderSliver]s that
|
|
/// came before this [RenderSliver].
|
|
///
|
|
/// # Edge Cases
|
|
///
|
|
/// [RenderSliver]s often lazily create their internal content as layout
|
|
/// occurs, e.g., [SliverList]. In this case, when [RenderSliver]s exceed the
|
|
/// viewport, their children are built lazily, and the [RenderSliver] does not
|
|
/// have enough information to estimate its total extent,
|
|
/// [precedingScrollExtent] will be [double.infinity] for all [RenderSliver]s
|
|
/// that appear after the lazily constructed child. This is because a total
|
|
/// [SliverGeometry.scrollExtent] cannot be calculated unless all inner
|
|
/// children have been created and sized, or the number of children and
|
|
/// estimated extents are provided. The infinite [SliverGeometry.scrollExtent]
|
|
/// will become finite as soon as enough information is available to estimate
|
|
/// the overall extent of all children within the given [RenderSliver].
|
|
///
|
|
/// [RenderSliver]s may legitimately be infinite, meaning that they can scroll
|
|
/// content forever without reaching the end. For any [RenderSliver]s that
|
|
/// appear after the infinite [RenderSliver], the [precedingScrollExtent] will
|
|
/// be [double.infinity].
|
|
final double precedingScrollExtent;
|
|
|
|
/// The number of pixels from where the pixels corresponding to the
|
|
/// [scrollOffset] will be painted up to the first pixel that has not yet been
|
|
/// painted on by an earlier sliver, in the [axisDirection].
|
|
///
|
|
/// For example, if the previous sliver had a [SliverGeometry.paintExtent] of
|
|
/// 100.0 pixels but a [SliverGeometry.layoutExtent] of only 50.0 pixels,
|
|
/// then the [overlap] of this sliver will be 50.0.
|
|
///
|
|
/// This is typically ignored unless the sliver is itself going to be pinned
|
|
/// or floating and wants to avoid doing so under the previous sliver.
|
|
final double overlap;
|
|
|
|
/// The number of pixels of content that the sliver should consider providing.
|
|
/// (Providing more pixels than this is inefficient.)
|
|
///
|
|
/// The actual number of pixels provided should be specified in the
|
|
/// [RenderSliver.geometry] as [SliverGeometry.paintExtent].
|
|
///
|
|
/// This value may be infinite, for example if the viewport is an
|
|
/// unconstrained [RenderShrinkWrappingViewport].
|
|
///
|
|
/// This value may be 0.0, for example if the sliver is scrolled off the
|
|
/// bottom of a downwards vertical viewport.
|
|
final double remainingPaintExtent;
|
|
|
|
/// The number of pixels in the cross-axis.
|
|
///
|
|
/// For a vertical list, this is the width of the sliver.
|
|
final double crossAxisExtent;
|
|
|
|
/// The direction in which children should be placed in the cross axis.
|
|
///
|
|
/// Typically used in vertical lists to describe whether the ambient
|
|
/// [TextDirection] is [TextDirection.rtl] or [TextDirection.ltr].
|
|
final AxisDirection crossAxisDirection;
|
|
|
|
/// The number of pixels the viewport can display in the main axis.
|
|
///
|
|
/// For a vertical list, this is the height of the viewport.
|
|
final double viewportMainAxisExtent;
|
|
|
|
/// Where the cache area starts relative to the [scrollOffset].
|
|
///
|
|
/// Slivers that fall into the cache area located before the leading edge and
|
|
/// after the trailing edge of the viewport should still render content
|
|
/// because they are about to become visible when the user scrolls.
|
|
///
|
|
/// The [cacheOrigin] describes where the [remainingCacheExtent] starts relative
|
|
/// to the [scrollOffset]. A cache origin of 0 means that the sliver does not
|
|
/// have to provide any content before the current [scrollOffset]. A
|
|
/// [cacheOrigin] of -250.0 means that even though the first visible part of
|
|
/// the sliver will be at the provided [scrollOffset], the sliver should
|
|
/// render content starting 250.0 before the [scrollOffset] to fill the
|
|
/// cache area of the viewport.
|
|
///
|
|
/// The [cacheOrigin] is always negative or zero and will never exceed
|
|
/// -[scrollOffset]. In other words, a sliver is never asked to provide
|
|
/// content before its zero [scrollOffset].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderViewport.cacheExtent] for a description of a viewport's cache area.
|
|
final double cacheOrigin;
|
|
|
|
|
|
/// Describes how much content the sliver should provide starting from the
|
|
/// [cacheOrigin].
|
|
///
|
|
/// Not all content in the [remainingCacheExtent] will be visible as some
|
|
/// of it might fall into the cache area of the viewport.
|
|
///
|
|
/// Each sliver should start laying out content at the [cacheOrigin] and
|
|
/// try to provide as much content as the [remainingCacheExtent] allows.
|
|
///
|
|
/// The [remainingCacheExtent] is always larger or equal to the
|
|
/// [remainingPaintExtent]. Content, that falls in the [remainingCacheExtent],
|
|
/// but is outside of the [remainingPaintExtent] is currently not visible
|
|
/// in the viewport.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderViewport.cacheExtent] for a description of a viewport's cache area.
|
|
final double remainingCacheExtent;
|
|
|
|
/// The axis along which the [scrollOffset] and [remainingPaintExtent] are measured.
|
|
Axis get axis => axisDirectionToAxis(axisDirection);
|
|
|
|
/// Return what the [growthDirection] would be if the [axisDirection] was
|
|
/// either [AxisDirection.down] or [AxisDirection.right].
|
|
///
|
|
/// This is the same as [growthDirection] unless the [axisDirection] is either
|
|
/// [AxisDirection.up] or [AxisDirection.left], in which case it is the
|
|
/// opposite growth direction.
|
|
///
|
|
/// This can be useful in combination with [axis] to view the [axisDirection]
|
|
/// and [growthDirection] in different terms.
|
|
GrowthDirection get normalizedGrowthDirection {
|
|
switch (axisDirection) {
|
|
case AxisDirection.down:
|
|
case AxisDirection.right:
|
|
return growthDirection;
|
|
case AxisDirection.up:
|
|
case AxisDirection.left:
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
return GrowthDirection.reverse;
|
|
case GrowthDirection.reverse:
|
|
return GrowthDirection.forward;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool get isTight => false;
|
|
|
|
@override
|
|
bool get isNormalized {
|
|
return scrollOffset >= 0.0
|
|
&& crossAxisExtent >= 0.0
|
|
&& axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)
|
|
&& viewportMainAxisExtent >= 0.0
|
|
&& remainingPaintExtent >= 0.0;
|
|
}
|
|
|
|
/// Returns [BoxConstraints] that reflects the sliver constraints.
|
|
///
|
|
/// The `minExtent` and `maxExtent` are used as the constraints in the main
|
|
/// axis. If non-null, the given `crossAxisExtent` is used as a tight
|
|
/// constraint in the cross axis. Otherwise, the [crossAxisExtent] from this
|
|
/// object is used as a constraint in the cross axis.
|
|
///
|
|
/// Useful for slivers that have [RenderBox] children.
|
|
BoxConstraints asBoxConstraints({
|
|
double minExtent = 0.0,
|
|
double maxExtent = double.infinity,
|
|
double? crossAxisExtent,
|
|
}) {
|
|
crossAxisExtent ??= this.crossAxisExtent;
|
|
switch (axis) {
|
|
case Axis.horizontal:
|
|
return BoxConstraints(
|
|
minHeight: crossAxisExtent,
|
|
maxHeight: crossAxisExtent,
|
|
minWidth: minExtent,
|
|
maxWidth: maxExtent,
|
|
);
|
|
case Axis.vertical:
|
|
return BoxConstraints(
|
|
minWidth: crossAxisExtent,
|
|
maxWidth: crossAxisExtent,
|
|
minHeight: minExtent,
|
|
maxHeight: maxExtent,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool debugAssertIsValid({
|
|
bool isAppliedConstraint = false,
|
|
InformationCollector? informationCollector,
|
|
}) {
|
|
assert(() {
|
|
bool hasErrors = false;
|
|
final StringBuffer errorMessage = StringBuffer('\n');
|
|
void verify(bool check, String message) {
|
|
if (check) {
|
|
return;
|
|
}
|
|
hasErrors = true;
|
|
errorMessage.writeln(' $message');
|
|
}
|
|
void verifyDouble(double property, String name, {bool mustBePositive = false, bool mustBeNegative = false}) {
|
|
if (property.isNaN) {
|
|
String additional = '.';
|
|
if (mustBePositive) {
|
|
additional = ', expected greater than or equal to zero.';
|
|
} else if (mustBeNegative) {
|
|
additional = ', expected less than or equal to zero.';
|
|
}
|
|
verify(false, 'The "$name" is NaN$additional');
|
|
} else if (mustBePositive) {
|
|
verify(property >= 0.0, 'The "$name" is negative.');
|
|
} else if (mustBeNegative) {
|
|
verify(property <= 0.0, 'The "$name" is positive.');
|
|
}
|
|
}
|
|
verifyDouble(scrollOffset, 'scrollOffset');
|
|
verifyDouble(overlap, 'overlap');
|
|
verifyDouble(crossAxisExtent, 'crossAxisExtent');
|
|
verifyDouble(scrollOffset, 'scrollOffset', mustBePositive: true);
|
|
verify(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection), 'The "axisDirection" and the "crossAxisDirection" are along the same axis.');
|
|
verifyDouble(viewportMainAxisExtent, 'viewportMainAxisExtent', mustBePositive: true);
|
|
verifyDouble(remainingPaintExtent, 'remainingPaintExtent', mustBePositive: true);
|
|
verifyDouble(remainingCacheExtent, 'remainingCacheExtent', mustBePositive: true);
|
|
verifyDouble(cacheOrigin, 'cacheOrigin', mustBeNegative: true);
|
|
verifyDouble(precedingScrollExtent, 'precedingScrollExtent', mustBePositive: true);
|
|
verify(isNormalized, 'The constraints are not normalized.'); // should be redundant with earlier checks
|
|
if (hasErrors) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('$runtimeType is not valid: $errorMessage'),
|
|
if (informationCollector != null)
|
|
...informationCollector(),
|
|
DiagnosticsProperty<SliverConstraints>('The offending constraints were', this, style: DiagnosticsTreeStyle.errorProperty),
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) {
|
|
return true;
|
|
}
|
|
if (other is! SliverConstraints) {
|
|
return false;
|
|
}
|
|
assert(other.debugAssertIsValid());
|
|
return other.axisDirection == axisDirection
|
|
&& other.growthDirection == growthDirection
|
|
&& other.scrollOffset == scrollOffset
|
|
&& other.overlap == overlap
|
|
&& other.remainingPaintExtent == remainingPaintExtent
|
|
&& other.crossAxisExtent == crossAxisExtent
|
|
&& other.crossAxisDirection == crossAxisDirection
|
|
&& other.viewportMainAxisExtent == viewportMainAxisExtent
|
|
&& other.remainingCacheExtent == remainingCacheExtent
|
|
&& other.cacheOrigin == cacheOrigin;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(
|
|
axisDirection,
|
|
growthDirection,
|
|
scrollOffset,
|
|
overlap,
|
|
remainingPaintExtent,
|
|
crossAxisExtent,
|
|
crossAxisDirection,
|
|
viewportMainAxisExtent,
|
|
remainingCacheExtent,
|
|
cacheOrigin,
|
|
);
|
|
|
|
@override
|
|
String toString() {
|
|
final List<String> properties = <String>[
|
|
'$axisDirection',
|
|
'$growthDirection',
|
|
'$userScrollDirection',
|
|
'scrollOffset: ${scrollOffset.toStringAsFixed(1)}',
|
|
'remainingPaintExtent: ${remainingPaintExtent.toStringAsFixed(1)}',
|
|
if (overlap != 0.0) 'overlap: ${overlap.toStringAsFixed(1)}',
|
|
'crossAxisExtent: ${crossAxisExtent.toStringAsFixed(1)}',
|
|
'crossAxisDirection: $crossAxisDirection',
|
|
'viewportMainAxisExtent: ${viewportMainAxisExtent.toStringAsFixed(1)}',
|
|
'remainingCacheExtent: ${remainingCacheExtent.toStringAsFixed(1)}',
|
|
'cacheOrigin: ${cacheOrigin.toStringAsFixed(1)}',
|
|
];
|
|
return 'SliverConstraints(${properties.join(', ')})';
|
|
}
|
|
}
|
|
|
|
/// Describes the amount of space occupied by a [RenderSliver].
|
|
///
|
|
/// A sliver can occupy space in several different ways, which is why this class
|
|
/// contains multiple values.
|
|
@immutable
|
|
class SliverGeometry with Diagnosticable {
|
|
/// Creates an object that describes the amount of space occupied by a sliver.
|
|
///
|
|
/// If the [layoutExtent] argument is null, [layoutExtent] defaults to the
|
|
/// [paintExtent]. If the [hitTestExtent] argument is null, [hitTestExtent]
|
|
/// defaults to the [paintExtent]. If [visible] is null, [visible] defaults to
|
|
/// whether [paintExtent] is greater than zero.
|
|
///
|
|
/// The other arguments must not be null.
|
|
const SliverGeometry({
|
|
this.scrollExtent = 0.0,
|
|
this.paintExtent = 0.0,
|
|
this.paintOrigin = 0.0,
|
|
double? layoutExtent,
|
|
this.maxPaintExtent = 0.0,
|
|
this.maxScrollObstructionExtent = 0.0,
|
|
this.crossAxisExtent,
|
|
double? hitTestExtent,
|
|
bool? visible,
|
|
this.hasVisualOverflow = false,
|
|
this.scrollOffsetCorrection,
|
|
double? cacheExtent,
|
|
}) : assert(scrollOffsetCorrection != 0.0),
|
|
layoutExtent = layoutExtent ?? paintExtent,
|
|
hitTestExtent = hitTestExtent ?? paintExtent,
|
|
cacheExtent = cacheExtent ?? layoutExtent ?? paintExtent,
|
|
visible = visible ?? paintExtent > 0.0;
|
|
|
|
/// Creates a copy of this object but with the given fields replaced with the
|
|
/// new values.
|
|
SliverGeometry copyWith({
|
|
double? scrollExtent,
|
|
double? paintExtent,
|
|
double? paintOrigin,
|
|
double? layoutExtent,
|
|
double? maxPaintExtent,
|
|
double? maxScrollObstructionExtent,
|
|
double? crossAxisExtent,
|
|
double? hitTestExtent,
|
|
bool? visible,
|
|
bool? hasVisualOverflow,
|
|
double? cacheExtent,
|
|
}) {
|
|
return SliverGeometry(
|
|
scrollExtent: scrollExtent ?? this.scrollExtent,
|
|
paintExtent: paintExtent ?? this.paintExtent,
|
|
paintOrigin: paintOrigin ?? this.paintOrigin,
|
|
layoutExtent: layoutExtent ?? this.layoutExtent,
|
|
maxPaintExtent: maxPaintExtent ?? this.maxPaintExtent,
|
|
maxScrollObstructionExtent: maxScrollObstructionExtent ?? this.maxScrollObstructionExtent,
|
|
crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent,
|
|
hitTestExtent: hitTestExtent ?? this.hitTestExtent,
|
|
visible: visible ?? this.visible,
|
|
hasVisualOverflow: hasVisualOverflow ?? this.hasVisualOverflow,
|
|
cacheExtent: cacheExtent ?? this.cacheExtent,
|
|
);
|
|
}
|
|
|
|
/// A sliver that occupies no space at all.
|
|
static const SliverGeometry zero = SliverGeometry();
|
|
|
|
/// The (estimated) total scrollable extent that this sliver has content for.
|
|
///
|
|
/// This is the amount of scrolling the user needs to do to get from the
|
|
/// beginning of this sliver to the end of this sliver.
|
|
///
|
|
/// The value is used to calculate the [SliverConstraints.scrollOffset] of
|
|
/// all slivers in the scrollable and thus should be provided whether the
|
|
/// sliver is currently in the viewport or not.
|
|
///
|
|
/// In a typical scrolling scenario, the [scrollExtent] is constant for a
|
|
/// sliver throughout the scrolling while [paintExtent] and [layoutExtent]
|
|
/// will progress from `0` when offscreen to between `0` and [scrollExtent]
|
|
/// as the sliver scrolls partially into and out of the screen and is
|
|
/// equal to [scrollExtent] while the sliver is entirely on screen. However,
|
|
/// these relationships can be customized to achieve more special effects.
|
|
///
|
|
/// This value must be accurate if the [paintExtent] is less than the
|
|
/// [SliverConstraints.remainingPaintExtent] provided during layout.
|
|
final double scrollExtent;
|
|
|
|
/// The visual location of the first visible part of this sliver relative to
|
|
/// its layout position.
|
|
///
|
|
/// For example, if the sliver wishes to paint visually before its layout
|
|
/// position, the [paintOrigin] is negative. The coordinate system this sliver
|
|
/// uses for painting is relative to this [paintOrigin]. In other words,
|
|
/// when [RenderSliver.paint] is called, the (0, 0) position of the [Offset]
|
|
/// given to it is at this [paintOrigin].
|
|
///
|
|
/// The coordinate system used for the [paintOrigin] itself is relative
|
|
/// to the start of this sliver's layout position rather than relative to
|
|
/// its current position on the viewport. In other words, in a typical
|
|
/// scrolling scenario, [paintOrigin] remains constant at 0.0 rather than
|
|
/// tracking from 0.0 to [SliverConstraints.viewportMainAxisExtent] as the
|
|
/// sliver scrolls past the viewport.
|
|
///
|
|
/// This value does not affect the layout of subsequent slivers. The next
|
|
/// sliver is still placed at [layoutExtent] after this sliver's layout
|
|
/// position. This value does affect where the [paintExtent] extent is
|
|
/// measured from when computing the [SliverConstraints.overlap] for the next
|
|
/// sliver.
|
|
///
|
|
/// Defaults to 0.0, which means slivers start painting at their layout
|
|
/// position by default.
|
|
final double paintOrigin;
|
|
|
|
/// The amount of currently visible visual space that was taken by the sliver
|
|
/// to render the subset of the sliver that covers all or part of the
|
|
/// [SliverConstraints.remainingPaintExtent] in the current viewport.
|
|
///
|
|
/// This value does not affect how the next sliver is positioned. In other
|
|
/// words, if this value was 100 and [layoutExtent] was 0, typical slivers
|
|
/// placed after it would end up drawing in the same 100 pixel space while
|
|
/// painting.
|
|
///
|
|
/// This must be between zero and [SliverConstraints.remainingPaintExtent].
|
|
///
|
|
/// This value is typically 0 when outside of the viewport and grows or
|
|
/// shrinks from 0 or to 0 as the sliver is being scrolled into and out of the
|
|
/// viewport unless the sliver wants to achieve a special effect and paint
|
|
/// even when scrolled away.
|
|
///
|
|
/// This contributes to the calculation for the next sliver's
|
|
/// [SliverConstraints.overlap].
|
|
final double paintExtent;
|
|
|
|
/// The distance from the first visible part of this sliver to the first
|
|
/// visible part of the next sliver, assuming the next sliver's
|
|
/// [SliverConstraints.scrollOffset] is zero.
|
|
///
|
|
/// This must be between zero and [paintExtent]. It defaults to [paintExtent].
|
|
///
|
|
/// This value is typically 0 when outside of the viewport and grows or
|
|
/// shrinks from 0 or to 0 as the sliver is being scrolled into and out of the
|
|
/// viewport unless the sliver wants to achieve a special effect and push
|
|
/// down the layout start position of subsequent slivers before the sliver is
|
|
/// even scrolled into the viewport.
|
|
final double layoutExtent;
|
|
|
|
/// The (estimated) total paint extent that this sliver would be able to
|
|
/// provide if the [SliverConstraints.remainingPaintExtent] was infinite.
|
|
///
|
|
/// This is used by viewports that implement shrink-wrapping.
|
|
///
|
|
/// By definition, this cannot be less than [paintExtent].
|
|
final double maxPaintExtent;
|
|
|
|
/// The maximum extent by which this sliver can reduce the area in which
|
|
/// content can scroll if the sliver were pinned at the edge.
|
|
///
|
|
/// Slivers that never get pinned at the edge, should return zero.
|
|
///
|
|
/// A pinned app bar is an example for a sliver that would use this setting:
|
|
/// When the app bar is pinned to the top, the area in which content can
|
|
/// actually scroll is reduced by the height of the app bar.
|
|
final double maxScrollObstructionExtent;
|
|
|
|
/// The distance from where this sliver started painting to the bottom of
|
|
/// where it should accept hits.
|
|
///
|
|
/// This must be between zero and [paintExtent]. It defaults to [paintExtent].
|
|
final double hitTestExtent;
|
|
|
|
/// Whether this sliver should be painted.
|
|
///
|
|
/// By default, this is true if [paintExtent] is greater than zero, and
|
|
/// false if [paintExtent] is zero.
|
|
final bool visible;
|
|
|
|
/// Whether this sliver has visual overflow.
|
|
///
|
|
/// By default, this is false, which means the viewport does not need to clip
|
|
/// its children. If any slivers have visual overflow, the viewport will apply
|
|
/// a clip to its children.
|
|
final bool hasVisualOverflow;
|
|
|
|
/// If this is non-zero after [RenderSliver.performLayout] returns, the scroll
|
|
/// offset will be adjusted by the parent and then the entire layout of the
|
|
/// parent will be rerun.
|
|
///
|
|
/// When the value is non-zero, the [RenderSliver] does not need to compute
|
|
/// the rest of the values when constructing the [SliverGeometry] or call
|
|
/// [RenderObject.layout] on its children since [RenderSliver.performLayout]
|
|
/// will be called again on this sliver in the same frame after the
|
|
/// [SliverConstraints.scrollOffset] correction has been applied, when the
|
|
/// proper [SliverGeometry] and layout of its children can be computed.
|
|
///
|
|
/// If the parent is also a [RenderSliver], it must propagate this value
|
|
/// in its own [RenderSliver.geometry] property until a viewport which adjusts
|
|
/// its offset based on this value.
|
|
final double? scrollOffsetCorrection;
|
|
|
|
/// How many pixels the sliver has consumed in the
|
|
/// [SliverConstraints.remainingCacheExtent].
|
|
///
|
|
/// This value should be equal to or larger than the [layoutExtent] because
|
|
/// the sliver always consumes at least the [layoutExtent] from the
|
|
/// [SliverConstraints.remainingCacheExtent] and possibly more if it falls
|
|
/// into the cache area of the viewport.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderViewport.cacheExtent] for a description of a viewport's cache area.
|
|
final double cacheExtent;
|
|
|
|
/// The amount of space allocated to the cross axis.
|
|
///
|
|
/// This value will be typically null unless it is different from
|
|
/// [SliverConstraints.crossAxisExtent]. If null, then the cross axis extent of
|
|
/// the sliver is assumed to be the same as the [SliverConstraints.crossAxisExtent].
|
|
/// This is because slivers typically consume all of the extent that is available
|
|
/// in the cross axis.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [SliverConstrainedCrossAxis] for an example of a sliver which takes up
|
|
/// a smaller cross axis extent than the provided constraint.
|
|
/// * [SliverCrossAxisGroup] for an example of a sliver which makes use of this
|
|
/// [crossAxisExtent] to lay out their children.
|
|
final double? crossAxisExtent;
|
|
|
|
/// Asserts that this geometry is internally consistent.
|
|
///
|
|
/// Does nothing if asserts are disabled. Always returns true.
|
|
bool debugAssertIsValid({
|
|
InformationCollector? informationCollector,
|
|
}) {
|
|
assert(() {
|
|
void verify(bool check, String summary, {List<DiagnosticsNode>? details}) {
|
|
if (check) {
|
|
return;
|
|
}
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('${objectRuntimeType(this, 'SliverGeometry')} is not valid: $summary'),
|
|
...?details,
|
|
if (informationCollector != null)
|
|
...informationCollector(),
|
|
]);
|
|
}
|
|
|
|
verify(scrollExtent >= 0.0, 'The "scrollExtent" is negative.');
|
|
verify(paintExtent >= 0.0, 'The "paintExtent" is negative.');
|
|
verify(layoutExtent >= 0.0, 'The "layoutExtent" is negative.');
|
|
verify(cacheExtent >= 0.0, 'The "cacheExtent" is negative.');
|
|
if (layoutExtent > paintExtent) {
|
|
verify(false,
|
|
'The "layoutExtent" exceeds the "paintExtent".',
|
|
details: _debugCompareFloats('paintExtent', paintExtent, 'layoutExtent', layoutExtent),
|
|
);
|
|
}
|
|
// If the paintExtent is slightly more than the maxPaintExtent, but the difference is still less
|
|
// than precisionErrorTolerance, we will not throw the assert below.
|
|
if (paintExtent - maxPaintExtent > precisionErrorTolerance) {
|
|
verify(false,
|
|
'The "maxPaintExtent" is less than the "paintExtent".',
|
|
details:
|
|
_debugCompareFloats('maxPaintExtent', maxPaintExtent, 'paintExtent', paintExtent)
|
|
..add(ErrorDescription("By definition, a sliver can't paint more than the maximum that it can paint!")),
|
|
);
|
|
}
|
|
verify(hitTestExtent >= 0.0, 'The "hitTestExtent" is negative.');
|
|
verify(scrollOffsetCorrection != 0.0, 'The "scrollOffsetCorrection" is zero.');
|
|
return true;
|
|
}());
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
String toStringShort() => objectRuntimeType(this, 'SliverGeometry');
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(DoubleProperty('scrollExtent', scrollExtent));
|
|
if (paintExtent > 0.0) {
|
|
properties.add(DoubleProperty('paintExtent', paintExtent, unit : visible ? null : ' but not painting'));
|
|
} else if (paintExtent == 0.0) {
|
|
if (visible) {
|
|
properties.add(DoubleProperty('paintExtent', paintExtent, unit: visible ? null : ' but visible'));
|
|
}
|
|
properties.add(FlagProperty('visible', value: visible, ifFalse: 'hidden'));
|
|
} else {
|
|
// Negative paintExtent!
|
|
properties.add(DoubleProperty('paintExtent', paintExtent, tooltip: '!'));
|
|
}
|
|
properties.add(DoubleProperty('paintOrigin', paintOrigin, defaultValue: 0.0));
|
|
properties.add(DoubleProperty('layoutExtent', layoutExtent, defaultValue: paintExtent));
|
|
properties.add(DoubleProperty('maxPaintExtent', maxPaintExtent));
|
|
properties.add(DoubleProperty('hitTestExtent', hitTestExtent, defaultValue: paintExtent));
|
|
properties.add(DiagnosticsProperty<bool>('hasVisualOverflow', hasVisualOverflow, defaultValue: false));
|
|
properties.add(DoubleProperty('scrollOffsetCorrection', scrollOffsetCorrection, defaultValue: null));
|
|
properties.add(DoubleProperty('cacheExtent', cacheExtent, defaultValue: 0.0));
|
|
}
|
|
}
|
|
|
|
/// Method signature for hit testing a [RenderSliver].
|
|
///
|
|
/// Used by [SliverHitTestResult.addWithAxisOffset] to hit test [RenderSliver]
|
|
/// children.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderSliver.hitTest], which documents more details around hit testing
|
|
/// [RenderSliver]s.
|
|
typedef SliverHitTest = bool Function(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition });
|
|
|
|
/// The result of performing a hit test on [RenderSliver]s.
|
|
///
|
|
/// An instance of this class is provided to [RenderSliver.hitTest] to record
|
|
/// the result of the hit test.
|
|
class SliverHitTestResult extends HitTestResult {
|
|
/// Creates an empty hit test result for hit testing on [RenderSliver].
|
|
SliverHitTestResult() : super();
|
|
|
|
/// Wraps `result` to create a [HitTestResult] that implements the
|
|
/// [SliverHitTestResult] protocol for hit testing on [RenderSliver]s.
|
|
///
|
|
/// This method is used by [RenderObject]s that adapt between the
|
|
/// [RenderSliver]-world and the non-[RenderSliver]-world to convert a
|
|
/// (subtype of) [HitTestResult] to a [SliverHitTestResult] for hit testing on
|
|
/// [RenderSliver]s.
|
|
///
|
|
/// The [HitTestEntry] instances added to the returned [SliverHitTestResult]
|
|
/// are also added to the wrapped `result` (both share the same underlying
|
|
/// data structure to store [HitTestEntry] instances).
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [HitTestResult.wrap], which turns a [SliverHitTestResult] back into a
|
|
/// generic [HitTestResult].
|
|
/// * [BoxHitTestResult.wrap], which turns a [SliverHitTestResult] into a
|
|
/// [BoxHitTestResult] for hit testing on [RenderBox] children.
|
|
SliverHitTestResult.wrap(super.result) : super.wrap();
|
|
|
|
/// Transforms `mainAxisPosition` and `crossAxisPosition` to the local
|
|
/// coordinate system of a child for hit-testing the child.
|
|
///
|
|
/// The actual hit testing of the child needs to be implemented in the
|
|
/// provided `hitTest` callback, which is invoked with the transformed
|
|
/// `position` as argument.
|
|
///
|
|
/// For the transform `mainAxisOffset` is subtracted from `mainAxisPosition`
|
|
/// and `crossAxisOffset` is subtracted from `crossAxisPosition`.
|
|
///
|
|
/// The `paintOffset` describes how the paint position of a point painted at
|
|
/// the provided `mainAxisPosition` and `crossAxisPosition` would change after
|
|
/// `mainAxisOffset` and `crossAxisOffset` have been applied. This
|
|
/// `paintOffset` is used to properly convert [PointerEvent]s to the local
|
|
/// coordinate system of the event receiver.
|
|
///
|
|
/// The `paintOffset` may be null if `mainAxisOffset` and `crossAxisOffset` are
|
|
/// both zero.
|
|
///
|
|
/// The function returns the return value of `hitTest`.
|
|
bool addWithAxisOffset({
|
|
required Offset? paintOffset,
|
|
required double mainAxisOffset,
|
|
required double crossAxisOffset,
|
|
required double mainAxisPosition,
|
|
required double crossAxisPosition,
|
|
required SliverHitTest hitTest,
|
|
}) {
|
|
if (paintOffset != null) {
|
|
pushOffset(-paintOffset);
|
|
}
|
|
final bool isHit = hitTest(
|
|
this,
|
|
mainAxisPosition: mainAxisPosition - mainAxisOffset,
|
|
crossAxisPosition: crossAxisPosition - crossAxisOffset,
|
|
);
|
|
if (paintOffset != null) {
|
|
popTransform();
|
|
}
|
|
return isHit;
|
|
}
|
|
}
|
|
|
|
/// A hit test entry used by [RenderSliver].
|
|
///
|
|
/// The coordinate system used by this hit test entry is relative to the
|
|
/// [AxisDirection] of the target sliver.
|
|
class SliverHitTestEntry extends HitTestEntry<RenderSliver> {
|
|
/// Creates a sliver hit test entry.
|
|
///
|
|
/// The [mainAxisPosition] and [crossAxisPosition] arguments must not be null.
|
|
SliverHitTestEntry(
|
|
super.target, {
|
|
required this.mainAxisPosition,
|
|
required this.crossAxisPosition,
|
|
});
|
|
|
|
/// The distance in the [AxisDirection] from the edge of the sliver's painted
|
|
/// area (as given by the [SliverConstraints.scrollOffset]) to the hit point.
|
|
/// This can be an unusual direction, for example in the [AxisDirection.up]
|
|
/// case this is a distance from the _bottom_ of the sliver's painted area.
|
|
final double mainAxisPosition;
|
|
|
|
/// The distance to the hit point in the axis opposite the
|
|
/// [SliverConstraints.axis].
|
|
///
|
|
/// If the cross axis is horizontal (i.e. the
|
|
/// [SliverConstraints.axisDirection] is either [AxisDirection.down] or
|
|
/// [AxisDirection.up]), then the [crossAxisPosition] is a distance from the
|
|
/// left edge of the sliver. If the cross axis is vertical (i.e. the
|
|
/// [SliverConstraints.axisDirection] is either [AxisDirection.right] or
|
|
/// [AxisDirection.left]), then the [crossAxisPosition] is a distance from the
|
|
/// top edge of the sliver.
|
|
///
|
|
/// This is always a distance from the left or top of the parent, never a
|
|
/// distance from the right or bottom.
|
|
final double crossAxisPosition;
|
|
|
|
@override
|
|
String toString() => '${target.runtimeType}@(mainAxis: $mainAxisPosition, crossAxis: $crossAxisPosition)';
|
|
}
|
|
|
|
/// Parent data structure used by parents of slivers that position their
|
|
/// children using layout offsets.
|
|
///
|
|
/// This data structure is optimized for fast layout. It is best used by parents
|
|
/// that expect to have many children whose relative positions don't change even
|
|
/// when the scroll offset does.
|
|
class SliverLogicalParentData extends ParentData {
|
|
/// The position of the child relative to the zero scroll offset.
|
|
///
|
|
/// The number of pixels from the zero scroll offset of the parent sliver
|
|
/// (the line at which its [SliverConstraints.scrollOffset] is zero) to the
|
|
/// side of the child closest to that offset. A [layoutOffset] can be null
|
|
/// when it cannot be determined. The value will be set after layout.
|
|
///
|
|
/// In a typical list, this does not change as the parent is scrolled.
|
|
///
|
|
/// Defaults to null.
|
|
double? layoutOffset;
|
|
|
|
@override
|
|
String toString() => 'layoutOffset=${layoutOffset == null ? 'None': layoutOffset!.toStringAsFixed(1)}';
|
|
}
|
|
|
|
/// Parent data for slivers that have multiple children and that position their
|
|
/// children using layout offsets.
|
|
class SliverLogicalContainerParentData extends SliverLogicalParentData with ContainerParentDataMixin<RenderSliver> { }
|
|
|
|
/// Parent data structure used by parents of slivers that position their
|
|
/// children using absolute coordinates.
|
|
///
|
|
/// For example, used by [RenderViewport].
|
|
///
|
|
/// This data structure is optimized for fast painting, at the cost of requiring
|
|
/// additional work during layout when the children change their offsets. It is
|
|
/// best used by parents that expect to have few children, especially if those
|
|
/// children will themselves be very tall relative to the parent.
|
|
class SliverPhysicalParentData extends ParentData {
|
|
/// The position of the child relative to the parent.
|
|
///
|
|
/// This is the distance from the top left visible corner of the parent to the
|
|
/// top left visible corner of the sliver.
|
|
Offset paintOffset = Offset.zero;
|
|
|
|
/// The [crossAxisFlex] factor to use for this sliver child.
|
|
///
|
|
/// If used outside of a [SliverCrossAxisGroup] widget, this value has no meaning.
|
|
///
|
|
/// If null or zero, the child is inflexible and determines its own size in the cross axis.
|
|
/// If non-zero, the amount of space the child can occupy in the cross axis is
|
|
/// determined by dividing the free space (after placing the inflexible children)
|
|
/// according to the flex factors of the flexible children.
|
|
///
|
|
/// This value is only used by the [SliverCrossAxisGroup] widget to determine
|
|
/// how to allocate its [SliverConstraints.crossAxisExtent] to its children.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [SliverCrossAxisGroup], which lays out multiple slivers along the
|
|
/// cross axis direction.
|
|
int? crossAxisFlex;
|
|
|
|
/// Apply the [paintOffset] to the given [transform].
|
|
///
|
|
/// Used to implement [RenderObject.applyPaintTransform] by slivers that use
|
|
/// [SliverPhysicalParentData].
|
|
void applyPaintTransform(Matrix4 transform) {
|
|
// Hit test logic relies on this always providing an invertible matrix.
|
|
transform.translate(paintOffset.dx, paintOffset.dy);
|
|
}
|
|
|
|
@override
|
|
String toString() => 'paintOffset=$paintOffset';
|
|
}
|
|
|
|
/// Parent data for slivers that have multiple children and that position their
|
|
/// children using absolute coordinates.
|
|
class SliverPhysicalContainerParentData extends SliverPhysicalParentData with ContainerParentDataMixin<RenderSliver> { }
|
|
|
|
List<DiagnosticsNode> _debugCompareFloats(String labelA, double valueA, String labelB, double valueB) {
|
|
return <DiagnosticsNode>[
|
|
if (valueA.toStringAsFixed(1) != valueB.toStringAsFixed(1))
|
|
ErrorDescription(
|
|
'The $labelA is ${valueA.toStringAsFixed(1)}, but '
|
|
'the $labelB is ${valueB.toStringAsFixed(1)}.',
|
|
)
|
|
else ...<DiagnosticsNode>[
|
|
ErrorDescription('The $labelA is $valueA, but the $labelB is $valueB.'),
|
|
ErrorHint(
|
|
'Maybe you have fallen prey to floating point rounding errors, and should explicitly '
|
|
'apply the min() or max() functions, or the clamp() method, to the $labelB?',
|
|
),
|
|
],
|
|
];
|
|
}
|
|
|
|
/// Base class for the render objects that implement scroll effects in viewports.
|
|
///
|
|
/// A [RenderViewport] has a list of child slivers. Each sliver — literally a
|
|
/// slice of the viewport's contents — is laid out in turn, covering the
|
|
/// viewport in the process. (Every sliver is laid out each time, including
|
|
/// those that have zero extent because they are "scrolled off" or are beyond
|
|
/// the end of the viewport.)
|
|
///
|
|
/// Slivers participate in the _sliver protocol_, wherein during [layout] each
|
|
/// sliver receives a [SliverConstraints] object and computes a corresponding
|
|
/// [SliverGeometry] that describes where it fits in the viewport. This is
|
|
/// analogous to the box protocol used by [RenderBox], which gets a
|
|
/// [BoxConstraints] as input and computes a [Size].
|
|
///
|
|
/// Slivers have a leading edge, which is where the position described by
|
|
/// [SliverConstraints.scrollOffset] for this sliver begins. Slivers have
|
|
/// several dimensions, the primary of which is [SliverGeometry.paintExtent],
|
|
/// which describes the extent of the sliver along the main axis, starting from
|
|
/// the leading edge, reaching either the end of the viewport or the end of the
|
|
/// sliver, whichever comes first.
|
|
///
|
|
/// Slivers can change dimensions based on the changing constraints in a
|
|
/// non-linear fashion, to achieve various scroll effects. For example, the
|
|
/// various [RenderSliverPersistentHeader] subclasses, on which [SliverAppBar]
|
|
/// is based, achieve effects such as staying visible despite the scroll offset,
|
|
/// or reappearing at different offsets based on the user's scroll direction
|
|
/// ([SliverConstraints.userScrollDirection]).
|
|
///
|
|
/// {@youtube 560 315 https://www.youtube.com/watch?v=Mz3kHQxBjGg}
|
|
///
|
|
/// ## Writing a RenderSliver subclass
|
|
///
|
|
/// Slivers can have sliver children, or children from another coordinate
|
|
/// system, typically box children. (For details on the box protocol, see
|
|
/// [RenderBox].) Slivers can also have different child models, typically having
|
|
/// either one child, or a list of children.
|
|
///
|
|
/// ### Examples of slivers
|
|
///
|
|
/// A good example of a sliver with a single child that is also itself a sliver
|
|
/// is [RenderSliverPadding], which indents its child. A sliver-to-sliver render
|
|
/// object such as this must construct a [SliverConstraints] object for its
|
|
/// child, then must take its child's [SliverGeometry] and use it to form its
|
|
/// own [geometry].
|
|
///
|
|
/// The other common kind of one-child sliver is a sliver that has a single
|
|
/// [RenderBox] child. An example of that would be [RenderSliverToBoxAdapter],
|
|
/// which lays out a single box and sizes itself around the box. Such a sliver
|
|
/// must use its [SliverConstraints] to create a [BoxConstraints] for the
|
|
/// child, lay the child out (using the child's [layout] method), and then use
|
|
/// the child's [RenderBox.size] to generate the sliver's [SliverGeometry].
|
|
///
|
|
/// The most common kind of sliver though is one with multiple children. The
|
|
/// most straight-forward example of this is [RenderSliverList], which arranges
|
|
/// its children one after the other in the main axis direction. As with the
|
|
/// one-box-child sliver case, it uses its [constraints] to create a
|
|
/// [BoxConstraints] for the children, and then it uses the aggregate
|
|
/// information from all its children to generate its [geometry]. Unlike the
|
|
/// one-child cases, however, it is judicious in which children it actually lays
|
|
/// out (and later paints). If the scroll offset is 1000 pixels, and it
|
|
/// previously determined that the first three children are each 400 pixels
|
|
/// tall, then it will skip the first two and start the layout with its third
|
|
/// child.
|
|
///
|
|
/// ### Layout
|
|
///
|
|
/// As they are laid out, slivers decide their [geometry], which includes their
|
|
/// size ([SliverGeometry.paintExtent]) and the position of the next sliver
|
|
/// ([SliverGeometry.layoutExtent]), as well as the position of each of their
|
|
/// children, based on the input [constraints] from the viewport such as the
|
|
/// scroll offset ([SliverConstraints.scrollOffset]).
|
|
///
|
|
/// For example, a sliver that just paints a box 100 pixels high would say its
|
|
/// [SliverGeometry.paintExtent] was 100 pixels when the scroll offset was zero,
|
|
/// but would say its [SliverGeometry.paintExtent] was 25 pixels when the scroll
|
|
/// offset was 75 pixels, and would say it was zero when the scroll offset was
|
|
/// 100 pixels or more. (This is assuming that
|
|
/// [SliverConstraints.remainingPaintExtent] was more than 100 pixels.)
|
|
///
|
|
/// The various dimensions that are provided as input to this system are in the
|
|
/// [constraints]. They are described in detail in the documentation for the
|
|
/// [SliverConstraints] class.
|
|
///
|
|
/// The [performLayout] function must take these [constraints] and create a
|
|
/// [SliverGeometry] object that it must then assign to the [geometry] property.
|
|
/// The different dimensions of the geometry that can be configured are
|
|
/// described in detail in the documentation for the [SliverGeometry] class.
|
|
///
|
|
/// ### Painting
|
|
///
|
|
/// In addition to implementing layout, a sliver must also implement painting.
|
|
/// This is achieved by overriding the [paint] method.
|
|
///
|
|
/// The [paint] method is called with an [Offset] from the [Canvas] origin to
|
|
/// the top-left corner of the sliver, _regardless of the axis direction_.
|
|
///
|
|
/// Subclasses should also override [applyPaintTransform] to provide the
|
|
/// [Matrix4] describing the position of each child relative to the sliver.
|
|
/// (This is used by, among other things, the accessibility layer, to determine
|
|
/// the bounds of the child.)
|
|
///
|
|
/// ### Hit testing
|
|
///
|
|
/// To implement hit testing, either override the [hitTestSelf] and
|
|
/// [hitTestChildren] methods, or, for more complex cases, instead override the
|
|
/// [hitTest] method directly.
|
|
///
|
|
/// To actually react to pointer events, the [handleEvent] method may be
|
|
/// implemented. By default it does nothing. (Typically gestures are handled by
|
|
/// widgets in the box protocol, not by slivers directly.)
|
|
///
|
|
/// ### Helper methods
|
|
///
|
|
/// There are a number of methods that a sliver should implement which will make
|
|
/// the other methods easier to implement. Each method listed below has detailed
|
|
/// documentation. In addition, the [RenderSliverHelpers] class can be used to
|
|
/// mix in some helpful methods.
|
|
///
|
|
/// #### childScrollOffset
|
|
///
|
|
/// If the subclass positions children anywhere other than at scroll offset
|
|
/// zero, it should override [childScrollOffset]. For example,
|
|
/// [RenderSliverList] and [RenderSliverGrid] override this method, but
|
|
/// [RenderSliverToBoxAdapter] does not.
|
|
///
|
|
/// This is used by, among other things, [Scrollable.ensureVisible].
|
|
///
|
|
/// #### childMainAxisPosition
|
|
///
|
|
/// Subclasses should implement [childMainAxisPosition] to describe where their
|
|
/// children are positioned.
|
|
///
|
|
/// #### childCrossAxisPosition
|
|
///
|
|
/// If the subclass positions children in the cross-axis at a position other
|
|
/// than zero, then it should override [childCrossAxisPosition]. For example
|
|
/// [RenderSliverGrid] overrides this method.
|
|
abstract class RenderSliver extends RenderObject {
|
|
// layout input
|
|
@override
|
|
SliverConstraints get constraints => super.constraints as SliverConstraints;
|
|
|
|
/// The amount of space this sliver occupies.
|
|
///
|
|
/// This value is stale whenever this object is marked as needing layout.
|
|
/// During [performLayout], do not read the [geometry] of a child unless you
|
|
/// pass true for parentUsesSize when calling the child's [layout] function.
|
|
///
|
|
/// The geometry of a sliver should be set only during the sliver's
|
|
/// [performLayout] or [performResize] functions. If you wish to change the
|
|
/// geometry of a sliver outside of those functions, call [markNeedsLayout]
|
|
/// instead to schedule a layout of the sliver.
|
|
SliverGeometry? get geometry => _geometry;
|
|
SliverGeometry? _geometry;
|
|
set geometry(SliverGeometry? value) {
|
|
assert(!(debugDoingThisResize && debugDoingThisLayout));
|
|
assert(sizedByParent || !debugDoingThisResize);
|
|
assert(() {
|
|
if ((sizedByParent && debugDoingThisResize) ||
|
|
(!sizedByParent && debugDoingThisLayout)) {
|
|
return true;
|
|
}
|
|
assert(!debugDoingThisResize);
|
|
DiagnosticsNode? contract, violation, hint;
|
|
if (debugDoingThisLayout) {
|
|
assert(sizedByParent);
|
|
violation = ErrorDescription('It appears that the geometry setter was called from performLayout().');
|
|
} else {
|
|
violation = ErrorDescription('The geometry setter was called from outside layout (neither performResize() nor performLayout() were being run for this object).');
|
|
if (owner != null && owner!.debugDoingLayout) {
|
|
hint = ErrorDescription('Only the object itself can set its geometry. It is a contract violation for other objects to set it.');
|
|
}
|
|
}
|
|
if (sizedByParent) {
|
|
contract = ErrorDescription('Because this RenderSliver has sizedByParent set to true, it must set its geometry in performResize().');
|
|
} else {
|
|
contract = ErrorDescription('Because this RenderSliver has sizedByParent set to false, it must set its geometry in performLayout().');
|
|
}
|
|
|
|
final List<DiagnosticsNode> information = <DiagnosticsNode>[
|
|
ErrorSummary('RenderSliver geometry setter called incorrectly.'),
|
|
violation,
|
|
if (hint != null) hint,
|
|
contract,
|
|
describeForError('The RenderSliver in question is'),
|
|
];
|
|
throw FlutterError.fromParts(information);
|
|
}());
|
|
_geometry = value;
|
|
}
|
|
|
|
@override
|
|
Rect get semanticBounds => paintBounds;
|
|
|
|
@override
|
|
Rect get paintBounds {
|
|
switch (constraints.axis) {
|
|
case Axis.horizontal:
|
|
return Rect.fromLTWH(
|
|
0.0, 0.0,
|
|
geometry!.paintExtent,
|
|
constraints.crossAxisExtent,
|
|
);
|
|
case Axis.vertical:
|
|
return Rect.fromLTWH(
|
|
0.0, 0.0,
|
|
constraints.crossAxisExtent,
|
|
geometry!.paintExtent,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void debugResetSize() { }
|
|
|
|
@override
|
|
void debugAssertDoesMeetConstraints() {
|
|
assert(geometry!.debugAssertIsValid(
|
|
informationCollector: () => <DiagnosticsNode>[
|
|
describeForError('The RenderSliver that returned the offending geometry was'),
|
|
],
|
|
));
|
|
assert(() {
|
|
if (geometry!.paintOrigin + geometry!.paintExtent > constraints.remainingPaintExtent) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('SliverGeometry has a paintOffset that exceeds the remainingPaintExtent from the constraints.'),
|
|
describeForError('The render object whose geometry violates the constraints is the following'),
|
|
..._debugCompareFloats(
|
|
'remainingPaintExtent', constraints.remainingPaintExtent,
|
|
'paintOrigin + paintExtent', geometry!.paintOrigin + geometry!.paintExtent,
|
|
),
|
|
ErrorDescription(
|
|
'The paintOrigin and paintExtent must cause the child sliver to paint '
|
|
'within the viewport, and so cannot exceed the remainingPaintExtent.',
|
|
),
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
}
|
|
|
|
@override
|
|
void performResize() {
|
|
assert(false);
|
|
}
|
|
|
|
/// For a center sliver, the distance before the absolute zero scroll offset
|
|
/// that this sliver can cover.
|
|
///
|
|
/// For example, if an [AxisDirection.down] viewport with an
|
|
/// [RenderViewport.anchor] of 0.5 has a single sliver with a height of 100.0
|
|
/// and its [centerOffsetAdjustment] returns 50.0, then the sliver will be
|
|
/// centered in the viewport when the scroll offset is 0.0.
|
|
///
|
|
/// The distance here is in the opposite direction of the
|
|
/// [RenderViewport.axisDirection], so values will typically be positive.
|
|
double get centerOffsetAdjustment => 0.0;
|
|
|
|
/// Determines the set of render objects located at the given position.
|
|
///
|
|
/// Returns true if the given point is contained in this render object or one
|
|
/// of its descendants. Adds any render objects that contain the point to the
|
|
/// given hit test result.
|
|
///
|
|
/// The caller is responsible for providing the position in the local
|
|
/// coordinate space of the callee. The callee is responsible for checking
|
|
/// whether the given position is within its bounds.
|
|
///
|
|
/// Hit testing requires layout to be up-to-date but does not require painting
|
|
/// to be up-to-date. That means a render object can rely upon [performLayout]
|
|
/// having been called in [hitTest] but cannot rely upon [paint] having been
|
|
/// called. For example, a render object might be a child of a [RenderOpacity]
|
|
/// object, which calls [hitTest] on its children when its opacity is zero
|
|
/// even through it does not [paint] its children.
|
|
///
|
|
/// ## Coordinates for RenderSliver objects
|
|
///
|
|
/// The `mainAxisPosition` is the distance in the [AxisDirection] (after
|
|
/// applying the [GrowthDirection]) from the edge of the sliver's painted
|
|
/// area. This can be an unusual direction, for example in the
|
|
/// [AxisDirection.up] case this is a distance from the _bottom_ of the
|
|
/// sliver's painted area.
|
|
///
|
|
/// The `crossAxisPosition` is the distance in the other axis. If the cross
|
|
/// axis is horizontal (i.e. the [SliverConstraints.axisDirection] is either
|
|
/// [AxisDirection.down] or [AxisDirection.up]), then the `crossAxisPosition`
|
|
/// is a distance from the left edge of the sliver. If the cross axis is
|
|
/// vertical (i.e. the [SliverConstraints.axisDirection] is either
|
|
/// [AxisDirection.right] or [AxisDirection.left]), then the
|
|
/// `crossAxisPosition` is a distance from the top edge of the sliver.
|
|
///
|
|
/// ## Implementing hit testing for slivers
|
|
///
|
|
/// The most straight-forward way to implement hit testing for a new sliver
|
|
/// render object is to override its [hitTestSelf] and [hitTestChildren]
|
|
/// methods.
|
|
bool hitTest(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
|
|
if (mainAxisPosition >= 0.0 && mainAxisPosition < geometry!.hitTestExtent &&
|
|
crossAxisPosition >= 0.0 && crossAxisPosition < constraints.crossAxisExtent) {
|
|
if (hitTestChildren(result, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition) ||
|
|
hitTestSelf(mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition)) {
|
|
result.add(SliverHitTestEntry(
|
|
this,
|
|
mainAxisPosition: mainAxisPosition,
|
|
crossAxisPosition: crossAxisPosition,
|
|
));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Override this method if this render object can be hit even if its
|
|
/// children were not hit.
|
|
///
|
|
/// Used by [hitTest]. If you override [hitTest] and do not call this
|
|
/// function, then you don't need to implement this function.
|
|
///
|
|
/// For a discussion of the semantics of the arguments, see [hitTest].
|
|
@protected
|
|
bool hitTestSelf({ required double mainAxisPosition, required double crossAxisPosition }) => false;
|
|
|
|
/// Override this method to check whether any children are located at the
|
|
/// given position.
|
|
///
|
|
/// Typically children should be hit-tested in reverse paint order so that
|
|
/// hit tests at locations where children overlap hit the child that is
|
|
/// visually "on top" (i.e., paints later).
|
|
///
|
|
/// Used by [hitTest]. If you override [hitTest] and do not call this
|
|
/// function, then you don't need to implement this function.
|
|
///
|
|
/// For a discussion of the semantics of the arguments, see [hitTest].
|
|
@protected
|
|
bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) => false;
|
|
|
|
/// Computes the portion of the region from `from` to `to` that is visible,
|
|
/// assuming that only the region from the [SliverConstraints.scrollOffset]
|
|
/// that is [SliverConstraints.remainingPaintExtent] high is visible, and that
|
|
/// the relationship between scroll offsets and paint offsets is linear.
|
|
///
|
|
/// For example, if the constraints have a scroll offset of 100 and a
|
|
/// remaining paint extent of 100, and the arguments to this method describe
|
|
/// the region 50..150, then the returned value would be 50 (from scroll
|
|
/// offset 100 to scroll offset 150).
|
|
///
|
|
/// This method is not useful if there is not a 1:1 relationship between
|
|
/// consumed scroll offset and consumed paint extent. For example, if the
|
|
/// sliver always paints the same amount but consumes a scroll offset extent
|
|
/// that is proportional to the [SliverConstraints.scrollOffset], then this
|
|
/// function's results will not be consistent.
|
|
// This could be a static method but isn't, because it would be less convenient
|
|
// to call it from subclasses if it was.
|
|
double calculatePaintOffset(SliverConstraints constraints, { required double from, required double to }) {
|
|
assert(from <= to);
|
|
final double a = constraints.scrollOffset;
|
|
final double b = constraints.scrollOffset + constraints.remainingPaintExtent;
|
|
// the clamp on the next line is to avoid floating point rounding errors
|
|
return clampDouble(clampDouble(to, a, b) - clampDouble(from, a, b), 0.0, constraints.remainingPaintExtent);
|
|
}
|
|
|
|
/// Computes the portion of the region from `from` to `to` that is within
|
|
/// the cache extent of the viewport, assuming that only the region from the
|
|
/// [SliverConstraints.cacheOrigin] that is
|
|
/// [SliverConstraints.remainingCacheExtent] high is visible, and that
|
|
/// the relationship between scroll offsets and paint offsets is linear.
|
|
///
|
|
/// This method is not useful if there is not a 1:1 relationship between
|
|
/// consumed scroll offset and consumed cache extent.
|
|
double calculateCacheOffset(SliverConstraints constraints, { required double from, required double to }) {
|
|
assert(from <= to);
|
|
final double a = constraints.scrollOffset + constraints.cacheOrigin;
|
|
final double b = constraints.scrollOffset + constraints.remainingCacheExtent;
|
|
// the clamp on the next line is to avoid floating point rounding errors
|
|
return clampDouble(clampDouble(to, a, b) - clampDouble(from, a, b), 0.0, constraints.remainingCacheExtent);
|
|
}
|
|
|
|
/// Returns the distance from the leading _visible_ edge of the sliver to the
|
|
/// side of the given child closest to that edge.
|
|
///
|
|
/// 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. 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] and
|
|
/// [SliverLogicalParentData.layoutOffset].
|
|
///
|
|
/// For children that are [RenderSliver]s, the leading edge of the _child_
|
|
/// will be the leading _visible_ edge of the child, not the part of the child
|
|
/// that would locally be a scroll offset 0.0. For children that are not
|
|
/// [RenderSliver]s, for example a [RenderBox] child, it's the actual distance
|
|
/// to the edge of the box, since those boxes do not know how to handle being
|
|
/// scrolled.
|
|
///
|
|
/// This method differs from [childScrollOffset] in that
|
|
/// [childMainAxisPosition] gives the distance from the leading _visible_ edge
|
|
/// of the sliver whereas [childScrollOffset] gives the distance from the
|
|
/// sliver's zero scroll offset.
|
|
///
|
|
/// Calling this for a child that is not visible is not valid.
|
|
@protected
|
|
double childMainAxisPosition(covariant RenderObject child) {
|
|
assert(() {
|
|
throw FlutterError('${objectRuntimeType(this, 'RenderSliver')} does not implement childPosition.');
|
|
}());
|
|
return 0.0;
|
|
}
|
|
|
|
/// Returns the distance along the cross axis from the zero of the cross axis
|
|
/// in this sliver's [paint] coordinate space to the nearest side of the given
|
|
/// child.
|
|
///
|
|
/// For example, if the [constraints] describe this sliver as having an axis
|
|
/// direction of [AxisDirection.down], then this is the distance from the left
|
|
/// of the sliver to the left of the child. Similarly, if the [constraints]
|
|
/// describe this sliver as having an axis direction of [AxisDirection.up],
|
|
/// then this is value is the same. If the axis direction is
|
|
/// [AxisDirection.left] or [AxisDirection.right], then it is the distance
|
|
/// from the top of the sliver to the top of the child.
|
|
///
|
|
/// Calling this for a child that is not visible is not valid.
|
|
@protected
|
|
double childCrossAxisPosition(covariant RenderObject child) => 0.0;
|
|
|
|
/// Returns the scroll offset for the leading edge of the given child.
|
|
///
|
|
/// The `child` must be a child of this sliver.
|
|
///
|
|
/// This method differs from [childMainAxisPosition] in that
|
|
/// [childMainAxisPosition] gives the distance from the leading _visible_ edge
|
|
/// of the sliver whereas [childScrollOffset] gives the distance from sliver's
|
|
/// zero scroll offset.
|
|
double? childScrollOffset(covariant RenderObject child) {
|
|
assert(child.parent == this);
|
|
return 0.0;
|
|
}
|
|
|
|
@override
|
|
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
|
assert(() {
|
|
throw FlutterError('${objectRuntimeType(this, 'RenderSliver')} does not implement applyPaintTransform.');
|
|
}());
|
|
}
|
|
|
|
/// This returns a [Size] with dimensions relative to the leading edge of the
|
|
/// sliver, specifically the same offset that is given to the [paint] method.
|
|
/// This means that the dimensions may be negative.
|
|
///
|
|
/// This is only valid after [layout] has completed.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [getAbsoluteSize], which returns absolute size.
|
|
@protected
|
|
Size getAbsoluteSizeRelativeToOrigin() {
|
|
assert(geometry != null);
|
|
assert(!debugNeedsLayout);
|
|
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
|
|
case AxisDirection.up:
|
|
return Size(constraints.crossAxisExtent, -geometry!.paintExtent);
|
|
case AxisDirection.right:
|
|
return Size(geometry!.paintExtent, constraints.crossAxisExtent);
|
|
case AxisDirection.down:
|
|
return Size(constraints.crossAxisExtent, geometry!.paintExtent);
|
|
case AxisDirection.left:
|
|
return Size(-geometry!.paintExtent, constraints.crossAxisExtent);
|
|
}
|
|
}
|
|
|
|
/// This returns the absolute [Size] of the sliver.
|
|
///
|
|
/// The dimensions are always positive and calling this is only valid after
|
|
/// [layout] has completed.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [getAbsoluteSizeRelativeToOrigin], which returns the size relative to
|
|
/// the leading edge of the sliver.
|
|
@protected
|
|
Size getAbsoluteSize() {
|
|
assert(geometry != null);
|
|
assert(!debugNeedsLayout);
|
|
switch (constraints.axisDirection) {
|
|
case AxisDirection.up:
|
|
case AxisDirection.down:
|
|
return Size(constraints.crossAxisExtent, geometry!.paintExtent);
|
|
case AxisDirection.right:
|
|
case AxisDirection.left:
|
|
return Size(geometry!.paintExtent, constraints.crossAxisExtent);
|
|
}
|
|
}
|
|
|
|
void _debugDrawArrow(Canvas canvas, Paint paint, Offset p0, Offset p1, GrowthDirection direction) {
|
|
assert(() {
|
|
if (p0 == p1) {
|
|
return true;
|
|
}
|
|
assert(p0.dx == p1.dx || p0.dy == p1.dy); // must be axis-aligned
|
|
final double d = (p1 - p0).distance * 0.2;
|
|
final Offset temp;
|
|
double dx1, dx2, dy1, dy2;
|
|
switch (direction) {
|
|
case GrowthDirection.forward:
|
|
dx1 = dx2 = dy1 = dy2 = d;
|
|
case GrowthDirection.reverse:
|
|
temp = p0;
|
|
p0 = p1;
|
|
p1 = temp;
|
|
dx1 = dx2 = dy1 = dy2 = -d;
|
|
}
|
|
if (p0.dx == p1.dx) {
|
|
dx2 = -dx2;
|
|
} else {
|
|
dy2 = -dy2;
|
|
}
|
|
canvas.drawPath(
|
|
Path()
|
|
..moveTo(p0.dx, p0.dy)
|
|
..lineTo(p1.dx, p1.dy)
|
|
..moveTo(p1.dx - dx1, p1.dy - dy1)
|
|
..lineTo(p1.dx, p1.dy)
|
|
..lineTo(p1.dx - dx2, p1.dy - dy2),
|
|
paint,
|
|
);
|
|
return true;
|
|
}());
|
|
}
|
|
|
|
@override
|
|
void debugPaint(PaintingContext context, Offset offset) {
|
|
assert(() {
|
|
if (debugPaintSizeEnabled) {
|
|
final double strokeWidth = math.min(4.0, geometry!.paintExtent / 30.0);
|
|
final Paint paint = Paint()
|
|
..color = const Color(0xFF33CC33)
|
|
..strokeWidth = strokeWidth
|
|
..style = PaintingStyle.stroke
|
|
..maskFilter = MaskFilter.blur(BlurStyle.solid, strokeWidth);
|
|
final double arrowExtent = geometry!.paintExtent;
|
|
final double padding = math.max(2.0, strokeWidth);
|
|
final Canvas canvas = context.canvas;
|
|
canvas.drawCircle(
|
|
offset.translate(padding, padding),
|
|
padding * 0.5,
|
|
paint,
|
|
);
|
|
switch (constraints.axis) {
|
|
case Axis.vertical:
|
|
canvas.drawLine(
|
|
offset,
|
|
offset.translate(constraints.crossAxisExtent, 0.0),
|
|
paint,
|
|
);
|
|
_debugDrawArrow(
|
|
canvas,
|
|
paint,
|
|
offset.translate(constraints.crossAxisExtent * 1.0 / 4.0, padding),
|
|
offset.translate(constraints.crossAxisExtent * 1.0 / 4.0, arrowExtent - padding),
|
|
constraints.normalizedGrowthDirection,
|
|
);
|
|
_debugDrawArrow(
|
|
canvas,
|
|
paint,
|
|
offset.translate(constraints.crossAxisExtent * 3.0 / 4.0, padding),
|
|
offset.translate(constraints.crossAxisExtent * 3.0 / 4.0, arrowExtent - padding),
|
|
constraints.normalizedGrowthDirection,
|
|
);
|
|
case Axis.horizontal:
|
|
canvas.drawLine(
|
|
offset,
|
|
offset.translate(0.0, constraints.crossAxisExtent),
|
|
paint,
|
|
);
|
|
_debugDrawArrow(
|
|
canvas,
|
|
paint,
|
|
offset.translate(padding, constraints.crossAxisExtent * 1.0 / 4.0),
|
|
offset.translate(arrowExtent - padding, constraints.crossAxisExtent * 1.0 / 4.0),
|
|
constraints.normalizedGrowthDirection,
|
|
);
|
|
_debugDrawArrow(
|
|
canvas,
|
|
paint,
|
|
offset.translate(padding, constraints.crossAxisExtent * 3.0 / 4.0),
|
|
offset.translate(arrowExtent - padding, constraints.crossAxisExtent * 3.0 / 4.0),
|
|
constraints.normalizedGrowthDirection,
|
|
);
|
|
}
|
|
}
|
|
return true;
|
|
}());
|
|
}
|
|
|
|
// This override exists only to change the type of the second argument.
|
|
@override
|
|
void handleEvent(PointerEvent event, SliverHitTestEntry entry) { }
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(DiagnosticsProperty<SliverGeometry>('geometry', geometry));
|
|
}
|
|
}
|
|
|
|
/// Mixin for [RenderSliver] subclasses that provides some utility functions.
|
|
mixin RenderSliverHelpers implements RenderSliver {
|
|
bool _getRightWayUp(SliverConstraints constraints) {
|
|
bool rightWayUp;
|
|
switch (constraints.axisDirection) {
|
|
case AxisDirection.up:
|
|
case AxisDirection.left:
|
|
rightWayUp = false;
|
|
case AxisDirection.down:
|
|
case AxisDirection.right:
|
|
rightWayUp = true;
|
|
}
|
|
switch (constraints.growthDirection) {
|
|
case GrowthDirection.forward:
|
|
break;
|
|
case GrowthDirection.reverse:
|
|
rightWayUp = !rightWayUp;
|
|
}
|
|
return rightWayUp;
|
|
}
|
|
|
|
/// Utility function for [hitTestChildren] for use when the children are
|
|
/// [RenderBox] widgets.
|
|
///
|
|
/// This function takes care of converting the position from the sliver
|
|
/// coordinate system to the Cartesian coordinate system used by [RenderBox].
|
|
///
|
|
/// This function relies on [childMainAxisPosition] to determine the position of
|
|
/// child in question.
|
|
///
|
|
/// Calling this for a child that is not visible is not valid.
|
|
@protected
|
|
bool hitTestBoxChild(BoxHitTestResult result, RenderBox child, { required double mainAxisPosition, required double crossAxisPosition }) {
|
|
final bool rightWayUp = _getRightWayUp(constraints);
|
|
double delta = childMainAxisPosition(child);
|
|
final double crossAxisDelta = childCrossAxisPosition(child);
|
|
double absolutePosition = mainAxisPosition - delta;
|
|
final double absoluteCrossAxisPosition = crossAxisPosition - crossAxisDelta;
|
|
Offset paintOffset, transformedPosition;
|
|
switch (constraints.axis) {
|
|
case Axis.horizontal:
|
|
if (!rightWayUp) {
|
|
absolutePosition = child.size.width - absolutePosition;
|
|
delta = geometry!.paintExtent - child.size.width - delta;
|
|
}
|
|
paintOffset = Offset(delta, crossAxisDelta);
|
|
transformedPosition = Offset(absolutePosition, absoluteCrossAxisPosition);
|
|
case Axis.vertical:
|
|
if (!rightWayUp) {
|
|
absolutePosition = child.size.height - absolutePosition;
|
|
delta = geometry!.paintExtent - child.size.height - delta;
|
|
}
|
|
paintOffset = Offset(crossAxisDelta, delta);
|
|
transformedPosition = Offset(absoluteCrossAxisPosition, absolutePosition);
|
|
}
|
|
return result.addWithOutOfBandPosition(
|
|
paintOffset: paintOffset,
|
|
hitTest: (BoxHitTestResult result) {
|
|
return child.hitTest(result, position: transformedPosition);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Utility function for [applyPaintTransform] for use when the children are
|
|
/// [RenderBox] widgets.
|
|
///
|
|
/// This function turns the value returned by [childMainAxisPosition] and
|
|
/// [childCrossAxisPosition]for the child in question into a translation that
|
|
/// it then applies to the given matrix.
|
|
///
|
|
/// Calling this for a child that is not visible is not valid.
|
|
@protected
|
|
void applyPaintTransformForBoxChild(RenderBox child, Matrix4 transform) {
|
|
final bool rightWayUp = _getRightWayUp(constraints);
|
|
double delta = childMainAxisPosition(child);
|
|
final double crossAxisDelta = childCrossAxisPosition(child);
|
|
switch (constraints.axis) {
|
|
case Axis.horizontal:
|
|
if (!rightWayUp) {
|
|
delta = geometry!.paintExtent - child.size.width - delta;
|
|
}
|
|
transform.translate(delta, crossAxisDelta);
|
|
case Axis.vertical:
|
|
if (!rightWayUp) {
|
|
delta = geometry!.paintExtent - child.size.height - delta;
|
|
}
|
|
transform.translate(crossAxisDelta, delta);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ADAPTER FOR RENDER BOXES INSIDE SLIVERS
|
|
// Transitions from the RenderSliver world to the RenderBox world.
|
|
|
|
/// An abstract class for [RenderSliver]s that contains a single [RenderBox].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderSliver], which explains more about the Sliver protocol.
|
|
/// * [RenderBox], which explains more about the Box protocol.
|
|
/// * [RenderSliverToBoxAdapter], which extends this class to size the child
|
|
/// according to its preferred size.
|
|
/// * [RenderSliverFillRemaining], which extends this class to size the child
|
|
/// to fill the remaining space in the viewport.
|
|
abstract class RenderSliverSingleBoxAdapter extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
|
|
/// Creates a [RenderSliver] that wraps a [RenderBox].
|
|
RenderSliverSingleBoxAdapter({
|
|
RenderBox? child,
|
|
}) {
|
|
this.child = child;
|
|
}
|
|
|
|
@override
|
|
void setupParentData(RenderObject child) {
|
|
if (child.parentData is! SliverPhysicalParentData) {
|
|
child.parentData = SliverPhysicalParentData();
|
|
}
|
|
}
|
|
|
|
/// Sets the [SliverPhysicalParentData.paintOffset] for the given child
|
|
/// according to the [SliverConstraints.axisDirection] and
|
|
/// [SliverConstraints.growthDirection] and the given geometry.
|
|
@protected
|
|
void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) {
|
|
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
|
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
|
|
case AxisDirection.up:
|
|
childParentData.paintOffset = Offset(0.0, -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)));
|
|
case AxisDirection.right:
|
|
childParentData.paintOffset = Offset(-constraints.scrollOffset, 0.0);
|
|
case AxisDirection.down:
|
|
childParentData.paintOffset = Offset(0.0, -constraints.scrollOffset);
|
|
case AxisDirection.left:
|
|
childParentData.paintOffset = Offset(-(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)), 0.0);
|
|
}
|
|
}
|
|
|
|
@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
|
|
double childMainAxisPosition(RenderBox child) {
|
|
return -constraints.scrollOffset;
|
|
}
|
|
|
|
@override
|
|
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
|
assert(child == this.child);
|
|
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
|
childParentData.applyPaintTransform(transform);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (child != null && geometry!.visible) {
|
|
final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData;
|
|
context.paintChild(child!, offset + childParentData.paintOffset);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A [RenderSliver] that contains a single [RenderBox].
|
|
///
|
|
/// The child will not be laid out if it is not visible. It is sized according
|
|
/// to the child's preferences in the main axis, and with a tight constraint
|
|
/// forcing it to the dimensions of the viewport in the cross axis.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderSliver], which explains more about the Sliver protocol.
|
|
/// * [RenderBox], which explains more about the Box protocol.
|
|
/// * [RenderViewport], which allows [RenderSliver] objects to be placed inside
|
|
/// a [RenderBox] (the opposite of this class).
|
|
class RenderSliverToBoxAdapter extends RenderSliverSingleBoxAdapter {
|
|
/// Creates a [RenderSliver] that wraps a [RenderBox].
|
|
RenderSliverToBoxAdapter({
|
|
super.child,
|
|
});
|
|
|
|
@override
|
|
void performLayout() {
|
|
if (child == null) {
|
|
geometry = SliverGeometry.zero;
|
|
return;
|
|
}
|
|
final SliverConstraints constraints = this.constraints;
|
|
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
|
|
final double childExtent;
|
|
switch (constraints.axis) {
|
|
case Axis.horizontal:
|
|
childExtent = child!.size.width;
|
|
case Axis.vertical:
|
|
childExtent = child!.size.height;
|
|
}
|
|
final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: childExtent);
|
|
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
|
|
|
|
assert(paintedChildSize.isFinite);
|
|
assert(paintedChildSize >= 0.0);
|
|
geometry = SliverGeometry(
|
|
scrollExtent: childExtent,
|
|
paintExtent: paintedChildSize,
|
|
cacheExtent: cacheExtent,
|
|
maxPaintExtent: childExtent,
|
|
hitTestExtent: paintedChildSize,
|
|
hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
|
|
);
|
|
setChildParentData(child!, constraints, geometry!);
|
|
}
|
|
}
|