Commit addcb1f7 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Move RenderViewport to its own file (#8310)

It doesn't need to be in the same file as RenderSliver.
parent 58898636
......@@ -54,6 +54,7 @@ export 'src/rendering/table.dart';
export 'src/rendering/tweens.dart';
export 'src/rendering/view.dart';
export 'src/rendering/viewport_offset.dart';
export 'src/rendering/viewport.dart';
export 'package:flutter/foundation.dart' show
VoidCallback,
......
......@@ -1174,1002 +1174,6 @@ abstract class RenderSliverHelpers implements RenderSliver {
}
}
// THE MAIN VIEWPORT CLASS
// Transitions from the RenderBox world to the RenderSliver world.
/// 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 implements RenderObject {
/// Returns the [RenderAbstractViewport] that most closely 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;
}
return null;
}
/// Returns the offset that would be needed to reveal the target render 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 and there must not be any other
/// [RenderAbstractViewport] objects between the target and this object.
double getOffsetToReveal(RenderObject target, double alignment);
}
typedef RenderSliver _Advancer(RenderSliver child);
// ///
// /// 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 {
RenderViewportBase({
AxisDirection axisDirection: AxisDirection.down,
@required ViewportOffset offset,
}) : _axisDirection = axisDirection,
_offset = offset {
assert(axisDirection != null);
assert(offset != null);
}
AxisDirection get axisDirection => _axisDirection;
AxisDirection _axisDirection;
set axisDirection(AxisDirection value) {
assert(value != null);
if (value == _axisDirection)
return;
_axisDirection = value;
markNeedsLayout();
}
Axis get axis => axisDirectionToAxis(axisDirection);
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();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(markNeedsLayout);
}
@override
void detach() {
_offset.removeListener(markNeedsLayout);
super.detach();
}
@override
bool get isRepaintBoundary => true;
@protected
double layoutOneSide(
RenderSliver child,
double scrollOffset,
double layoutOffset,
double remainingPaintExtent,
double mainAxisExtent,
double crossAxisExtent,
GrowthDirection growthDirection,
_Advancer advance,
) {
assert(scrollOffset.isFinite);
assert(scrollOffset >= 0.0);
ScrollDirection adjustedUserScrollDirection;
switch (growthDirection) {
case GrowthDirection.forward:
adjustedUserScrollDirection = offset.userScrollDirection;
break;
case GrowthDirection.reverse:
switch (offset.userScrollDirection) {
case ScrollDirection.forward:
adjustedUserScrollDirection = ScrollDirection.reverse;
break;
case ScrollDirection.reverse:
adjustedUserScrollDirection = ScrollDirection.forward;
break;
case ScrollDirection.idle:
adjustedUserScrollDirection = ScrollDirection.idle;
break;
}
break;
}
assert(adjustedUserScrollDirection != null);
double maxPaintOffset = layoutOffset;
double initialLayoutOffset = layoutOffset;
while (child != null) {
assert(scrollOffset >= 0.0);
child.layout(new SliverConstraints(
axisDirection: axisDirection,
growthDirection: growthDirection,
userScrollDirection: adjustedUserScrollDirection,
scrollOffset: scrollOffset,
overlap: maxPaintOffset - layoutOffset,
remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
crossAxisExtent: crossAxisExtent,
viewportMainAxisExtent: mainAxisExtent,
), parentUsesSize: true);
// collect the child's objects
final SliverGeometry childLayoutGeometry = child.geometry;
assert(childLayoutGeometry.debugAssertIsValid);
// first check that there isn't a correction to apply. If there is we'll
// have to start over.
if (childLayoutGeometry.scrollOffsetCorrection != 0.0)
return childLayoutGeometry.scrollOffsetCorrection;
// geometry
updateChildLayoutOffset(child, layoutOffset, growthDirection);
maxPaintOffset = math.max(layoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
scrollOffset -= childLayoutGeometry.scrollExtent;
layoutOffset += childLayoutGeometry.layoutExtent;
if (scrollOffset <= 0.0)
scrollOffset = 0.0;
updateOutOfBoundsData(growthDirection, childLayoutGeometry);
// move on to the next child
child = advance(child);
}
// we made it without a correction, whee!
return 0.0;
}
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
if (hasVisualOverflow) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
} else {
paintContents(context, offset);
}
}
@protected
void paintContents(PaintingContext context, Offset offset) {
for (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 = new 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 = new Size(child.constraints.crossAxisExtent, child.geometry.layoutExtent);
break;
case Axis.horizontal:
size = new 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(HitTestResult result, { Point position }) {
double mainAxisPosition, crossAxisPosition;
switch (axis) {
case Axis.vertical:
mainAxisPosition = position.y;
crossAxisPosition = position.x;
break;
case Axis.horizontal:
mainAxisPosition = position.x;
crossAxisPosition = position.y;
break;
}
assert(mainAxisPosition != null);
assert(crossAxisPosition != null);
for (RenderSliver child in childrenInHitTestOrder) {
if (child.geometry.visible && child.hitTest(
result,
mainAxisPosition: computeChildMainAxisPosition(child, mainAxisPosition),
crossAxisPosition: crossAxisPosition
)) {
return true;
}
}
return false;
}
@override
double getOffsetToReveal(RenderObject target, double alignment) {
double leadingScrollOffset;
double targetMainAxisExtent;
RenderObject descendant;
if (target is RenderBox) {
final RenderBox targetBox = target;
RenderBox pivot = targetBox;
while (pivot.parent is RenderBox)
pivot = pivot.parent;
assert(pivot.parent != null);
assert(pivot.parent != this);
assert(pivot != this);
final Matrix4 transform = targetBox.getTransformTo(pivot);
final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds);
target = pivot;
// TODO(abarth): Support other kinds of render objects besides slivers.
assert(target.parent is RenderSliver);
final RenderSliver pivotParent = target.parent;
final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
case AxisDirection.up:
leadingScrollOffset = pivot.size.height - bounds.bottom;
targetMainAxisExtent = bounds.height;
break;
case AxisDirection.right:
leadingScrollOffset = bounds.left;
targetMainAxisExtent = bounds.width;
break;
case AxisDirection.down:
leadingScrollOffset = bounds.top;
targetMainAxisExtent = bounds.height;
break;
case AxisDirection.left:
leadingScrollOffset = pivot.size.width - bounds.right;
targetMainAxisExtent = bounds.width;
break;
}
descendant = pivot;
} else if (target is RenderSliver) {
final RenderSliver targetSliver = target;
leadingScrollOffset = 0.0;
targetMainAxisExtent = targetSliver.geometry.scrollExtent;
descendant = targetSliver;
} else {
return offset.pixels;
}
// The child will be the topmost object before we get to the viewport.
RenderObject child = descendant;
while (child.parent is RenderSliver) {
final RenderSliver parent = child.parent;
leadingScrollOffset += parent.childScrollOffset(child);
child = parent;
}
assert(child.parent == this);
assert(child is RenderSliver);
final RenderSliver sliver = child;
leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
double mainAxisExtent;
switch (axis) {
case Axis.horizontal:
mainAxisExtent = size.width;
break;
case Axis.vertical:
mainAxisExtent = size.height;
break;
}
return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
}
@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 new Offset(0.0, size.height - (layoutOffset + child.geometry.paintExtent));
case AxisDirection.right:
return new Offset(layoutOffset, 0.0);
case AxisDirection.down:
return new Offset(0.0, layoutOffset);
case AxisDirection.left:
return new Offset(size.width - (layoutOffset + child.geometry.paintExtent), 0.0);
}
return null;
}
// TODO(ianh): semantics - shouldn't walk the invisible children
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
description.add('offset: $offset');
}
@override
String debugDescribeChildren(String prefix) {
if (firstChild == null)
return '$prefix\n';
int count = indexOfFirstChild();
String result = '$prefix \u2502\n';
RenderSliver child = firstChild;
while (child != lastChild) {
result += '${child.toStringDeep("$prefix \u251C\u2500${labelForChild(count)}: ", "$prefix \u2502")}';
count += 1;
child = childAfter(child);
}
assert(child == lastChild);
result += '${child.toStringDeep("$prefix \u2514\u2500${labelForChild(count)}: ", "$prefix ")}';
return result;
}
// API TO BE IMPLEMENTED BY SUBCLASSES
// setupParentData
// performLayout (and optionally sizedByParent and performResize)
@protected
bool get hasVisualOverflow;
@protected
void updateOutOfBoundsData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry);
@protected
void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection);
@protected
Offset paintOffsetOf(RenderSliver child);
@protected
double scrollOffsetOf(RenderSliver child, double scrollOffset);
// applyPaintTransform
/// 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);
@protected
int indexOfFirstChild();
@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;
}
// ///
// /// 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 ViewportOffset offset,
double anchor: 0.0,
List<RenderSliver> children,
RenderSliver center,
}) : _anchor = anchor,
_center = center,
super(axisDirection: axisDirection, offset: offset) {
assert(anchor != null);
assert(anchor >= 0.0 && anchor <= 1.0);
addAll(children);
if (center == null && firstChild != null)
_center = firstChild;
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalContainerParentData)
child.parentData = new SliverPhysicalContainerParentData();
}
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();
}
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(constraints.hasBoundedHeight && constraints.hasBoundedWidth);
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 _kMaxLayoutCycles = 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 < _kMaxLayoutCycles);
assert(() {
if (count >= _kMaxLayoutCycles) {
assert(count != 1);
throw new 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). The other two are that, but clamped to the visible
// region of the viewport.
final double centerOffset = mainAxisExtent * anchor - correctedOffset;
final double clampedForwardCenter = math.max(0.0, math.min(mainAxisExtent, centerOffset));
final double clampedReverseCenter = math.max(0.0, math.min(mainAxisExtent, mainAxisExtent - centerOffset));
// negative scroll offsets
double result = layoutOneSide(
childBefore(center),
math.max(mainAxisExtent, mainAxisExtent * anchor - correctedOffset) - mainAxisExtent,
clampedReverseCenter,
clampedForwardCenter,
mainAxisExtent,
crossAxisExtent,
GrowthDirection.reverse,
childBefore,
);
if (result != 0.0)
return -result;
// positive scroll offsets
return layoutOneSide(
center,
math.max(0.0, correctedOffset - mainAxisExtent * anchor),
clampedForwardCenter,
clampedReverseCenter,
mainAxisExtent,
crossAxisExtent,
GrowthDirection.forward,
childAfter,
);
}
@override
bool get hasVisualOverflow => _hasVisualOverflow;
@override
void updateOutOfBoundsData(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;
childParentData.paintOffset = computeAbsolutePaintOffset(child, layoutOffset, growthDirection);
}
@override
Offset paintOffsetOf(RenderSliver child) {
final SliverPhysicalParentData childParentData = child.parentData;
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
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
final SliverPhysicalParentData childParentData = child.parentData;
childParentData.applyPaintTransform(transform);
}
@override
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
assert(child != null);
assert(child.constraints != null);
final SliverPhysicalParentData childParentData = child.parentData;
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 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 debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('anchor: $anchor');
}
}
// ///
// /// 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 ViewportOffset offset,
List<RenderSliver> children,
}) : super(axisDirection: axisDirection, offset: offset) {
addAll(children);
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverLogicalContainerParentData)
child.parentData = new SliverLogicalContainerParentData();
}
// Out-of-band data computed during layout.
double _maxScrollExtent;
double _shrinkWrapExtent;
bool _hasVisualOverflow = false;
@override
void performLayout() {
if (firstChild == null) {
switch (axis) {
case Axis.vertical:
assert(constraints.hasBoundedWidth);
size = new Size(constraints.maxWidth, constraints.minHeight);
break;
case Axis.horizontal:
assert(constraints.hasBoundedHeight);
size = new 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) {
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 layoutOneSide(
firstChild,
math.max(0.0, correctedOffset),
0.0,
mainAxisExtent,
mainAxisExtent,
crossAxisExtent,
GrowthDirection.forward,
childAfter,
);
}
@override
bool get hasVisualOverflow => _hasVisualOverflow;
@override
void updateOutOfBoundsData(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;
childParentData.layoutOffset = layoutOffset;
}
@override
Offset paintOffsetOf(RenderSliver child) {
final SliverLogicalParentData childParentData = child.parentData;
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
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
final Offset offset = paintOffsetOf(child);
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;
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 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);
}
}
}
// ADAPTER FOR RENDER BOXES INSIDE SLIVERS
// Transitions from the RenderSliver world to the RenderBox world.
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'binding.dart';
import 'object.dart';
import 'sliver.dart';
import 'viewport_offset.dart';
/// 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 implements RenderObject {
/// Returns the [RenderAbstractViewport] that most closely 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;
}
return null;
}
/// Returns the offset that would be needed to reveal the target render 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 and there must not be any other
/// [RenderAbstractViewport] objects between the target and this object.
double getOffsetToReveal(RenderObject target, double alignment);
}
typedef RenderSliver _Advancer(RenderSliver child);
// ///
// /// 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 {
RenderViewportBase({
AxisDirection axisDirection: AxisDirection.down,
@required ViewportOffset offset,
}) : _axisDirection = axisDirection,
_offset = offset {
assert(axisDirection != null);
assert(offset != null);
}
AxisDirection get axisDirection => _axisDirection;
AxisDirection _axisDirection;
set axisDirection(AxisDirection value) {
assert(value != null);
if (value == _axisDirection)
return;
_axisDirection = value;
markNeedsLayout();
}
Axis get axis => axisDirectionToAxis(axisDirection);
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();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(markNeedsLayout);
}
@override
void detach() {
_offset.removeListener(markNeedsLayout);
super.detach();
}
@override
bool get isRepaintBoundary => true;
@protected
double layoutOneSide(
RenderSliver child,
double scrollOffset,
double layoutOffset,
double remainingPaintExtent,
double mainAxisExtent,
double crossAxisExtent,
GrowthDirection growthDirection,
_Advancer advance,
) {
assert(scrollOffset.isFinite);
assert(scrollOffset >= 0.0);
ScrollDirection adjustedUserScrollDirection;
switch (growthDirection) {
case GrowthDirection.forward:
adjustedUserScrollDirection = offset.userScrollDirection;
break;
case GrowthDirection.reverse:
switch (offset.userScrollDirection) {
case ScrollDirection.forward:
adjustedUserScrollDirection = ScrollDirection.reverse;
break;
case ScrollDirection.reverse:
adjustedUserScrollDirection = ScrollDirection.forward;
break;
case ScrollDirection.idle:
adjustedUserScrollDirection = ScrollDirection.idle;
break;
}
break;
}
assert(adjustedUserScrollDirection != null);
double maxPaintOffset = layoutOffset;
double initialLayoutOffset = layoutOffset;
while (child != null) {
assert(scrollOffset >= 0.0);
child.layout(new SliverConstraints(
axisDirection: axisDirection,
growthDirection: growthDirection,
userScrollDirection: adjustedUserScrollDirection,
scrollOffset: scrollOffset,
overlap: maxPaintOffset - layoutOffset,
remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
crossAxisExtent: crossAxisExtent,
viewportMainAxisExtent: mainAxisExtent,
), parentUsesSize: true);
// collect the child's objects
final SliverGeometry childLayoutGeometry = child.geometry;
assert(childLayoutGeometry.debugAssertIsValid);
// first check that there isn't a correction to apply. If there is we'll
// have to start over.
if (childLayoutGeometry.scrollOffsetCorrection != 0.0)
return childLayoutGeometry.scrollOffsetCorrection;
// geometry
updateChildLayoutOffset(child, layoutOffset, growthDirection);
maxPaintOffset = math.max(layoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
scrollOffset -= childLayoutGeometry.scrollExtent;
layoutOffset += childLayoutGeometry.layoutExtent;
if (scrollOffset <= 0.0)
scrollOffset = 0.0;
updateOutOfBoundsData(growthDirection, childLayoutGeometry);
// move on to the next child
child = advance(child);
}
// we made it without a correction, whee!
return 0.0;
}
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
if (hasVisualOverflow) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
} else {
paintContents(context, offset);
}
}
@protected
void paintContents(PaintingContext context, Offset offset) {
for (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 = new 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 = new Size(child.constraints.crossAxisExtent, child.geometry.layoutExtent);
break;
case Axis.horizontal:
size = new 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(HitTestResult result, { Point position }) {
double mainAxisPosition, crossAxisPosition;
switch (axis) {
case Axis.vertical:
mainAxisPosition = position.y;
crossAxisPosition = position.x;
break;
case Axis.horizontal:
mainAxisPosition = position.x;
crossAxisPosition = position.y;
break;
}
assert(mainAxisPosition != null);
assert(crossAxisPosition != null);
for (RenderSliver child in childrenInHitTestOrder) {
if (child.geometry.visible && child.hitTest(
result,
mainAxisPosition: computeChildMainAxisPosition(child, mainAxisPosition),
crossAxisPosition: crossAxisPosition
)) {
return true;
}
}
return false;
}
@override
double getOffsetToReveal(RenderObject target, double alignment) {
double leadingScrollOffset;
double targetMainAxisExtent;
RenderObject descendant;
if (target is RenderBox) {
final RenderBox targetBox = target;
RenderBox pivot = targetBox;
while (pivot.parent is RenderBox)
pivot = pivot.parent;
assert(pivot.parent != null);
assert(pivot.parent != this);
assert(pivot != this);
final Matrix4 transform = targetBox.getTransformTo(pivot);
final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds);
target = pivot;
// TODO(abarth): Support other kinds of render objects besides slivers.
assert(target.parent is RenderSliver);
final RenderSliver pivotParent = target.parent;
final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
case AxisDirection.up:
leadingScrollOffset = pivot.size.height - bounds.bottom;
targetMainAxisExtent = bounds.height;
break;
case AxisDirection.right:
leadingScrollOffset = bounds.left;
targetMainAxisExtent = bounds.width;
break;
case AxisDirection.down:
leadingScrollOffset = bounds.top;
targetMainAxisExtent = bounds.height;
break;
case AxisDirection.left:
leadingScrollOffset = pivot.size.width - bounds.right;
targetMainAxisExtent = bounds.width;
break;
}
descendant = pivot;
} else if (target is RenderSliver) {
final RenderSliver targetSliver = target;
leadingScrollOffset = 0.0;
targetMainAxisExtent = targetSliver.geometry.scrollExtent;
descendant = targetSliver;
} else {
return offset.pixels;
}
// The child will be the topmost object before we get to the viewport.
RenderObject child = descendant;
while (child.parent is RenderSliver) {
final RenderSliver parent = child.parent;
leadingScrollOffset += parent.childScrollOffset(child);
child = parent;
}
assert(child.parent == this);
assert(child is RenderSliver);
final RenderSliver sliver = child;
leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
double mainAxisExtent;
switch (axis) {
case Axis.horizontal:
mainAxisExtent = size.width;
break;
case Axis.vertical:
mainAxisExtent = size.height;
break;
}
return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
}
@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 new Offset(0.0, size.height - (layoutOffset + child.geometry.paintExtent));
case AxisDirection.right:
return new Offset(layoutOffset, 0.0);
case AxisDirection.down:
return new Offset(0.0, layoutOffset);
case AxisDirection.left:
return new Offset(size.width - (layoutOffset + child.geometry.paintExtent), 0.0);
}
return null;
}
// TODO(ianh): semantics - shouldn't walk the invisible children
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
description.add('offset: $offset');
}
@override
String debugDescribeChildren(String prefix) {
if (firstChild == null)
return '$prefix\n';
int count = indexOfFirstChild();
String result = '$prefix \u2502\n';
RenderSliver child = firstChild;
while (child != lastChild) {
result += '${child.toStringDeep("$prefix \u251C\u2500${labelForChild(count)}: ", "$prefix \u2502")}';
count += 1;
child = childAfter(child);
}
assert(child == lastChild);
result += '${child.toStringDeep("$prefix \u2514\u2500${labelForChild(count)}: ", "$prefix ")}';
return result;
}
// API TO BE IMPLEMENTED BY SUBCLASSES
// setupParentData
// performLayout (and optionally sizedByParent and performResize)
@protected
bool get hasVisualOverflow;
@protected
void updateOutOfBoundsData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry);
@protected
void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection);
@protected
Offset paintOffsetOf(RenderSliver child);
@protected
double scrollOffsetOf(RenderSliver child, double scrollOffset);
// applyPaintTransform
/// 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);
@protected
int indexOfFirstChild();
@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;
}
// ///
// /// 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 ViewportOffset offset,
double anchor: 0.0,
List<RenderSliver> children,
RenderSliver center,
}) : _anchor = anchor,
_center = center,
super(axisDirection: axisDirection, offset: offset) {
assert(anchor != null);
assert(anchor >= 0.0 && anchor <= 1.0);
addAll(children);
if (center == null && firstChild != null)
_center = firstChild;
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalContainerParentData)
child.parentData = new SliverPhysicalContainerParentData();
}
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();
}
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(constraints.hasBoundedHeight && constraints.hasBoundedWidth);
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 _kMaxLayoutCycles = 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 < _kMaxLayoutCycles);
assert(() {
if (count >= _kMaxLayoutCycles) {
assert(count != 1);
throw new 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). The other two are that, but clamped to the visible
// region of the viewport.
final double centerOffset = mainAxisExtent * anchor - correctedOffset;
final double clampedForwardCenter = math.max(0.0, math.min(mainAxisExtent, centerOffset));
final double clampedReverseCenter = math.max(0.0, math.min(mainAxisExtent, mainAxisExtent - centerOffset));
// negative scroll offsets
double result = layoutOneSide(
childBefore(center),
math.max(mainAxisExtent, mainAxisExtent * anchor - correctedOffset) - mainAxisExtent,
clampedReverseCenter,
clampedForwardCenter,
mainAxisExtent,
crossAxisExtent,
GrowthDirection.reverse,
childBefore,
);
if (result != 0.0)
return -result;
// positive scroll offsets
return layoutOneSide(
center,
math.max(0.0, correctedOffset - mainAxisExtent * anchor),
clampedForwardCenter,
clampedReverseCenter,
mainAxisExtent,
crossAxisExtent,
GrowthDirection.forward,
childAfter,
);
}
@override
bool get hasVisualOverflow => _hasVisualOverflow;
@override
void updateOutOfBoundsData(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;
childParentData.paintOffset = computeAbsolutePaintOffset(child, layoutOffset, growthDirection);
}
@override
Offset paintOffsetOf(RenderSliver child) {
final SliverPhysicalParentData childParentData = child.parentData;
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
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
final SliverPhysicalParentData childParentData = child.parentData;
childParentData.applyPaintTransform(transform);
}
@override
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
assert(child != null);
assert(child.constraints != null);
final SliverPhysicalParentData childParentData = child.parentData;
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 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 debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('anchor: $anchor');
}
}
// ///
// /// 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 ViewportOffset offset,
List<RenderSliver> children,
}) : super(axisDirection: axisDirection, offset: offset) {
addAll(children);
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverLogicalContainerParentData)
child.parentData = new SliverLogicalContainerParentData();
}
// Out-of-band data computed during layout.
double _maxScrollExtent;
double _shrinkWrapExtent;
bool _hasVisualOverflow = false;
@override
void performLayout() {
if (firstChild == null) {
switch (axis) {
case Axis.vertical:
assert(constraints.hasBoundedWidth);
size = new Size(constraints.maxWidth, constraints.minHeight);
break;
case Axis.horizontal:
assert(constraints.hasBoundedHeight);
size = new 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) {
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 layoutOneSide(
firstChild,
math.max(0.0, correctedOffset),
0.0,
mainAxisExtent,
mainAxisExtent,
crossAxisExtent,
GrowthDirection.forward,
childAfter,
);
}
@override
bool get hasVisualOverflow => _hasVisualOverflow;
@override
void updateOutOfBoundsData(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;
childParentData.layoutOffset = layoutOffset;
}
@override
Offset paintOffsetOf(RenderSliver child) {
final SliverLogicalParentData childParentData = child.parentData;
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
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
final Offset offset = paintOffsetOf(child);
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;
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 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);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment