mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
1898 lines
72 KiB
Dart
1898 lines
72 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/animation.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/semantics.dart';
|
|
import 'package:vector_math/vector_math_64.dart';
|
|
|
|
import 'box.dart';
|
|
import 'object.dart';
|
|
import 'sliver.dart';
|
|
import 'viewport_offset.dart';
|
|
|
|
/// The unit of measurement for a [Viewport.cacheExtent].
|
|
enum CacheExtentStyle {
|
|
/// Treat the [Viewport.cacheExtent] as logical pixels.
|
|
pixel,
|
|
/// Treat the [Viewport.cacheExtent] as a multiplier of the main axis extent.
|
|
viewport,
|
|
}
|
|
|
|
/// An interface for render objects that are bigger on the inside.
|
|
///
|
|
/// Some render objects, such as [RenderViewport], present a portion of their
|
|
/// content, which can be controlled by a [ViewportOffset]. This interface lets
|
|
/// the framework recognize such render objects and interact with them without
|
|
/// having specific knowledge of all the various types of viewports.
|
|
abstract class RenderAbstractViewport extends RenderObject {
|
|
// This class is intended to be used as an interface, and should not be
|
|
// extended directly; this constructor prevents instantiation and extension.
|
|
// ignore: unused_element
|
|
factory RenderAbstractViewport._() => null;
|
|
|
|
/// Returns the [RenderAbstractViewport] that most tightly encloses the given
|
|
/// render object.
|
|
///
|
|
/// If the object does not have a [RenderAbstractViewport] as an ancestor,
|
|
/// this function returns null.
|
|
static RenderAbstractViewport of(RenderObject object) {
|
|
while (object != null) {
|
|
if (object is RenderAbstractViewport)
|
|
return object;
|
|
object = object.parent as RenderObject;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Returns the offset that would be needed to reveal the `target`
|
|
/// [RenderObject].
|
|
///
|
|
/// The optional `rect` parameter describes which area of that `target` object
|
|
/// should be revealed in the viewport. If `rect` is null, the entire
|
|
/// `target` [RenderObject] (as defined by its [RenderObject.paintBounds])
|
|
/// will be revealed. If `rect` is provided it has to be given in the
|
|
/// coordinate system of the `target` object.
|
|
///
|
|
/// The `alignment` argument describes where the target should be positioned
|
|
/// after applying the returned offset. If `alignment` is 0.0, the child must
|
|
/// be positioned as close to the leading edge of the viewport as possible. If
|
|
/// `alignment` is 1.0, the child must be positioned as close to the trailing
|
|
/// edge of the viewport as possible. If `alignment` is 0.5, the child must be
|
|
/// positioned as close to the center of the viewport as possible.
|
|
///
|
|
/// The `target` might not be a direct child of this viewport but it must be a
|
|
/// descendant of the viewport. Other viewports in between this viewport and
|
|
/// the `target` will not be adjusted.
|
|
///
|
|
/// This method assumes that the content of the viewport moves linearly, i.e.
|
|
/// when the offset of the viewport is changed by x then `target` also moves
|
|
/// by x within the viewport.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RevealedOffset], which describes the return value of this method.
|
|
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect rect });
|
|
|
|
/// The default value for the cache extent of the viewport.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderViewportBase.cacheExtent] for a definition of the cache extent.
|
|
@protected
|
|
@visibleForTesting
|
|
static const double defaultCacheExtent = 250.0;
|
|
}
|
|
|
|
/// Return value for [RenderAbstractViewport.getOffsetToReveal].
|
|
///
|
|
/// It indicates the [offset] required to reveal an element in a viewport and
|
|
/// the [rect] position said element would have in the viewport at that
|
|
/// [offset].
|
|
class RevealedOffset {
|
|
|
|
/// Instantiates a return value for [RenderAbstractViewport.getOffsetToReveal].
|
|
const RevealedOffset({
|
|
@required this.offset,
|
|
@required this.rect,
|
|
}) : assert(offset != null),
|
|
assert(rect != null);
|
|
|
|
/// Offset for the viewport to reveal a specific element in the viewport.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderAbstractViewport.getOffsetToReveal], which calculates this
|
|
/// value for a specific element.
|
|
final double offset;
|
|
|
|
/// The [Rect] in the outer coordinate system of the viewport at which the
|
|
/// to-be-revealed element would be located if the viewport's offset is set
|
|
/// to [offset].
|
|
///
|
|
/// A viewport usually has two coordinate systems and works as an adapter
|
|
/// between the two:
|
|
///
|
|
/// The inner coordinate system has its origin at the top left corner of the
|
|
/// content that moves inside the viewport. The origin of this coordinate
|
|
/// system usually moves around relative to the leading edge of the viewport
|
|
/// when the viewport offset changes.
|
|
///
|
|
/// The outer coordinate system has its origin at the top left corner of the
|
|
/// visible part of the viewport. This origin stays at the same position
|
|
/// regardless of the current viewport offset.
|
|
///
|
|
/// In other words: [rect] describes where the revealed element would be
|
|
/// located relative to the top left corner of the visible part of the
|
|
/// viewport if the viewport's offset is set to [offset].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderAbstractViewport.getOffsetToReveal], which calculates this
|
|
/// value for a specific element.
|
|
final Rect rect;
|
|
|
|
@override
|
|
String toString() {
|
|
return '${objectRuntimeType(this, 'RevealedOffset')}(offset: $offset, rect: $rect)';
|
|
}
|
|
}
|
|
|
|
/// A base class for render objects that are bigger on the inside.
|
|
///
|
|
/// This render object provides the shared code for render objects that host
|
|
/// [RenderSliver] render objects inside a [RenderBox]. The viewport establishes
|
|
/// an [axisDirection], which orients the sliver's coordinate system, which is
|
|
/// based on scroll offsets rather than Cartesian coordinates.
|
|
///
|
|
/// The viewport also listens to an [offset], which determines the
|
|
/// [SliverConstraints.scrollOffset] input to the sliver layout protocol.
|
|
///
|
|
/// Subclasses typically override [performLayout] and call
|
|
/// [layoutChildSequence], perhaps multiple times.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderSliver], which explains more about the Sliver protocol.
|
|
/// * [RenderBox], which explains more about the Box protocol.
|
|
/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
|
|
/// placed inside a [RenderSliver] (the opposite of this class).
|
|
abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMixin<RenderSliver>>
|
|
extends RenderBox with ContainerRenderObjectMixin<RenderSliver, ParentDataClass>
|
|
implements RenderAbstractViewport {
|
|
/// Initializes fields for subclasses.
|
|
RenderViewportBase({
|
|
AxisDirection axisDirection = AxisDirection.down,
|
|
@required AxisDirection crossAxisDirection,
|
|
@required ViewportOffset offset,
|
|
double cacheExtent,
|
|
CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
|
|
}) : assert(axisDirection != null),
|
|
assert(crossAxisDirection != null),
|
|
assert(offset != null),
|
|
assert(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)),
|
|
assert(cacheExtentStyle != null),
|
|
assert(cacheExtent != null || cacheExtentStyle == CacheExtentStyle.pixel),
|
|
_axisDirection = axisDirection,
|
|
_crossAxisDirection = crossAxisDirection,
|
|
_offset = offset,
|
|
_cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent,
|
|
_cacheExtentStyle = cacheExtentStyle;
|
|
|
|
@override
|
|
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
|
super.describeSemanticsConfiguration(config);
|
|
|
|
config.addTagForChildren(RenderViewport.useTwoPaneSemantics);
|
|
}
|
|
|
|
@override
|
|
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
|
childrenInPaintOrder
|
|
.where((RenderSliver sliver) => sliver.geometry.visible || sliver.geometry.cacheExtent > 0.0)
|
|
.forEach(visitor);
|
|
}
|
|
|
|
/// The direction in which the [SliverConstraints.scrollOffset] increases.
|
|
///
|
|
/// For example, if the [axisDirection] is [AxisDirection.down], a scroll
|
|
/// offset of zero is at the top of the viewport and increases towards the
|
|
/// bottom of the viewport.
|
|
AxisDirection get axisDirection => _axisDirection;
|
|
AxisDirection _axisDirection;
|
|
set axisDirection(AxisDirection value) {
|
|
assert(value != null);
|
|
if (value == _axisDirection)
|
|
return;
|
|
_axisDirection = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// The direction in which child should be laid out in the cross axis.
|
|
///
|
|
/// For example, if the [axisDirection] is [AxisDirection.down], this property
|
|
/// is typically [AxisDirection.left] if the ambient [TextDirection] is
|
|
/// [TextDirection.rtl] and [AxisDirection.right] if the ambient
|
|
/// [TextDirection] is [TextDirection.ltr].
|
|
AxisDirection get crossAxisDirection => _crossAxisDirection;
|
|
AxisDirection _crossAxisDirection;
|
|
set crossAxisDirection(AxisDirection value) {
|
|
assert(value != null);
|
|
if (value == _crossAxisDirection)
|
|
return;
|
|
_crossAxisDirection = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// The axis along which the viewport scrolls.
|
|
///
|
|
/// For example, if the [axisDirection] is [AxisDirection.down], then the
|
|
/// [axis] is [Axis.vertical] and the viewport scrolls vertically.
|
|
Axis get axis => axisDirectionToAxis(axisDirection);
|
|
|
|
/// Which part of the content inside the viewport should be visible.
|
|
///
|
|
/// The [ViewportOffset.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.
|
|
ViewportOffset get offset => _offset;
|
|
ViewportOffset _offset;
|
|
set offset(ViewportOffset value) {
|
|
assert(value != null);
|
|
if (value == _offset)
|
|
return;
|
|
if (attached)
|
|
_offset.removeListener(markNeedsLayout);
|
|
_offset = value;
|
|
if (attached)
|
|
_offset.addListener(markNeedsLayout);
|
|
// We need to go through layout even if the new offset has the same pixels
|
|
// value as the old offset so that we will apply our viewport and content
|
|
// dimensions.
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// {@template flutter.rendering.viewport.cacheExtent}
|
|
/// The viewport has an area before and after the visible area to cache items
|
|
/// that are about to become visible when the user scrolls.
|
|
///
|
|
/// Items that fall in this cache area are laid out even though they are not
|
|
/// (yet) visible on screen. The [cacheExtent] describes how many pixels
|
|
/// the cache area extends before the leading edge and after the trailing edge
|
|
/// of the viewport.
|
|
///
|
|
/// The total extent, which the viewport will try to cover with children, is
|
|
/// [cacheExtent] before the leading edge + extent of the main axis +
|
|
/// [cacheExtent] after the trailing edge.
|
|
///
|
|
/// The cache area is also used to implement implicit accessibility scrolling
|
|
/// on iOS: When the accessibility focus moves from an item in the visible
|
|
/// viewport to an invisible item in the cache area, the framework will bring
|
|
/// that item into view with an (implicit) scroll action.
|
|
/// {@endtemplate}
|
|
double get cacheExtent => _cacheExtent;
|
|
double _cacheExtent;
|
|
set cacheExtent(double value) {
|
|
value = value ?? RenderAbstractViewport.defaultCacheExtent;
|
|
assert(value != null);
|
|
if (value == _cacheExtent)
|
|
return;
|
|
_cacheExtent = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// This value is set during layout based on the [CacheExtentStyle].
|
|
///
|
|
/// When the style is [CacheExtentStyle.viewport], it is the main axis extent
|
|
/// of the viewport multiplied by the requested cache extent, which is still
|
|
/// expressed in pixels.
|
|
double _calculatedCacheExtent;
|
|
|
|
/// {@template flutter.rendering.viewport.cacheExtentStyle}
|
|
/// Controls how the [cacheExtent] is interpreted.
|
|
///
|
|
/// If set to [CacheExtentStyle.pixels], the [cacheExtent] will be treated as
|
|
/// a logical pixels.
|
|
///
|
|
/// If set to [CacheExtentStyle.viewport], the [cacheExtent] will be treated
|
|
/// as a multiplier for the main axis extent of the viewport. In this case,
|
|
/// the [cacheExtent] must not be null.
|
|
/// {@endtemplate}
|
|
CacheExtentStyle get cacheExtentStyle => _cacheExtentStyle;
|
|
CacheExtentStyle _cacheExtentStyle;
|
|
set cacheExtentStyle(CacheExtentStyle value) {
|
|
assert(value != null);
|
|
if (value == _cacheExtentStyle) {
|
|
return;
|
|
}
|
|
_cacheExtentStyle = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
@override
|
|
void attach(PipelineOwner owner) {
|
|
super.attach(owner);
|
|
_offset.addListener(markNeedsLayout);
|
|
}
|
|
|
|
@override
|
|
void detach() {
|
|
_offset.removeListener(markNeedsLayout);
|
|
super.detach();
|
|
}
|
|
|
|
/// Throws an exception saying that the object does not support returning
|
|
/// intrinsic dimensions if, in checked mode, we are not in the
|
|
/// [RenderObject.debugCheckingIntrinsics] mode.
|
|
///
|
|
/// This is used by [computeMinIntrinsicWidth] et al because viewports do not
|
|
/// generally support returning intrinsic dimensions. See the discussion at
|
|
/// [computeMinIntrinsicWidth].
|
|
@protected
|
|
bool debugThrowIfNotCheckingIntrinsics() {
|
|
assert(() {
|
|
if (!RenderObject.debugCheckingIntrinsics) {
|
|
assert(this is! RenderShrinkWrappingViewport); // it has its own message
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('$runtimeType does not support returning intrinsic dimensions.'),
|
|
ErrorDescription(
|
|
'Calculating the intrinsic dimensions would require instantiating every child of '
|
|
'the viewport, which defeats the point of viewports being lazy.',
|
|
),
|
|
ErrorHint(
|
|
'If you are merely trying to shrink-wrap the viewport in the main axis direction, '
|
|
'consider a RenderShrinkWrappingViewport render object (ShrinkWrappingViewport widget), '
|
|
'which achieves that effect without implementing the intrinsic dimension API.'
|
|
),
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
double computeMinIntrinsicWidth(double height) {
|
|
assert(debugThrowIfNotCheckingIntrinsics());
|
|
return 0.0;
|
|
}
|
|
|
|
@override
|
|
double computeMaxIntrinsicWidth(double height) {
|
|
assert(debugThrowIfNotCheckingIntrinsics());
|
|
return 0.0;
|
|
}
|
|
|
|
@override
|
|
double computeMinIntrinsicHeight(double width) {
|
|
assert(debugThrowIfNotCheckingIntrinsics());
|
|
return 0.0;
|
|
}
|
|
|
|
@override
|
|
double computeMaxIntrinsicHeight(double width) {
|
|
assert(debugThrowIfNotCheckingIntrinsics());
|
|
return 0.0;
|
|
}
|
|
|
|
@override
|
|
bool get isRepaintBoundary => true;
|
|
|
|
/// Determines the size and position of some of the children of the viewport.
|
|
///
|
|
/// This function is the workhorse of `performLayout` implementations in
|
|
/// subclasses.
|
|
///
|
|
/// Layout starts with `child`, proceeds according to the `advance` callback,
|
|
/// and stops once `advance` returns null.
|
|
///
|
|
/// * `scrollOffset` is the [SliverConstraints.scrollOffset] to pass the
|
|
/// first child. The scroll offset is adjusted by
|
|
/// [SliverGeometry.scrollExtent] for subsequent children.
|
|
/// * `overlap` is the [SliverConstraints.overlap] to pass the first child.
|
|
/// The overlay is adjusted by the [SliverGeometry.paintOrigin] and
|
|
/// [SliverGeometry.paintExtent] for subsequent children.
|
|
/// * `layoutOffset` is the layout offset at which to place the first child.
|
|
/// The layout offset is updated by the [SliverGeometry.layoutExtent] for
|
|
/// subsequent children.
|
|
/// * `remainingPaintExtent` is [SliverConstraints.remainingPaintExtent] to
|
|
/// pass the first child. The remaining paint extent is updated by the
|
|
/// [SliverGeometry.layoutExtent] for subsequent children.
|
|
/// * `mainAxisExtent` is the [SliverConstraints.viewportMainAxisExtent] to
|
|
/// pass to each child.
|
|
/// * `crossAxisExtent` is the [SliverConstraints.crossAxisExtent] to pass to
|
|
/// each child.
|
|
/// * `growthDirection` is the [SliverConstraints.growthDirection] to pass to
|
|
/// each child.
|
|
///
|
|
/// Returns the first non-zero [SliverGeometry.scrollOffsetCorrection]
|
|
/// encountered, if any. Otherwise returns 0.0. Typical callers will call this
|
|
/// function repeatedly until it returns 0.0.
|
|
@protected
|
|
double layoutChildSequence({
|
|
@required RenderSliver child,
|
|
@required double scrollOffset,
|
|
@required double overlap,
|
|
@required double layoutOffset,
|
|
@required double remainingPaintExtent,
|
|
@required double mainAxisExtent,
|
|
@required double crossAxisExtent,
|
|
@required GrowthDirection growthDirection,
|
|
@required RenderSliver advance(RenderSliver child),
|
|
@required double remainingCacheExtent,
|
|
@required double cacheOrigin,
|
|
}) {
|
|
assert(scrollOffset.isFinite);
|
|
assert(scrollOffset >= 0.0);
|
|
final double initialLayoutOffset = layoutOffset;
|
|
final ScrollDirection adjustedUserScrollDirection =
|
|
applyGrowthDirectionToScrollDirection(offset.userScrollDirection, growthDirection);
|
|
assert(adjustedUserScrollDirection != null);
|
|
double maxPaintOffset = layoutOffset + overlap;
|
|
double precedingScrollExtent = 0.0;
|
|
|
|
while (child != null) {
|
|
final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;
|
|
// If the scrollOffset is too small we adjust the paddedOrigin because it
|
|
// doesn't make sense to ask a sliver for content before its scroll
|
|
// offset.
|
|
final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);
|
|
final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;
|
|
|
|
assert(sliverScrollOffset >= correctedCacheOrigin.abs());
|
|
assert(correctedCacheOrigin <= 0.0);
|
|
assert(sliverScrollOffset >= 0.0);
|
|
assert(cacheExtentCorrection <= 0.0);
|
|
|
|
child.layout(SliverConstraints(
|
|
axisDirection: axisDirection,
|
|
growthDirection: growthDirection,
|
|
userScrollDirection: adjustedUserScrollDirection,
|
|
scrollOffset: sliverScrollOffset,
|
|
precedingScrollExtent: precedingScrollExtent,
|
|
overlap: maxPaintOffset - layoutOffset,
|
|
remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
|
|
crossAxisExtent: crossAxisExtent,
|
|
crossAxisDirection: crossAxisDirection,
|
|
viewportMainAxisExtent: mainAxisExtent,
|
|
remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
|
|
cacheOrigin: correctedCacheOrigin,
|
|
), parentUsesSize: true);
|
|
|
|
final SliverGeometry childLayoutGeometry = child.geometry;
|
|
assert(childLayoutGeometry.debugAssertIsValid());
|
|
|
|
// If there is a correction to apply, we'll have to start over.
|
|
if (childLayoutGeometry.scrollOffsetCorrection != null)
|
|
return childLayoutGeometry.scrollOffsetCorrection;
|
|
|
|
// We use the child's paint origin in our coordinate system as the
|
|
// layoutOffset we store in the child's parent data.
|
|
final double effectiveLayoutOffset = layoutOffset + childLayoutGeometry.paintOrigin;
|
|
|
|
// `effectiveLayoutOffset` becomes meaningless once we moved past the trailing edge
|
|
// because `childLayoutGeometry.layoutExtent` is zero. Using the still increasing
|
|
// 'scrollOffset` to roughly position these invisible slivers in the right order.
|
|
if (childLayoutGeometry.visible || scrollOffset > 0) {
|
|
updateChildLayoutOffset(child, effectiveLayoutOffset, growthDirection);
|
|
} else {
|
|
updateChildLayoutOffset(child, -scrollOffset + initialLayoutOffset, growthDirection);
|
|
}
|
|
|
|
maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
|
|
scrollOffset -= childLayoutGeometry.scrollExtent;
|
|
precedingScrollExtent += childLayoutGeometry.scrollExtent;
|
|
layoutOffset += childLayoutGeometry.layoutExtent;
|
|
if (childLayoutGeometry.cacheExtent != 0.0) {
|
|
remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
|
|
cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
|
|
}
|
|
|
|
updateOutOfBandData(growthDirection, childLayoutGeometry);
|
|
|
|
// move on to the next child
|
|
child = advance(child);
|
|
}
|
|
|
|
// we made it without a correction, whee!
|
|
return 0.0;
|
|
}
|
|
|
|
@override
|
|
Rect describeApproximatePaintClip(RenderSliver child) {
|
|
final Rect viewportClip = Offset.zero & size;
|
|
// The child's viewportMainAxisExtent can be infinite when a
|
|
// RenderShrinkWrappingViewport is given infinite constraints, such as when
|
|
// it is the child of a Row or Column (depending on orientation).
|
|
//
|
|
// For example, a shrink wrapping render sliver may have infinite
|
|
// constraints along the viewport's main axis but may also have bouncing
|
|
// scroll physics, which will allow for some scrolling effect to occur.
|
|
// We should just use the viewportClip - the start of the overlap is at
|
|
// double.infinity and so it is effectively meaningless.
|
|
if (child.constraints.overlap == 0 || !child.constraints.viewportMainAxisExtent.isFinite) {
|
|
return viewportClip;
|
|
}
|
|
|
|
// Adjust the clip rect for this sliver by the overlap from the previous sliver.
|
|
double left = viewportClip.left;
|
|
double right = viewportClip.right;
|
|
double top = viewportClip.top;
|
|
double bottom = viewportClip.bottom;
|
|
final double startOfOverlap = child.constraints.viewportMainAxisExtent - child.constraints.remainingPaintExtent;
|
|
final double overlapCorrection = startOfOverlap + child.constraints.overlap;
|
|
switch (applyGrowthDirectionToAxisDirection(axisDirection, child.constraints.growthDirection)) {
|
|
case AxisDirection.down:
|
|
top += overlapCorrection;
|
|
break;
|
|
case AxisDirection.up:
|
|
bottom -= overlapCorrection;
|
|
break;
|
|
case AxisDirection.right:
|
|
left += overlapCorrection;
|
|
break;
|
|
case AxisDirection.left:
|
|
right -= overlapCorrection;
|
|
break;
|
|
}
|
|
return Rect.fromLTRB(left, top, right, bottom);
|
|
}
|
|
|
|
@override
|
|
Rect describeSemanticsClip(RenderSliver child) {
|
|
assert(axis != null);
|
|
|
|
if (_calculatedCacheExtent == null) {
|
|
return semanticBounds;
|
|
}
|
|
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
return Rect.fromLTRB(
|
|
semanticBounds.left,
|
|
semanticBounds.top - _calculatedCacheExtent,
|
|
semanticBounds.right,
|
|
semanticBounds.bottom + _calculatedCacheExtent,
|
|
);
|
|
case Axis.horizontal:
|
|
return Rect.fromLTRB(
|
|
semanticBounds.left - _calculatedCacheExtent,
|
|
semanticBounds.top,
|
|
semanticBounds.right + _calculatedCacheExtent,
|
|
semanticBounds.bottom,
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (firstChild == null)
|
|
return;
|
|
if (hasVisualOverflow) {
|
|
context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintContents);
|
|
} else {
|
|
_paintContents(context, offset);
|
|
}
|
|
}
|
|
|
|
void _paintContents(PaintingContext context, Offset offset) {
|
|
for (final RenderSliver child in childrenInPaintOrder) {
|
|
if (child.geometry.visible)
|
|
context.paintChild(child, offset + paintOffsetOf(child));
|
|
}
|
|
}
|
|
|
|
@override
|
|
void debugPaintSize(PaintingContext context, Offset offset) {
|
|
assert(() {
|
|
super.debugPaintSize(context, offset);
|
|
final Paint paint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1.0
|
|
..color = const Color(0xFF00FF00);
|
|
final Canvas canvas = context.canvas;
|
|
RenderSliver child = firstChild;
|
|
while (child != null) {
|
|
Size size;
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
size = Size(child.constraints.crossAxisExtent, child.geometry.layoutExtent);
|
|
break;
|
|
case Axis.horizontal:
|
|
size = Size(child.geometry.layoutExtent, child.constraints.crossAxisExtent);
|
|
break;
|
|
}
|
|
assert(size != null);
|
|
canvas.drawRect(((offset + paintOffsetOf(child)) & size).deflate(0.5), paint);
|
|
child = childAfter(child);
|
|
}
|
|
return true;
|
|
}());
|
|
}
|
|
|
|
@override
|
|
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
|
|
double mainAxisPosition, crossAxisPosition;
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
mainAxisPosition = position.dy;
|
|
crossAxisPosition = position.dx;
|
|
break;
|
|
case Axis.horizontal:
|
|
mainAxisPosition = position.dx;
|
|
crossAxisPosition = position.dy;
|
|
break;
|
|
}
|
|
assert(mainAxisPosition != null);
|
|
assert(crossAxisPosition != null);
|
|
final SliverHitTestResult sliverResult = SliverHitTestResult.wrap(result);
|
|
for (final RenderSliver child in childrenInHitTestOrder) {
|
|
if (!child.geometry.visible) {
|
|
continue;
|
|
}
|
|
final Matrix4 transform = Matrix4.identity();
|
|
applyPaintTransform(child, transform);
|
|
final bool isHit = result.addWithPaintTransform(
|
|
transform: transform,
|
|
position: null, // Manually adapting from box to sliver position below.
|
|
hitTest: (BoxHitTestResult result, Offset _) {
|
|
return child.hitTest(
|
|
sliverResult,
|
|
mainAxisPosition: computeChildMainAxisPosition(child, mainAxisPosition),
|
|
crossAxisPosition: crossAxisPosition,
|
|
);
|
|
},
|
|
);
|
|
if (isHit) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect rect }) {
|
|
double leadingScrollOffset = 0.0;
|
|
double targetMainAxisExtent;
|
|
rect ??= target.paintBounds;
|
|
|
|
// Starting at `target` and walking towards the root:
|
|
// - `child` will be the last object before we reach this viewport, and
|
|
// - `pivot` will be the last RenderBox before we reach this viewport.
|
|
RenderObject child = target;
|
|
RenderBox pivot;
|
|
bool onlySlivers = target is RenderSliver; // ... between viewport and `target` (`target` included).
|
|
while (child.parent != this) {
|
|
final RenderObject parent = child.parent as RenderObject;
|
|
assert(parent != null, '$target must be a descendant of $this');
|
|
if (child is RenderBox) {
|
|
pivot = child;
|
|
}
|
|
if (parent is RenderSliver) {
|
|
leadingScrollOffset += parent.childScrollOffset(child);
|
|
} else {
|
|
onlySlivers = false;
|
|
leadingScrollOffset = 0.0;
|
|
}
|
|
child = parent;
|
|
}
|
|
|
|
if (pivot != null) {
|
|
assert(pivot.parent != null);
|
|
assert(pivot.parent != this);
|
|
assert(pivot != this);
|
|
assert(pivot.parent is RenderSliver); // TODO(abarth): Support other kinds of render objects besides slivers.
|
|
final RenderSliver pivotParent = pivot.parent as RenderSliver;
|
|
|
|
final Matrix4 transform = target.getTransformTo(pivot);
|
|
final Rect bounds = MatrixUtils.transformRect(transform, rect);
|
|
|
|
final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
|
|
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
|
|
case AxisDirection.up:
|
|
double offset;
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
offset = bounds.bottom;
|
|
break;
|
|
case GrowthDirection.reverse:
|
|
offset = bounds.top;
|
|
break;
|
|
}
|
|
leadingScrollOffset += pivot.size.height - offset;
|
|
targetMainAxisExtent = bounds.height;
|
|
break;
|
|
case AxisDirection.right:
|
|
double offset;
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
offset = bounds.left;
|
|
break;
|
|
case GrowthDirection.reverse:
|
|
offset = bounds.right;
|
|
break;
|
|
}
|
|
leadingScrollOffset += offset;
|
|
targetMainAxisExtent = bounds.width;
|
|
break;
|
|
case AxisDirection.down:
|
|
double offset;
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
offset = bounds.top;
|
|
break;
|
|
case GrowthDirection.reverse:
|
|
offset = bounds.bottom;
|
|
break;
|
|
}
|
|
leadingScrollOffset += offset;
|
|
targetMainAxisExtent = bounds.height;
|
|
break;
|
|
case AxisDirection.left:
|
|
double offset;
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
offset = bounds.right;
|
|
break;
|
|
case GrowthDirection.reverse:
|
|
offset = bounds.left;
|
|
break;
|
|
}
|
|
leadingScrollOffset += pivot.size.width - offset;
|
|
targetMainAxisExtent = bounds.width;
|
|
break;
|
|
}
|
|
} else if (onlySlivers) {
|
|
final RenderSliver targetSliver = target as RenderSliver;
|
|
targetMainAxisExtent = targetSliver.geometry.scrollExtent;
|
|
} else {
|
|
return RevealedOffset(offset: offset.pixels, rect: rect);
|
|
}
|
|
|
|
assert(child.parent == this);
|
|
assert(child is RenderSliver);
|
|
final RenderSliver sliver = child as RenderSliver;
|
|
final double extentOfPinnedSlivers = maxScrollObstructionExtentBefore(sliver);
|
|
leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
|
|
switch (sliver.constraints.growthDirection) {
|
|
case GrowthDirection.forward:
|
|
leadingScrollOffset -= extentOfPinnedSlivers;
|
|
break;
|
|
case GrowthDirection.reverse:
|
|
// Nothing to do.
|
|
break;
|
|
}
|
|
|
|
double mainAxisExtent;
|
|
switch (axis) {
|
|
case Axis.horizontal:
|
|
mainAxisExtent = size.width - extentOfPinnedSlivers;
|
|
break;
|
|
case Axis.vertical:
|
|
mainAxisExtent = size.height - extentOfPinnedSlivers;
|
|
break;
|
|
}
|
|
|
|
final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
|
|
final double offsetDifference = offset.pixels - targetOffset;
|
|
|
|
final Matrix4 transform = target.getTransformTo(this);
|
|
applyPaintTransform(child, transform);
|
|
Rect targetRect = MatrixUtils.transformRect(transform, rect);
|
|
|
|
switch (axisDirection) {
|
|
case AxisDirection.down:
|
|
targetRect = targetRect.translate(0.0, offsetDifference);
|
|
break;
|
|
case AxisDirection.right:
|
|
targetRect = targetRect.translate(offsetDifference, 0.0);
|
|
break;
|
|
case AxisDirection.up:
|
|
targetRect = targetRect.translate(0.0, -offsetDifference);
|
|
break;
|
|
case AxisDirection.left:
|
|
targetRect = targetRect.translate(-offsetDifference, 0.0);
|
|
break;
|
|
}
|
|
|
|
return RevealedOffset(offset: targetOffset, rect: targetRect);
|
|
}
|
|
|
|
/// The offset at which the given `child` should be painted.
|
|
///
|
|
/// The returned offset is from the top left corner of the inside of the
|
|
/// viewport to the top left corner of the paint coordinate system of the
|
|
/// `child`.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [paintOffsetOf], which uses the layout offset and growth direction
|
|
/// computed for the child during layout.
|
|
@protected
|
|
Offset computeAbsolutePaintOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
|
|
assert(hasSize); // this is only usable once we have a size
|
|
assert(axisDirection != null);
|
|
assert(growthDirection != null);
|
|
assert(child != null);
|
|
assert(child.geometry != null);
|
|
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
|
|
case AxisDirection.up:
|
|
return Offset(0.0, size.height - (layoutOffset + child.geometry.paintExtent));
|
|
case AxisDirection.right:
|
|
return Offset(layoutOffset, 0.0);
|
|
case AxisDirection.down:
|
|
return Offset(0.0, layoutOffset);
|
|
case AxisDirection.left:
|
|
return Offset(size.width - (layoutOffset + child.geometry.paintExtent), 0.0);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
|
|
properties.add(EnumProperty<AxisDirection>('crossAxisDirection', crossAxisDirection));
|
|
properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
|
|
}
|
|
|
|
@override
|
|
List<DiagnosticsNode> debugDescribeChildren() {
|
|
final List<DiagnosticsNode> children = <DiagnosticsNode>[];
|
|
RenderSliver child = firstChild;
|
|
if (child == null)
|
|
return children;
|
|
|
|
int count = indexOfFirstChild;
|
|
while (true) {
|
|
children.add(child.toDiagnosticsNode(name: labelForChild(count)));
|
|
if (child == lastChild)
|
|
break;
|
|
count += 1;
|
|
child = childAfter(child);
|
|
}
|
|
return children;
|
|
}
|
|
|
|
// API TO BE IMPLEMENTED BY SUBCLASSES
|
|
|
|
// setupParentData
|
|
|
|
// performLayout (and optionally sizedByParent and performResize)
|
|
|
|
/// Whether the contents of this viewport would paint outside the bounds of
|
|
/// the viewport if [paint] did not clip.
|
|
///
|
|
/// This property enables an optimization whereby [paint] can skip apply a
|
|
/// clip of the contents of the viewport are known to paint entirely within
|
|
/// the bounds of the viewport.
|
|
@protected
|
|
bool get hasVisualOverflow;
|
|
|
|
/// Called during [layoutChildSequence] for each child.
|
|
///
|
|
/// Typically used by subclasses to update any out-of-band data, such as the
|
|
/// max scroll extent, for each child.
|
|
@protected
|
|
void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry);
|
|
|
|
/// Called during [layoutChildSequence] to store the layout offset for the
|
|
/// given child.
|
|
///
|
|
/// Different subclasses using different representations for their children's
|
|
/// layout offset (e.g., logical or physical coordinates). This function lets
|
|
/// subclasses transform the child's layout offset before storing it in the
|
|
/// child's parent data.
|
|
@protected
|
|
void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection);
|
|
|
|
/// The offset at which the given `child` should be painted.
|
|
///
|
|
/// The returned offset is from the top left corner of the inside of the
|
|
/// viewport to the top left corner of the paint coordinate system of the
|
|
/// `child`.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [computeAbsolutePaintOffset], which computes the paint offset from an
|
|
/// explicit layout offset and growth direction instead of using the values
|
|
/// computed for the child during layout.
|
|
@protected
|
|
Offset paintOffsetOf(RenderSliver child);
|
|
|
|
/// Returns the scroll offset within the viewport for the given
|
|
/// `scrollOffsetWithinChild` within the given `child`.
|
|
///
|
|
/// The returned value is an estimate that assumes the slivers within the
|
|
/// viewport do not change the layout extent in response to changes in their
|
|
/// scroll offset.
|
|
@protected
|
|
double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild);
|
|
|
|
/// Returns the total scroll obstruction extent of all slivers in the viewport
|
|
/// before [child].
|
|
///
|
|
/// This is the extent by which the actual area in which content can scroll
|
|
/// is reduced. For example, an app bar that is pinned at the top will reduce
|
|
/// the area in which content can actually scroll by the height of the app bar.
|
|
@protected
|
|
double maxScrollObstructionExtentBefore(RenderSliver child);
|
|
|
|
/// Converts the `parentMainAxisPosition` into the child's coordinate system.
|
|
///
|
|
/// The `parentMainAxisPosition` is a distance from the top edge (for vertical
|
|
/// viewports) or left edge (for horizontal viewports) of the viewport bounds.
|
|
/// This describes a line, perpendicular to the viewport's main axis, heretofor
|
|
/// known as the target line.
|
|
///
|
|
/// The child's coordinate system's origin in the main axis is at the leading
|
|
/// edge of the given child, as given by the child's
|
|
/// [SliverConstraints.axisDirection] and [SliverConstraints.growthDirection].
|
|
///
|
|
/// This method returns the distance from the leading edge of the given child to
|
|
/// the target line described above.
|
|
///
|
|
/// (The `parentMainAxisPosition` is not from the leading edge of the
|
|
/// viewport, it's always the top or left edge.)
|
|
@protected
|
|
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition);
|
|
|
|
/// The index of the first child of the viewport relative to the center child.
|
|
///
|
|
/// For example, the center child has index zero and the first child in the
|
|
/// reverse growth direction has index -1.
|
|
@protected
|
|
int get indexOfFirstChild;
|
|
|
|
/// A short string to identify the child with the given index.
|
|
///
|
|
/// Used by [debugDescribeChildren] to label the children.
|
|
@protected
|
|
String labelForChild(int index);
|
|
|
|
/// Provides an iterable that walks the children of the viewport, in the order
|
|
/// that they should be painted.
|
|
///
|
|
/// This should be the reverse order of [childrenInHitTestOrder].
|
|
@protected
|
|
Iterable<RenderSliver> get childrenInPaintOrder;
|
|
|
|
/// Provides an iterable that walks the children of the viewport, in the order
|
|
/// that hit-testing should use.
|
|
///
|
|
/// This should be the reverse order of [childrenInPaintOrder].
|
|
@protected
|
|
Iterable<RenderSliver> get childrenInHitTestOrder;
|
|
|
|
@override
|
|
void showOnScreen({
|
|
RenderObject descendant,
|
|
Rect rect,
|
|
Duration duration = Duration.zero,
|
|
Curve curve = Curves.ease,
|
|
}) {
|
|
if (!offset.allowImplicitScrolling) {
|
|
return super.showOnScreen(
|
|
descendant: descendant,
|
|
rect: rect,
|
|
duration: duration,
|
|
curve: curve,
|
|
);
|
|
}
|
|
|
|
final Rect newRect = RenderViewportBase.showInViewport(
|
|
descendant: descendant,
|
|
viewport: this,
|
|
offset: offset,
|
|
rect: rect,
|
|
duration: duration,
|
|
curve: curve,
|
|
);
|
|
super.showOnScreen(
|
|
rect: newRect,
|
|
duration: duration,
|
|
curve: curve,
|
|
);
|
|
}
|
|
|
|
/// Make (a portion of) the given `descendant` of the given `viewport` fully
|
|
/// visible in the `viewport` by manipulating the provided [ViewportOffset]
|
|
/// `offset`.
|
|
///
|
|
/// The optional `rect` parameter describes which area of the `descendant`
|
|
/// should be shown in the viewport. If `rect` is null, the entire
|
|
/// `descendant` will be revealed. The `rect` parameter is interpreted
|
|
/// relative to the coordinate system of `descendant`.
|
|
///
|
|
/// The returned [Rect] describes the new location of `descendant` or `rect`
|
|
/// in the viewport after it has been revealed. See [RevealedOffset.rect]
|
|
/// for a full definition of this [Rect].
|
|
///
|
|
/// The parameters `viewport` and `offset` are required and cannot be null.
|
|
/// If `descendant` is null, this is a no-op and `rect` is returned.
|
|
///
|
|
/// If both `descendant` and `rect` are null, null is returned because there is
|
|
/// nothing to be shown in the viewport.
|
|
///
|
|
/// The `duration` parameter can be set to a non-zero value to animate the
|
|
/// target object into the viewport with an animation defined by `curve`.
|
|
static Rect showInViewport({
|
|
RenderObject descendant,
|
|
Rect rect,
|
|
@required RenderAbstractViewport viewport,
|
|
@required ViewportOffset offset,
|
|
Duration duration = Duration.zero,
|
|
Curve curve = Curves.ease,
|
|
}) {
|
|
assert(viewport != null);
|
|
assert(offset != null);
|
|
if (descendant == null) {
|
|
return rect;
|
|
}
|
|
final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal(descendant, 0.0, rect: rect);
|
|
final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal(descendant, 1.0, rect: rect);
|
|
final double currentOffset = offset.pixels;
|
|
|
|
// scrollOffset
|
|
// 0 +---------+
|
|
// | |
|
|
// _ | |
|
|
// viewport position | | |
|
|
// with `descendant` at | | | _
|
|
// trailing edge |_ | xxxxxxx | | viewport position
|
|
// | | | with `descendant` at
|
|
// | | _| leading edge
|
|
// | |
|
|
// 800 +---------+
|
|
//
|
|
// `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the
|
|
// viewport on the left in image above.
|
|
// `leadingEdgeOffset`: Distance from scrollOffset 0 to the start of the
|
|
// viewport on the right in image above.
|
|
//
|
|
// The viewport position on the left is achieved by setting `offset.pixels`
|
|
// to `trailingEdgeOffset`, the one on the right by setting it to
|
|
// `leadingEdgeOffset`.
|
|
|
|
RevealedOffset targetOffset;
|
|
if (leadingEdgeOffset.offset < trailingEdgeOffset.offset) {
|
|
// `descendant` is too big to be visible on screen in its entirety. Let's
|
|
// align it with the edge that requires the least amount of scrolling.
|
|
final double leadingEdgeDiff = (offset.pixels - leadingEdgeOffset.offset).abs();
|
|
final double trailingEdgeDiff = (offset.pixels - trailingEdgeOffset.offset).abs();
|
|
targetOffset = leadingEdgeDiff < trailingEdgeDiff ? leadingEdgeOffset : trailingEdgeOffset;
|
|
} else if (currentOffset > leadingEdgeOffset.offset) {
|
|
// `descendant` currently starts above the leading edge and can be shown
|
|
// fully on screen by scrolling down (which means: moving viewport up).
|
|
targetOffset = leadingEdgeOffset;
|
|
} else if (currentOffset < trailingEdgeOffset.offset) {
|
|
// `descendant currently ends below the trailing edge and can be shown
|
|
// fully on screen by scrolling up (which means: moving viewport down)
|
|
targetOffset = trailingEdgeOffset;
|
|
} else {
|
|
// `descendant` is between leading and trailing edge and hence already
|
|
// fully shown on screen. No action necessary.
|
|
final Matrix4 transform = descendant.getTransformTo(viewport.parent as RenderObject);
|
|
return MatrixUtils.transformRect(transform, rect ?? descendant.paintBounds);
|
|
}
|
|
|
|
assert(targetOffset != null);
|
|
|
|
offset.moveTo(targetOffset.offset, duration: duration, curve: curve);
|
|
return targetOffset.rect;
|
|
}
|
|
}
|
|
|
|
/// A render object that is bigger on the inside.
|
|
///
|
|
/// [RenderViewport] is the visual workhorse of the scrolling machinery. It
|
|
/// displays a subset of its children according to its own dimensions and the
|
|
/// given [offset]. As the offset varies, different children are visible through
|
|
/// the viewport.
|
|
///
|
|
/// [RenderViewport] hosts a bidirectional list of slivers, anchored on a
|
|
/// [center] sliver, which is placed at the zero scroll offset. The center
|
|
/// widget is displayed in the viewport according to the [anchor] property.
|
|
///
|
|
/// Slivers that are earlier in the child list than [center] are displayed in
|
|
/// reverse order in the reverse [axisDirection] starting from the [center]. For
|
|
/// example, if the [axisDirection] is [AxisDirection.down], the first sliver
|
|
/// before [center] is placed above the [center]. The slivers that are later in
|
|
/// the child list than [center] are placed in order in the [axisDirection]. For
|
|
/// example, in the preceding scenario, the first sliver after [center] is
|
|
/// placed below the [center].
|
|
///
|
|
/// [RenderViewport] cannot contain [RenderBox] children directly. Instead, use
|
|
/// a [RenderSliverList], [RenderSliverFixedExtentList], [RenderSliverGrid], or
|
|
/// a [RenderSliverToBoxAdapter], for example.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderSliver], which explains more about the Sliver protocol.
|
|
/// * [RenderBox], which explains more about the Box protocol.
|
|
/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
|
|
/// placed inside a [RenderSliver] (the opposite of this class).
|
|
/// * [RenderShrinkWrappingViewport], a variant of [RenderViewport] that
|
|
/// shrink-wraps its contents along the main axis.
|
|
class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentData> {
|
|
/// Creates a viewport for [RenderSliver] objects.
|
|
///
|
|
/// If the [center] is not specified, then the first child in the `children`
|
|
/// list, if any, is used.
|
|
///
|
|
/// The [offset] must be specified. For testing purposes, consider passing a
|
|
/// [new ViewportOffset.zero] or [new ViewportOffset.fixed].
|
|
RenderViewport({
|
|
AxisDirection axisDirection = AxisDirection.down,
|
|
@required AxisDirection crossAxisDirection,
|
|
@required ViewportOffset offset,
|
|
double anchor = 0.0,
|
|
List<RenderSliver> children,
|
|
RenderSliver center,
|
|
double cacheExtent,
|
|
CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
|
|
}) : assert(anchor != null),
|
|
assert(anchor >= 0.0 && anchor <= 1.0),
|
|
assert(cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null),
|
|
_anchor = anchor,
|
|
_center = center,
|
|
super(
|
|
axisDirection: axisDirection,
|
|
crossAxisDirection: crossAxisDirection,
|
|
offset: offset,
|
|
cacheExtent: cacheExtent,
|
|
cacheExtentStyle: cacheExtentStyle,
|
|
) {
|
|
addAll(children);
|
|
if (center == null && firstChild != null)
|
|
_center = firstChild;
|
|
}
|
|
|
|
/// If a [RenderAbstractViewport] overrides
|
|
/// [RenderObject.describeSemanticsConfiguration] to add the [SemanticsTag]
|
|
/// [useTwoPaneSemantics] to its [SemanticsConfiguration], two semantics nodes
|
|
/// will be used to represent the viewport with its associated scrolling
|
|
/// actions in the semantics tree.
|
|
///
|
|
/// Two semantics nodes (an inner and an outer node) are necessary to exclude
|
|
/// certain child nodes (via the [excludeFromScrolling] tag) from the
|
|
/// scrollable area for semantic purposes: The [SemanticsNode]s of children
|
|
/// that should be excluded from scrolling will be attached to the outer node.
|
|
/// The semantic scrolling actions and the [SemanticsNode]s of scrollable
|
|
/// children will be attached to the inner node, which itself is a child of
|
|
/// the outer node.
|
|
static const SemanticsTag useTwoPaneSemantics = SemanticsTag('RenderViewport.twoPane');
|
|
|
|
/// When a top-level [SemanticsNode] below a [RenderAbstractViewport] is
|
|
/// tagged with [excludeFromScrolling] it will not be part of the scrolling
|
|
/// area for semantic purposes.
|
|
///
|
|
/// This behavior is only active if the [RenderAbstractViewport]
|
|
/// tagged its [SemanticsConfiguration] with [useTwoPaneSemantics].
|
|
/// Otherwise, the [excludeFromScrolling] tag is ignored.
|
|
///
|
|
/// As an example, a [RenderSliver] that stays on the screen within a
|
|
/// [Scrollable] even though the user has scrolled past it (e.g. a pinned app
|
|
/// bar) can tag its [SemanticsNode] with [excludeFromScrolling] to indicate
|
|
/// that it should no longer be considered for semantic actions related to
|
|
/// scrolling.
|
|
static const SemanticsTag excludeFromScrolling = SemanticsTag('RenderViewport.excludeFromScrolling');
|
|
|
|
@override
|
|
void setupParentData(RenderObject child) {
|
|
if (child.parentData is! SliverPhysicalContainerParentData)
|
|
child.parentData = SliverPhysicalContainerParentData();
|
|
}
|
|
|
|
/// The relative position of the zero scroll offset.
|
|
///
|
|
/// For example, if [anchor] is 0.5 and the [axisDirection] is
|
|
/// [AxisDirection.down] or [AxisDirection.up], then the zero scroll offset is
|
|
/// vertically centered within the viewport. If the [anchor] is 1.0, and the
|
|
/// [axisDirection] is [AxisDirection.right], then the zero scroll offset is
|
|
/// on the left edge of the viewport.
|
|
double get anchor => _anchor;
|
|
double _anchor;
|
|
set anchor(double value) {
|
|
assert(value != null);
|
|
assert(value >= 0.0 && value <= 1.0);
|
|
if (value == _anchor)
|
|
return;
|
|
_anchor = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// The first child in the [GrowthDirection.forward] growth direction.
|
|
///
|
|
/// This child that will be at the position defined by [anchor] when the
|
|
/// [offset.pixels] is `0`.
|
|
///
|
|
/// Children after [center] will be placed in the [axisDirection] relative to
|
|
/// the [center]. Children before [center] will be placed in the opposite of
|
|
/// the [axisDirection] relative to the [center].
|
|
///
|
|
/// The [center] must be a child of the viewport.
|
|
RenderSliver get center => _center;
|
|
RenderSliver _center;
|
|
set center(RenderSliver value) {
|
|
if (value == _center)
|
|
return;
|
|
_center = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
@override
|
|
bool get sizedByParent => true;
|
|
|
|
@override
|
|
void performResize() {
|
|
assert(() {
|
|
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
if (!constraints.hasBoundedHeight) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('Vertical viewport was given unbounded height.'),
|
|
ErrorDescription(
|
|
'Viewports expand in the scrolling direction to fill their container. '
|
|
'In this case, a vertical viewport was given an unlimited amount of '
|
|
'vertical space in which to expand. This situation typically happens '
|
|
'when a scrollable widget is nested inside another scrollable widget.'
|
|
),
|
|
ErrorHint(
|
|
'If this widget is always nested in a scrollable widget there '
|
|
'is no need to use a viewport because there will always be enough '
|
|
'vertical space for the children. In this case, consider using a '
|
|
'Column instead. Otherwise, consider using the "shrinkWrap" property '
|
|
'(or a ShrinkWrappingViewport) to size the height of the viewport '
|
|
'to the sum of the heights of its children.'
|
|
)
|
|
]);
|
|
}
|
|
if (!constraints.hasBoundedWidth) {
|
|
throw FlutterError(
|
|
'Vertical viewport was given unbounded width.\n'
|
|
'Viewports expand in the cross axis to fill their container and '
|
|
'constrain their children to match their extent in the cross axis. '
|
|
'In this case, a vertical viewport was given an unlimited amount of '
|
|
'horizontal space in which to expand.'
|
|
);
|
|
}
|
|
break;
|
|
case Axis.horizontal:
|
|
if (!constraints.hasBoundedWidth) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('Horizontal viewport was given unbounded width.'),
|
|
ErrorDescription(
|
|
'Viewports expand in the scrolling direction to fill their container. '
|
|
'In this case, a horizontal viewport was given an unlimited amount of '
|
|
'horizontal space in which to expand. This situation typically happens '
|
|
'when a scrollable widget is nested inside another scrollable widget.'
|
|
),
|
|
ErrorHint(
|
|
'If this widget is always nested in a scrollable widget there '
|
|
'is no need to use a viewport because there will always be enough '
|
|
'horizontal space for the children. In this case, consider using a '
|
|
'Row instead. Otherwise, consider using the "shrinkWrap" property '
|
|
'(or a ShrinkWrappingViewport) to size the width of the viewport '
|
|
'to the sum of the widths of its children.'
|
|
)
|
|
]);
|
|
}
|
|
if (!constraints.hasBoundedHeight) {
|
|
throw FlutterError(
|
|
'Horizontal viewport was given unbounded height.\n'
|
|
'Viewports expand in the cross axis to fill their container and '
|
|
'constrain their children to match their extent in the cross axis. '
|
|
'In this case, a horizontal viewport was given an unlimited amount of '
|
|
'vertical space in which to expand.'
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}());
|
|
size = constraints.biggest;
|
|
// We ignore the return value of applyViewportDimension below because we are
|
|
// going to go through performLayout next regardless.
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
offset.applyViewportDimension(size.height);
|
|
break;
|
|
case Axis.horizontal:
|
|
offset.applyViewportDimension(size.width);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static const int _maxLayoutCycles = 10;
|
|
|
|
// Out-of-band data computed during layout.
|
|
double _minScrollExtent;
|
|
double _maxScrollExtent;
|
|
bool _hasVisualOverflow = false;
|
|
|
|
@override
|
|
void performLayout() {
|
|
if (center == null) {
|
|
assert(firstChild == null);
|
|
_minScrollExtent = 0.0;
|
|
_maxScrollExtent = 0.0;
|
|
_hasVisualOverflow = false;
|
|
offset.applyContentDimensions(0.0, 0.0);
|
|
return;
|
|
}
|
|
assert(center.parent == this);
|
|
|
|
double mainAxisExtent;
|
|
double crossAxisExtent;
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
mainAxisExtent = size.height;
|
|
crossAxisExtent = size.width;
|
|
break;
|
|
case Axis.horizontal:
|
|
mainAxisExtent = size.width;
|
|
crossAxisExtent = size.height;
|
|
break;
|
|
}
|
|
|
|
final double centerOffsetAdjustment = center.centerOffsetAdjustment;
|
|
|
|
double correction;
|
|
int count = 0;
|
|
do {
|
|
assert(offset.pixels != null);
|
|
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
|
|
if (correction != 0.0) {
|
|
offset.correctBy(correction);
|
|
} else {
|
|
if (offset.applyContentDimensions(
|
|
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
|
|
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
|
|
))
|
|
break;
|
|
}
|
|
count += 1;
|
|
} while (count < _maxLayoutCycles);
|
|
assert(() {
|
|
if (count >= _maxLayoutCycles) {
|
|
assert(count != 1);
|
|
throw FlutterError(
|
|
'A RenderViewport exceeded its maximum number of layout cycles.\n'
|
|
'RenderViewport render objects, during layout, can retry if either their '
|
|
'slivers or their ViewportOffset decide that the offset should be corrected '
|
|
'to take into account information collected during that layout.\n'
|
|
'In the case of this RenderViewport object, however, this happened $count '
|
|
'times and still there was no consensus on the scroll offset. This usually '
|
|
'indicates a bug. Specifically, it means that one of the following three '
|
|
'problems is being experienced by the RenderViewport object:\n'
|
|
' * One of the RenderSliver children or the ViewportOffset have a bug such'
|
|
' that they always think that they need to correct the offset regardless.\n'
|
|
' * Some combination of the RenderSliver children and the ViewportOffset'
|
|
' have a bad interaction such that one applies a correction then another'
|
|
' applies a reverse correction, leading to an infinite loop of corrections.\n'
|
|
' * There is a pathological case that would eventually resolve, but it is'
|
|
' so complicated that it cannot be resolved in any reasonable number of'
|
|
' layout passes.'
|
|
);
|
|
}
|
|
return true;
|
|
}());
|
|
}
|
|
|
|
double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
|
|
assert(!mainAxisExtent.isNaN);
|
|
assert(mainAxisExtent >= 0.0);
|
|
assert(crossAxisExtent.isFinite);
|
|
assert(crossAxisExtent >= 0.0);
|
|
assert(correctedOffset.isFinite);
|
|
_minScrollExtent = 0.0;
|
|
_maxScrollExtent = 0.0;
|
|
_hasVisualOverflow = false;
|
|
|
|
// centerOffset is the offset from the leading edge of the RenderViewport
|
|
// to the zero scroll offset (the line between the forward slivers and the
|
|
// reverse slivers).
|
|
final double centerOffset = mainAxisExtent * anchor - correctedOffset;
|
|
final double reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent) as double;
|
|
final double forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent) as double;
|
|
|
|
switch (cacheExtentStyle) {
|
|
case CacheExtentStyle.pixel:
|
|
_calculatedCacheExtent = cacheExtent;
|
|
break;
|
|
case CacheExtentStyle.viewport:
|
|
_calculatedCacheExtent = mainAxisExtent * cacheExtent;
|
|
break;
|
|
}
|
|
|
|
final double fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent;
|
|
final double centerCacheOffset = centerOffset + _calculatedCacheExtent;
|
|
final double reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent) as double;
|
|
final double forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent) as double;
|
|
|
|
final RenderSliver leadingNegativeChild = childBefore(center);
|
|
|
|
if (leadingNegativeChild != null) {
|
|
// negative scroll offsets
|
|
final double result = layoutChildSequence(
|
|
child: leadingNegativeChild,
|
|
scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
|
|
overlap: 0.0,
|
|
layoutOffset: forwardDirectionRemainingPaintExtent,
|
|
remainingPaintExtent: reverseDirectionRemainingPaintExtent,
|
|
mainAxisExtent: mainAxisExtent,
|
|
crossAxisExtent: crossAxisExtent,
|
|
growthDirection: GrowthDirection.reverse,
|
|
advance: childBefore,
|
|
remainingCacheExtent: reverseDirectionRemainingCacheExtent,
|
|
cacheOrigin: (mainAxisExtent - centerOffset).clamp(-_calculatedCacheExtent, 0.0) as double,
|
|
);
|
|
if (result != 0.0)
|
|
return -result;
|
|
}
|
|
|
|
// positive scroll offsets
|
|
return layoutChildSequence(
|
|
child: center,
|
|
scrollOffset: math.max(0.0, -centerOffset),
|
|
overlap: leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
|
|
layoutOffset: centerOffset >= mainAxisExtent ? centerOffset: reverseDirectionRemainingPaintExtent,
|
|
remainingPaintExtent: forwardDirectionRemainingPaintExtent,
|
|
mainAxisExtent: mainAxisExtent,
|
|
crossAxisExtent: crossAxisExtent,
|
|
growthDirection: GrowthDirection.forward,
|
|
advance: childAfter,
|
|
remainingCacheExtent: forwardDirectionRemainingCacheExtent,
|
|
cacheOrigin: centerOffset.clamp(-_calculatedCacheExtent, 0.0) as double,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get hasVisualOverflow => _hasVisualOverflow;
|
|
|
|
@override
|
|
void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
_maxScrollExtent += childLayoutGeometry.scrollExtent;
|
|
break;
|
|
case GrowthDirection.reverse:
|
|
_minScrollExtent -= childLayoutGeometry.scrollExtent;
|
|
break;
|
|
}
|
|
if (childLayoutGeometry.hasVisualOverflow)
|
|
_hasVisualOverflow = true;
|
|
}
|
|
|
|
@override
|
|
void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
|
|
final SliverPhysicalParentData childParentData = child.parentData as SliverPhysicalParentData;
|
|
childParentData.paintOffset = computeAbsolutePaintOffset(child, layoutOffset, growthDirection);
|
|
}
|
|
|
|
@override
|
|
Offset paintOffsetOf(RenderSliver child) {
|
|
final SliverPhysicalParentData childParentData = child.parentData as SliverPhysicalParentData;
|
|
return childParentData.paintOffset;
|
|
}
|
|
|
|
@override
|
|
double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) {
|
|
assert(child.parent == this);
|
|
final GrowthDirection growthDirection = child.constraints.growthDirection;
|
|
assert(growthDirection != null);
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
double scrollOffsetToChild = 0.0;
|
|
RenderSliver current = center;
|
|
while (current != child) {
|
|
scrollOffsetToChild += current.geometry.scrollExtent;
|
|
current = childAfter(current);
|
|
}
|
|
return scrollOffsetToChild + scrollOffsetWithinChild;
|
|
case GrowthDirection.reverse:
|
|
double scrollOffsetToChild = 0.0;
|
|
RenderSliver current = childBefore(center);
|
|
while (current != child) {
|
|
scrollOffsetToChild -= current.geometry.scrollExtent;
|
|
current = childBefore(current);
|
|
}
|
|
return scrollOffsetToChild - scrollOffsetWithinChild;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
double maxScrollObstructionExtentBefore(RenderSliver child) {
|
|
assert(child.parent == this);
|
|
final GrowthDirection growthDirection = child.constraints.growthDirection;
|
|
assert(growthDirection != null);
|
|
switch (growthDirection) {
|
|
case GrowthDirection.forward:
|
|
double pinnedExtent = 0.0;
|
|
RenderSliver current = center;
|
|
while (current != child) {
|
|
pinnedExtent += current.geometry.maxScrollObstructionExtent;
|
|
current = childAfter(current);
|
|
}
|
|
return pinnedExtent;
|
|
case GrowthDirection.reverse:
|
|
double pinnedExtent = 0.0;
|
|
RenderSliver current = childBefore(center);
|
|
while (current != child) {
|
|
pinnedExtent += current.geometry.maxScrollObstructionExtent;
|
|
current = childBefore(current);
|
|
}
|
|
return pinnedExtent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
|
assert(child != null);
|
|
final SliverPhysicalParentData childParentData = child.parentData as SliverPhysicalParentData;
|
|
childParentData.applyPaintTransform(transform);
|
|
}
|
|
|
|
@override
|
|
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
|
|
assert(child != null);
|
|
assert(child.constraints != null);
|
|
final SliverPhysicalParentData childParentData = child.parentData as SliverPhysicalParentData;
|
|
switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
|
|
case AxisDirection.down:
|
|
return parentMainAxisPosition - childParentData.paintOffset.dy;
|
|
case AxisDirection.right:
|
|
return parentMainAxisPosition - childParentData.paintOffset.dx;
|
|
case AxisDirection.up:
|
|
return child.geometry.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dy);
|
|
case AxisDirection.left:
|
|
return child.geometry.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dx);
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
@override
|
|
int get indexOfFirstChild {
|
|
assert(center != null);
|
|
assert(center.parent == this);
|
|
assert(firstChild != null);
|
|
int count = 0;
|
|
RenderSliver child = center;
|
|
while (child != firstChild) {
|
|
count -= 1;
|
|
child = childBefore(child);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
@override
|
|
String labelForChild(int index) {
|
|
if (index == 0)
|
|
return 'center child';
|
|
return 'child $index';
|
|
}
|
|
|
|
@override
|
|
Iterable<RenderSliver> get childrenInPaintOrder sync* {
|
|
if (firstChild == null)
|
|
return;
|
|
RenderSliver child = firstChild;
|
|
while (child != center) {
|
|
yield child;
|
|
child = childAfter(child);
|
|
}
|
|
child = lastChild;
|
|
while (true) {
|
|
yield child;
|
|
if (child == center)
|
|
return;
|
|
child = childBefore(child);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Iterable<RenderSliver> get childrenInHitTestOrder sync* {
|
|
if (firstChild == null)
|
|
return;
|
|
RenderSliver child = center;
|
|
while (child != null) {
|
|
yield child;
|
|
child = childAfter(child);
|
|
}
|
|
child = childBefore(center);
|
|
while (child != null) {
|
|
yield child;
|
|
child = childBefore(child);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(DoubleProperty('anchor', anchor));
|
|
}
|
|
}
|
|
|
|
/// A render object that is bigger on the inside and shrink wraps its children
|
|
/// in the main axis.
|
|
///
|
|
/// [RenderShrinkWrappingViewport] displays a subset of its children according
|
|
/// to its own dimensions and the given [offset]. As the offset varies, different
|
|
/// children are visible through the viewport.
|
|
///
|
|
/// [RenderShrinkWrappingViewport] differs from [RenderViewport] in that
|
|
/// [RenderViewport] expands to fill the main axis whereas
|
|
/// [RenderShrinkWrappingViewport] sizes itself to match its children in the
|
|
/// main axis. This shrink wrapping behavior is expensive because the children,
|
|
/// and hence the viewport, could potentially change size whenever the [offset]
|
|
/// changes (e.g., because of a collapsing header).
|
|
///
|
|
/// [RenderShrinkWrappingViewport] cannot contain [RenderBox] children directly.
|
|
/// Instead, use a [RenderSliverList], [RenderSliverFixedExtentList],
|
|
/// [RenderSliverGrid], or a [RenderSliverToBoxAdapter], for example.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RenderViewport], a viewport that does not shrink-wrap its contents.
|
|
/// * [RenderSliver], which explains more about the Sliver protocol.
|
|
/// * [RenderBox], which explains more about the Box protocol.
|
|
/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
|
|
/// placed inside a [RenderSliver] (the opposite of this class).
|
|
class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalContainerParentData> {
|
|
/// Creates a viewport (for [RenderSliver] objects) that shrink-wraps its
|
|
/// contents.
|
|
///
|
|
/// The [offset] must be specified. For testing purposes, consider passing a
|
|
/// [new ViewportOffset.zero] or [new ViewportOffset.fixed].
|
|
RenderShrinkWrappingViewport({
|
|
AxisDirection axisDirection = AxisDirection.down,
|
|
@required AxisDirection crossAxisDirection,
|
|
@required ViewportOffset offset,
|
|
List<RenderSliver> children,
|
|
}) : super(axisDirection: axisDirection, crossAxisDirection: crossAxisDirection, offset: offset) {
|
|
addAll(children);
|
|
}
|
|
|
|
@override
|
|
void setupParentData(RenderObject child) {
|
|
if (child.parentData is! SliverLogicalContainerParentData)
|
|
child.parentData = SliverLogicalContainerParentData();
|
|
}
|
|
|
|
@override
|
|
bool debugThrowIfNotCheckingIntrinsics() {
|
|
assert(() {
|
|
if (!RenderObject.debugCheckingIntrinsics) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('$runtimeType does not support returning intrinsic dimensions.'),
|
|
ErrorDescription(
|
|
'Calculating the intrinsic dimensions would require instantiating every child of '
|
|
'the viewport, which defeats the point of viewports being lazy.'
|
|
),
|
|
ErrorHint(
|
|
'If you are merely trying to shrink-wrap the viewport in the main axis direction, '
|
|
'you should be able to achieve that effect by just giving the viewport loose '
|
|
'constraints, without needing to measure its intrinsic dimensions.'
|
|
)
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
return true;
|
|
}
|
|
|
|
// Out-of-band data computed during layout.
|
|
double _maxScrollExtent;
|
|
double _shrinkWrapExtent;
|
|
bool _hasVisualOverflow = false;
|
|
|
|
@override
|
|
void performLayout() {
|
|
final BoxConstraints constraints = this.constraints;
|
|
if (firstChild == null) {
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
assert(constraints.hasBoundedWidth);
|
|
size = Size(constraints.maxWidth, constraints.minHeight);
|
|
break;
|
|
case Axis.horizontal:
|
|
assert(constraints.hasBoundedHeight);
|
|
size = Size(constraints.minWidth, constraints.maxHeight);
|
|
break;
|
|
}
|
|
offset.applyViewportDimension(0.0);
|
|
_maxScrollExtent = 0.0;
|
|
_shrinkWrapExtent = 0.0;
|
|
_hasVisualOverflow = false;
|
|
offset.applyContentDimensions(0.0, 0.0);
|
|
return;
|
|
}
|
|
|
|
double mainAxisExtent;
|
|
double crossAxisExtent;
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
assert(constraints.hasBoundedWidth);
|
|
mainAxisExtent = constraints.maxHeight;
|
|
crossAxisExtent = constraints.maxWidth;
|
|
break;
|
|
case Axis.horizontal:
|
|
assert(constraints.hasBoundedHeight);
|
|
mainAxisExtent = constraints.maxWidth;
|
|
crossAxisExtent = constraints.maxHeight;
|
|
break;
|
|
}
|
|
|
|
double correction;
|
|
double effectiveExtent;
|
|
do {
|
|
assert(offset.pixels != null);
|
|
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels);
|
|
if (correction != 0.0) {
|
|
offset.correctBy(correction);
|
|
} else {
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent);
|
|
break;
|
|
case Axis.horizontal:
|
|
effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent);
|
|
break;
|
|
}
|
|
final bool didAcceptViewportDimension = offset.applyViewportDimension(effectiveExtent);
|
|
final bool didAcceptContentDimension = offset.applyContentDimensions(0.0, math.max(0.0, _maxScrollExtent - effectiveExtent));
|
|
if (didAcceptViewportDimension && didAcceptContentDimension)
|
|
break;
|
|
}
|
|
} while (true);
|
|
switch (axis) {
|
|
case Axis.vertical:
|
|
size = constraints.constrainDimensions(crossAxisExtent, effectiveExtent);
|
|
break;
|
|
case Axis.horizontal:
|
|
size = constraints.constrainDimensions(effectiveExtent, crossAxisExtent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
|
|
// We can't assert mainAxisExtent is finite, because it could be infinite if
|
|
// it is within a column or row for example. In such a case, there's not
|
|
// even any scrolling to do, although some scroll physics (i.e.
|
|
// BouncingScrollPhysics) could still temporarily scroll the content in a
|
|
// simulation.
|
|
assert(!mainAxisExtent.isNaN);
|
|
assert(mainAxisExtent >= 0.0);
|
|
assert(crossAxisExtent.isFinite);
|
|
assert(crossAxisExtent >= 0.0);
|
|
assert(correctedOffset.isFinite);
|
|
_maxScrollExtent = 0.0;
|
|
_shrinkWrapExtent = 0.0;
|
|
_hasVisualOverflow = false;
|
|
return layoutChildSequence(
|
|
child: firstChild,
|
|
scrollOffset: math.max(0.0, correctedOffset),
|
|
overlap: math.min(0.0, correctedOffset),
|
|
layoutOffset: 0.0,
|
|
remainingPaintExtent: mainAxisExtent,
|
|
mainAxisExtent: mainAxisExtent,
|
|
crossAxisExtent: crossAxisExtent,
|
|
growthDirection: GrowthDirection.forward,
|
|
advance: childAfter,
|
|
remainingCacheExtent: mainAxisExtent + 2 * cacheExtent,
|
|
cacheOrigin: -cacheExtent,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get hasVisualOverflow => _hasVisualOverflow;
|
|
|
|
@override
|
|
void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
|
|
assert(growthDirection == GrowthDirection.forward);
|
|
_maxScrollExtent += childLayoutGeometry.scrollExtent;
|
|
if (childLayoutGeometry.hasVisualOverflow)
|
|
_hasVisualOverflow = true;
|
|
_shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
|
|
}
|
|
|
|
@override
|
|
void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
|
|
assert(growthDirection == GrowthDirection.forward);
|
|
final SliverLogicalParentData childParentData = child.parentData as SliverLogicalParentData;
|
|
childParentData.layoutOffset = layoutOffset;
|
|
}
|
|
|
|
@override
|
|
Offset paintOffsetOf(RenderSliver child) {
|
|
final SliverLogicalParentData childParentData = child.parentData as SliverLogicalParentData;
|
|
return computeAbsolutePaintOffset(child, childParentData.layoutOffset, GrowthDirection.forward);
|
|
}
|
|
|
|
@override
|
|
double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) {
|
|
assert(child.parent == this);
|
|
assert(child.constraints.growthDirection == GrowthDirection.forward);
|
|
double scrollOffsetToChild = 0.0;
|
|
RenderSliver current = firstChild;
|
|
while (current != child) {
|
|
scrollOffsetToChild += current.geometry.scrollExtent;
|
|
current = childAfter(current);
|
|
}
|
|
return scrollOffsetToChild + scrollOffsetWithinChild;
|
|
}
|
|
|
|
@override
|
|
double maxScrollObstructionExtentBefore(RenderSliver child) {
|
|
assert(child.parent == this);
|
|
assert(child.constraints.growthDirection == GrowthDirection.forward);
|
|
double pinnedExtent = 0.0;
|
|
RenderSliver current = firstChild;
|
|
while (current != child) {
|
|
pinnedExtent += current.geometry.maxScrollObstructionExtent;
|
|
current = childAfter(current);
|
|
}
|
|
return pinnedExtent;
|
|
}
|
|
|
|
@override
|
|
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
|
assert(child != null);
|
|
final Offset offset = paintOffsetOf(child as RenderSliver);
|
|
transform.translate(offset.dx, offset.dy);
|
|
}
|
|
|
|
@override
|
|
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
|
|
assert(child != null);
|
|
assert(child.constraints != null);
|
|
assert(hasSize);
|
|
final SliverLogicalParentData childParentData = child.parentData as SliverLogicalParentData;
|
|
switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
|
|
case AxisDirection.down:
|
|
case AxisDirection.right:
|
|
return parentMainAxisPosition - childParentData.layoutOffset;
|
|
case AxisDirection.up:
|
|
return (size.height - parentMainAxisPosition) - childParentData.layoutOffset;
|
|
case AxisDirection.left:
|
|
return (size.width - parentMainAxisPosition) - childParentData.layoutOffset;
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
@override
|
|
int get indexOfFirstChild => 0;
|
|
|
|
@override
|
|
String labelForChild(int index) => 'child $index';
|
|
|
|
@override
|
|
Iterable<RenderSliver> get childrenInPaintOrder sync* {
|
|
RenderSliver child = firstChild;
|
|
while (child != null) {
|
|
yield child;
|
|
child = childAfter(child);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Iterable<RenderSliver> get childrenInHitTestOrder sync* {
|
|
RenderSliver child = lastChild;
|
|
while (child != null) {
|
|
yield child;
|
|
child = childBefore(child);
|
|
}
|
|
}
|
|
}
|