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