| // 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/gestures.dart' show DragStartBehavior; |
| import 'package:flutter/rendering.dart'; |
| |
| import 'basic.dart'; |
| import 'focus_manager.dart'; |
| import 'focus_scope.dart'; |
| import 'framework.dart'; |
| import 'notification_listener.dart'; |
| import 'primary_scroll_controller.dart'; |
| import 'scroll_controller.dart'; |
| import 'scroll_notification.dart'; |
| import 'scroll_physics.dart'; |
| import 'scroll_view.dart'; |
| import 'scrollable.dart'; |
| |
| /// A box in which a single widget can be scrolled. |
| /// |
| /// This widget is useful when you have a single box that will normally be |
| /// entirely visible, for example a clock face in a time picker, but you need to |
| /// make sure it can be scrolled if the container gets too small in one axis |
| /// (the scroll direction). |
| /// |
| /// It is also useful if you need to shrink-wrap in both axes (the main |
| /// scrolling direction as well as the cross axis), as one might see in a dialog |
| /// or pop-up menu. In that case, you might pair the [SingleChildScrollView] |
| /// with a [ListBody] child. |
| /// |
| /// When you have a list of children and do not require cross-axis |
| /// shrink-wrapping behavior, for example a scrolling list that is always the |
| /// width of the screen, consider [ListView], which is vastly more efficient |
| /// than a [SingleChildScrollView] containing a [ListBody] or [Column] with |
| /// many children. |
| /// |
| /// ## Sample code: Using [SingleChildScrollView] with a [Column] |
| /// |
| /// Sometimes a layout is designed around the flexible properties of a |
| /// [Column], but there is the concern that in some cases, there might not |
| /// be enough room to see the entire contents. This could be because some |
| /// devices have unusually small screens, or because the application can |
| /// be used in landscape mode where the aspect ratio isn't what was |
| /// originally envisioned, or because the application is being shown in a |
| /// small window in split-screen mode. In any case, as a result, it might |
| /// make sense to wrap the layout in a [SingleChildScrollView]. |
| /// |
| /// Simply doing so, however, usually results in a conflict between the [Column], |
| /// which typically tries to grow as big as it can, and the [SingleChildScrollView], |
| /// which provides its children with an infinite amount of space. |
| /// |
| /// To resolve this apparent conflict, there are a couple of techniques, as |
| /// discussed below. These techniques should only be used when the content is |
| /// normally expected to fit on the screen, so that the lazy instantiation of |
| /// a sliver-based [ListView] or [CustomScrollView] is not expected to provide |
| /// any performance benefit. If the viewport is expected to usually contain |
| /// content beyond the dimensions of the screen, then [SingleChildScrollView] |
| /// would be very expensive. |
| /// |
| /// ### Centering, spacing, or aligning fixed-height content |
| /// |
| /// If the content has fixed (or intrinsic) dimensions but needs to be spaced out, |
| /// centered, or otherwise positioned using the [Flex] layout model of a [Column], |
| /// the following technique can be used to provide the [Column] with a minimum |
| /// dimension while allowing it to shrink-wrap the contents when there isn't enough |
| /// room to apply these spacing or alignment needs. |
| /// |
| /// A [LayoutBuilder] is used to obtain the size of the viewport (implicitly via |
| /// the constraints that the [SingleChildScrollView] sees, since viewports |
| /// typically grow to fit their maximum height constraint). Then, inside the |
| /// scroll view, a [ConstrainedBox] is used to set the minimum height of the |
| /// [Column]. |
| /// |
| /// The [Column] has no [Expanded] children, so rather than take on the infinite |
| /// height from its [BoxConstraints.maxHeight], (the viewport provides no maximum height |
| /// constraint), it automatically tries to shrink to fit its children. It cannot |
| /// be smaller than its [BoxConstraints.minHeight], though, and It therefore |
| /// becomes the bigger of the minimum height provided by the |
| /// [ConstrainedBox] and the sum of the heights of the children. |
| /// |
| /// If the children aren't enough to fit that minimum size, the [Column] ends up |
| /// with some remaining space to allocate as specified by its |
| /// [Column.mainAxisAlignment] argument. |
| /// |
| /// {@tool dartpad} |
| /// In this example, the children are spaced out equally, unless there's no more |
| /// room, in which case they stack vertically and scroll. |
| /// |
| /// When using this technique, [Expanded] and [Flexible] are not useful, because |
| /// in both cases the "available space" is infinite (since this is in a viewport). |
| /// The next section describes a technique for providing a maximum height constraint. |
| /// |
| /// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart ** |
| /// {@end-tool} |
| /// |
| /// ### Expanding content to fit the viewport |
| /// |
| /// The following example builds on the previous one. In addition to providing a |
| /// minimum dimension for the child [Column], an [IntrinsicHeight] widget is used |
| /// to force the column to be exactly as big as its contents. This constraint |
| /// combines with the [ConstrainedBox] constraints discussed previously to ensure |
| /// that the column becomes either as big as viewport, or as big as the contents, |
| /// whichever is biggest. |
| /// |
| /// Both constraints must be used to get the desired effect. If only the |
| /// [IntrinsicHeight] was specified, then the column would not grow to fit the |
| /// entire viewport when its children were smaller than the whole screen. If only |
| /// the size of the viewport was used, then the [Column] would overflow if the |
| /// children were bigger than the viewport. |
| /// |
| /// The widget that is to grow to fit the remaining space so provided is wrapped |
| /// in an [Expanded] widget. |
| /// |
| /// This technique is quite expensive, as it more or less requires that the contents |
| /// of the viewport be laid out twice (once to find their intrinsic dimensions, and |
| /// once to actually lay them out). The number of widgets within the column should |
| /// therefore be kept small. Alternatively, subsets of the children that have known |
| /// dimensions can be wrapped in a [SizedBox] that has tight vertical constraints, |
| /// so that the intrinsic sizing algorithm can short-circuit the computation when it |
| /// reaches those parts of the subtree. |
| /// |
| /// {@tool dartpad} |
| /// In this example, the column becomes either as big as viewport, or as big as |
| /// the contents, whichever is biggest. |
| /// |
| /// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [ListView], which handles multiple children in a scrolling list. |
| /// * [GridView], which handles multiple children in a scrolling grid. |
| /// * [PageView], for a scrollable that works page by page. |
| /// * [Scrollable], which handles arbitrary scrolling effects. |
| class SingleChildScrollView extends StatelessWidget { |
| /// Creates a box in which a single widget can be scrolled. |
| const SingleChildScrollView({ |
| super.key, |
| this.scrollDirection = Axis.vertical, |
| this.reverse = false, |
| this.padding, |
| this.primary, |
| this.physics, |
| this.controller, |
| this.child, |
| this.dragStartBehavior = DragStartBehavior.start, |
| this.clipBehavior = Clip.hardEdge, |
| this.restorationId, |
| this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, |
| }) : assert(scrollDirection != null), |
| assert(dragStartBehavior != null), |
| assert(clipBehavior != null), |
| assert( |
| !(controller != null && (primary ?? false)), |
| 'Primary ScrollViews obtain their ScrollController via inheritance ' |
| 'from a PrimaryScrollController widget. You cannot both set primary to ' |
| 'true and pass an explicit controller.', |
| ); |
| |
| /// The axis along which the scroll view scrolls. |
| /// |
| /// Defaults to [Axis.vertical]. |
| final Axis scrollDirection; |
| |
| /// Whether the scroll view scrolls in the reading direction. |
| /// |
| /// For example, if the reading direction is left-to-right and |
| /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from |
| /// left to right when [reverse] is false and from right to left when |
| /// [reverse] is true. |
| /// |
| /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view |
| /// scrolls from top to bottom when [reverse] is false and from bottom to top |
| /// when [reverse] is true. |
| /// |
| /// Defaults to false. |
| final bool reverse; |
| |
| /// The amount of space by which to inset the child. |
| final EdgeInsetsGeometry? padding; |
| |
| /// An object that can be used to control the position to which this scroll |
| /// view is scrolled. |
| /// |
| /// Must be null if [primary] is true. |
| /// |
| /// A [ScrollController] serves several purposes. It can be used to control |
| /// the initial scroll position (see [ScrollController.initialScrollOffset]). |
| /// It can be used to control whether the scroll view should automatically |
| /// save and restore its scroll position in the [PageStorage] (see |
| /// [ScrollController.keepScrollOffset]). It can be used to read the current |
| /// scroll position (see [ScrollController.offset]), or change it (see |
| /// [ScrollController.animateTo]). |
| final ScrollController? controller; |
| |
| /// {@macro flutter.widgets.scroll_view.primary} |
| final bool? primary; |
| |
| /// How the scroll view should respond to user input. |
| /// |
| /// For example, determines how the scroll view continues to animate after the |
| /// user stops dragging the scroll view. |
| /// |
| /// Defaults to matching platform conventions. |
| final ScrollPhysics? physics; |
| |
| /// The widget that scrolls. |
| /// |
| /// {@macro flutter.widgets.ProxyWidget.child} |
| final Widget? child; |
| |
| /// {@macro flutter.widgets.scrollable.dragStartBehavior} |
| final DragStartBehavior dragStartBehavior; |
| |
| /// {@macro flutter.material.Material.clipBehavior} |
| /// |
| /// Defaults to [Clip.hardEdge]. |
| final Clip clipBehavior; |
| |
| /// {@macro flutter.widgets.scrollable.restorationId} |
| final String? restorationId; |
| |
| /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} |
| final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; |
| |
| AxisDirection _getDirection(BuildContext context) { |
| return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final AxisDirection axisDirection = _getDirection(context); |
| Widget? contents = child; |
| if (padding != null) { |
| contents = Padding(padding: padding!, child: contents); |
| } |
| final bool effectivePrimary = primary |
| ?? controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection); |
| |
| final ScrollController? scrollController = effectivePrimary |
| ? PrimaryScrollController.of(context) |
| : controller; |
| |
| Widget scrollable = Scrollable( |
| dragStartBehavior: dragStartBehavior, |
| axisDirection: axisDirection, |
| controller: scrollController, |
| physics: physics, |
| restorationId: restorationId, |
| viewportBuilder: (BuildContext context, ViewportOffset offset) { |
| return _SingleChildViewport( |
| axisDirection: axisDirection, |
| offset: offset, |
| clipBehavior: clipBehavior, |
| child: contents, |
| ); |
| }, |
| ); |
| |
| if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { |
| scrollable = NotificationListener<ScrollUpdateNotification>( |
| child: scrollable, |
| onNotification: (ScrollUpdateNotification notification) { |
| final FocusScopeNode focusNode = FocusScope.of(context); |
| if (notification.dragDetails != null && focusNode.hasFocus) { |
| focusNode.unfocus(); |
| } |
| return false; |
| }, |
| ); |
| } |
| |
| return effectivePrimary && scrollController != null |
| // Further descendant ScrollViews will not inherit the same |
| // PrimaryScrollController |
| ? PrimaryScrollController.none(child: scrollable) |
| : scrollable; |
| } |
| } |
| |
| class _SingleChildViewport extends SingleChildRenderObjectWidget { |
| const _SingleChildViewport({ |
| this.axisDirection = AxisDirection.down, |
| required this.offset, |
| super.child, |
| required this.clipBehavior, |
| }) : assert(axisDirection != null), |
| assert(clipBehavior != null); |
| |
| final AxisDirection axisDirection; |
| final ViewportOffset offset; |
| final Clip clipBehavior; |
| |
| @override |
| _RenderSingleChildViewport createRenderObject(BuildContext context) { |
| return _RenderSingleChildViewport( |
| axisDirection: axisDirection, |
| offset: offset, |
| clipBehavior: clipBehavior, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderSingleChildViewport renderObject) { |
| // Order dependency: The offset setter reads the axis direction. |
| renderObject |
| ..axisDirection = axisDirection |
| ..offset = offset |
| ..clipBehavior = clipBehavior; |
| } |
| |
| @override |
| SingleChildRenderObjectElement createElement() { |
| return _SingleChildViewportElement(this); |
| } |
| } |
| |
| class _SingleChildViewportElement extends SingleChildRenderObjectElement with NotifiableElementMixin, ViewportElementMixin { |
| _SingleChildViewportElement(_SingleChildViewport super.widget); |
| } |
| |
| class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport { |
| _RenderSingleChildViewport({ |
| AxisDirection axisDirection = AxisDirection.down, |
| required ViewportOffset offset, |
| double cacheExtent = RenderAbstractViewport.defaultCacheExtent, |
| RenderBox? child, |
| required Clip clipBehavior, |
| }) : assert(axisDirection != null), |
| assert(offset != null), |
| assert(cacheExtent != null), |
| assert(clipBehavior != null), |
| _axisDirection = axisDirection, |
| _offset = offset, |
| _cacheExtent = cacheExtent, |
| _clipBehavior = clipBehavior { |
| this.child = child; |
| } |
| |
| AxisDirection get axisDirection => _axisDirection; |
| AxisDirection _axisDirection; |
| set axisDirection(AxisDirection value) { |
| assert(value != null); |
| if (value == _axisDirection) { |
| return; |
| } |
| _axisDirection = value; |
| markNeedsLayout(); |
| } |
| |
| Axis get axis => axisDirectionToAxis(axisDirection); |
| |
| ViewportOffset get offset => _offset; |
| ViewportOffset _offset; |
| set offset(ViewportOffset value) { |
| assert(value != null); |
| if (value == _offset) { |
| return; |
| } |
| if (attached) { |
| _offset.removeListener(_hasScrolled); |
| } |
| _offset = value; |
| if (attached) { |
| _offset.addListener(_hasScrolled); |
| } |
| markNeedsLayout(); |
| } |
| |
| /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} |
| double get cacheExtent => _cacheExtent; |
| double _cacheExtent; |
| set cacheExtent(double value) { |
| assert(value != null); |
| if (value == _cacheExtent) { |
| return; |
| } |
| _cacheExtent = value; |
| markNeedsLayout(); |
| } |
| |
| /// {@macro flutter.material.Material.clipBehavior} |
| /// |
| /// Defaults to [Clip.none], and must not be null. |
| Clip get clipBehavior => _clipBehavior; |
| Clip _clipBehavior = Clip.none; |
| set clipBehavior(Clip value) { |
| assert(value != null); |
| if (value != _clipBehavior) { |
| _clipBehavior = value; |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| } |
| |
| void _hasScrolled() { |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| @override |
| void setupParentData(RenderObject child) { |
| // We don't actually use the offset argument in BoxParentData, so let's |
| // avoid allocating it at all. |
| if (child.parentData is! ParentData) { |
| child.parentData = ParentData(); |
| } |
| } |
| |
| @override |
| void attach(PipelineOwner owner) { |
| super.attach(owner); |
| _offset.addListener(_hasScrolled); |
| } |
| |
| @override |
| void detach() { |
| _offset.removeListener(_hasScrolled); |
| super.detach(); |
| } |
| |
| @override |
| bool get isRepaintBoundary => true; |
| |
| double get _viewportExtent { |
| assert(hasSize); |
| switch (axis) { |
| case Axis.horizontal: |
| return size.width; |
| case Axis.vertical: |
| return size.height; |
| } |
| } |
| |
| double get _minScrollExtent { |
| assert(hasSize); |
| return 0.0; |
| } |
| |
| double get _maxScrollExtent { |
| assert(hasSize); |
| if (child == null) { |
| return 0.0; |
| } |
| switch (axis) { |
| case Axis.horizontal: |
| return math.max(0.0, child!.size.width - size.width); |
| case Axis.vertical: |
| return math.max(0.0, child!.size.height - size.height); |
| } |
| } |
| |
| BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
| switch (axis) { |
| case Axis.horizontal: |
| return constraints.heightConstraints(); |
| case Axis.vertical: |
| return constraints.widthConstraints(); |
| } |
| } |
| |
| @override |
| double computeMinIntrinsicWidth(double height) { |
| if (child != null) { |
| return child!.getMinIntrinsicWidth(height); |
| } |
| return 0.0; |
| } |
| |
| @override |
| double computeMaxIntrinsicWidth(double height) { |
| if (child != null) { |
| return child!.getMaxIntrinsicWidth(height); |
| } |
| return 0.0; |
| } |
| |
| @override |
| double computeMinIntrinsicHeight(double width) { |
| if (child != null) { |
| return child!.getMinIntrinsicHeight(width); |
| } |
| return 0.0; |
| } |
| |
| @override |
| double computeMaxIntrinsicHeight(double width) { |
| if (child != null) { |
| return child!.getMaxIntrinsicHeight(width); |
| } |
| return 0.0; |
| } |
| |
| // We don't override computeDistanceToActualBaseline(), because we |
| // want the default behavior (returning null). Otherwise, as you |
| // scroll, it would shift in its parent if the parent was baseline-aligned, |
| // which makes no sense. |
| |
| @override |
| Size computeDryLayout(BoxConstraints constraints) { |
| if (child == null) { |
| return constraints.smallest; |
| } |
| final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints)); |
| return constraints.constrain(childSize); |
| } |
| |
| @override |
| void performLayout() { |
| final BoxConstraints constraints = this.constraints; |
| if (child == null) { |
| size = constraints.smallest; |
| } else { |
| child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
| size = constraints.constrain(child!.size); |
| } |
| |
| offset.applyViewportDimension(_viewportExtent); |
| offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); |
| } |
| |
| Offset get _paintOffset => _paintOffsetForPosition(offset.pixels); |
| |
| Offset _paintOffsetForPosition(double position) { |
| assert(axisDirection != null); |
| switch (axisDirection) { |
| case AxisDirection.up: |
| return Offset(0.0, position - child!.size.height + size.height); |
| case AxisDirection.down: |
| return Offset(0.0, -position); |
| case AxisDirection.left: |
| return Offset(position - child!.size.width + size.width, 0.0); |
| case AxisDirection.right: |
| return Offset(-position, 0.0); |
| } |
| } |
| |
| bool _shouldClipAtPaintOffset(Offset paintOffset) { |
| assert(child != null); |
| switch (clipBehavior) { |
| case Clip.none: |
| return false; |
| case Clip.hardEdge: |
| case Clip.antiAlias: |
| case Clip.antiAliasWithSaveLayer: |
| return paintOffset.dx < 0 || |
| paintOffset.dy < 0 || |
| paintOffset.dx + child!.size.width > size.width || |
| paintOffset.dy + child!.size.height > size.height; |
| } |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (child != null) { |
| final Offset paintOffset = _paintOffset; |
| |
| void paintContents(PaintingContext context, Offset offset) { |
| context.paintChild(child!, offset + paintOffset); |
| } |
| |
| if (_shouldClipAtPaintOffset(paintOffset)) { |
| _clipRectLayer.layer = context.pushClipRect( |
| needsCompositing, |
| offset, |
| Offset.zero & size, |
| paintContents, |
| clipBehavior: clipBehavior, |
| oldLayer: _clipRectLayer.layer, |
| ); |
| } else { |
| _clipRectLayer.layer = null; |
| paintContents(context, offset); |
| } |
| } |
| } |
| |
| final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
| |
| @override |
| void dispose() { |
| _clipRectLayer.layer = null; |
| super.dispose(); |
| } |
| |
| @override |
| void applyPaintTransform(RenderBox child, Matrix4 transform) { |
| final Offset paintOffset = _paintOffset; |
| transform.translate(paintOffset.dx, paintOffset.dy); |
| } |
| |
| @override |
| Rect? describeApproximatePaintClip(RenderObject? child) { |
| if (child != null && _shouldClipAtPaintOffset(_paintOffset)) { |
| return Offset.zero & size; |
| } |
| return null; |
| } |
| |
| @override |
| bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
| if (child != null) { |
| return result.addWithPaintOffset( |
| offset: _paintOffset, |
| position: position, |
| hitTest: (BoxHitTestResult result, Offset transformed) { |
| assert(transformed == position + -_paintOffset); |
| return child!.hitTest(result, position: transformed); |
| }, |
| ); |
| } |
| return false; |
| } |
| |
| @override |
| RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) { |
| rect ??= target.paintBounds; |
| if (target is! RenderBox) { |
| return RevealedOffset(offset: offset.pixels, rect: rect); |
| } |
| |
| final RenderBox targetBox = target; |
| final Matrix4 transform = targetBox.getTransformTo(child); |
| final Rect bounds = MatrixUtils.transformRect(transform, rect); |
| final Size contentSize = child!.size; |
| |
| final double leadingScrollOffset; |
| final double targetMainAxisExtent; |
| final double mainAxisExtent; |
| |
| assert(axisDirection != null); |
| switch (axisDirection) { |
| case AxisDirection.up: |
| mainAxisExtent = size.height; |
| leadingScrollOffset = contentSize.height - bounds.bottom; |
| targetMainAxisExtent = bounds.height; |
| break; |
| case AxisDirection.right: |
| mainAxisExtent = size.width; |
| leadingScrollOffset = bounds.left; |
| targetMainAxisExtent = bounds.width; |
| break; |
| case AxisDirection.down: |
| mainAxisExtent = size.height; |
| leadingScrollOffset = bounds.top; |
| targetMainAxisExtent = bounds.height; |
| break; |
| case AxisDirection.left: |
| mainAxisExtent = size.width; |
| leadingScrollOffset = contentSize.width - bounds.right; |
| targetMainAxisExtent = bounds.width; |
| break; |
| } |
| |
| final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; |
| final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset)); |
| return RevealedOffset(offset: targetOffset, rect: targetRect); |
| } |
| |
| @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, |
| ); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<Offset>('offset', _paintOffset)); |
| } |
| |
| @override |
| Rect describeSemanticsClip(RenderObject child) { |
| assert(axis != null); |
| switch (axis) { |
| case Axis.vertical: |
| return Rect.fromLTRB( |
| semanticBounds.left, |
| semanticBounds.top - cacheExtent, |
| semanticBounds.right, |
| semanticBounds.bottom + cacheExtent, |
| ); |
| case Axis.horizontal: |
| return Rect.fromLTRB( |
| semanticBounds.left - cacheExtent, |
| semanticBounds.top, |
| semanticBounds.right + cacheExtent, |
| semanticBounds.bottom, |
| ); |
| } |
| } |
| } |