blob: 0424305f67786332e85d9a7937248fc309f856ae [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '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);
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);
}
}
}