// 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:flutter/semantics.dart'; import 'package:vector_math/vector_math_64.dart'; import 'binding.dart'; import 'box.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 extends RenderObject { // This class is intended to be used as an interface with the implements // keyword, and should not be extended directly. 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; } 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); /// The default value for the cache extent of the viewport. /// /// See also: /// /// * [RenderViewportBase.cacheExtent] for a definition of the cache extent. @protected static const double defaultCacheExtent = 250.0; } /// 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, }) : assert(axisDirection != null), assert(crossAxisDirection != null), assert(offset != null), assert(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)), _axisDirection = axisDirection, _crossAxisDirection = crossAxisDirection, _offset = offset, _cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent; @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(); } @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 new FlutterError( '$runtimeType does not support returning intrinsic dimensions.\n' 'Calculating the intrinsic dimensions would require instantiating every child of ' 'the viewport, which defeats the point of viewports being lazy.\n' '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; 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 corectedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset); final double cacheExtentCorrection = cacheOrigin - corectedCacheOrigin; assert(sliverScrollOffset >= corectedCacheOrigin.abs()); assert(corectedCacheOrigin <= 0.0); assert(sliverScrollOffset >= 0.0); assert(cacheExtentCorrection <= 0.0); child.layout(new SliverConstraints( axisDirection: axisDirection, growthDirection: growthDirection, userScrollDirection: adjustedUserScrollDirection, scrollOffset: sliverScrollOffset, 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: corectedCacheOrigin, ), 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; layoutOffset += childLayoutGeometry.layoutExtent; if (childLayoutGeometry.cacheExtent != 0.0) { remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection; cacheOrigin = math.min(corectedCacheOrigin + 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; if (child.constraints.overlap == 0) { 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 new Rect.fromLTRB(left, top, right, bottom); } @override Rect describeSemanticsClip(RenderSliver child) { assert (axis != null); switch (axis) { case Axis.vertical: return new Rect.fromLTRB( semanticBounds.left, semanticBounds.top - cacheExtent, semanticBounds.right, semanticBounds.bottom + cacheExtent, ); case Axis.horizontal: return new Rect.fromLTRB( semanticBounds.left - cacheExtent, semanticBounds.top, semanticBounds.right + cacheExtent, 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 (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, { 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); 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: 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: leadingScrollOffset = bounds.left; targetMainAxisExtent = bounds.width; break; case AxisDirection.down: leadingScrollOffset = bounds.top; 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; } 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; 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; } return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; } /// 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 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 debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(new EnumProperty<AxisDirection>('axisDirection', axisDirection)); properties.add(new EnumProperty<AxisDirection>('crossAxisDirection', crossAxisDirection)); properties.add(new 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 child]) { // Logic duplicated in [_RenderSingleChildViewport.showOnScreen]. if (child != null) { // TODO(goderbauer): Don't scroll if it is already visible. // TODO(goderbauer): Don't guess if we need to align at leading or trailing edge. // Move viewport the smallest distance to bring [child] on screen. final double leadingEdgeOffset = getOffsetToReveal(child, 0.0); final double trailingEdgeOffset = getOffsetToReveal(child, 1.0); final double currentOffset = offset.pixels; // TODO(goderbauer): Don't scroll if that puts us outside of viewport's bounds. if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) { offset.jumpTo(leadingEdgeOffset); } else { offset.jumpTo(trailingEdgeOffset); } } // Make sure the viewport itself is on screen. super.showOnScreen(); } } /// 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, }) : assert(anchor != null), assert(anchor >= 0.0 && anchor <= 1.0), _anchor = anchor, _center = center, super(axisDirection: axisDirection, crossAxisDirection: crossAxisDirection, offset: offset, cacheExtent: cacheExtent) { 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 = const 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 = const SemanticsTag('RenderViewport.excludeFromScrolling'); @override void setupParentData(RenderObject child) { if (child.parentData is! SliverPhysicalContainerParentData) child.parentData = new 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. /// /// 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 new FlutterError( 'Vertical viewport was given unbounded height.\n' '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.\n' '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 new 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 new FlutterError( 'Horizontal viewport was given unbounded width.\n' '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.\n' '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 new 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 _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). final double centerOffset = mainAxisExtent * anchor - correctedOffset; final double reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent); final double forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent); final double fullCacheExtent = mainAxisExtent + 2 * cacheExtent; final double centerCacheOffset = centerOffset + cacheExtent; final double reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent); final double forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent); 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(-cacheExtent, 0.0), ); 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(-cacheExtent, 0.0), ); } @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; 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 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; 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 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(new 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 = new SliverLogicalContainerParentData(); } @override bool debugThrowIfNotCheckingIntrinsics() { assert(() { if (!RenderObject.debugCheckingIntrinsics) { throw new FlutterError( '$runtimeType does not support returning intrinsic dimensions.\n' 'Calculating the intrinsic dimensions would require instantiating every child of ' 'the viewport, which defeats the point of viewports being lazy.\n' '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() { 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 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; 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 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); 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 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); } } }