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