single_child_scroll_view.dart 26.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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;

7
import 'package:flutter/gestures.dart' show DragStartBehavior;
8
import 'package:flutter/rendering.dart';
9 10

import 'basic.dart';
11 12
import 'focus_manager.dart';
import 'focus_scope.dart';
13
import 'framework.dart';
14
import 'notification_listener.dart';
15
import 'primary_scroll_controller.dart';
16
import 'scroll_controller.dart';
17
import 'scroll_notification.dart';
18
import 'scroll_physics.dart';
19
import 'scroll_view.dart';
20 21
import 'scrollable.dart';

22 23 24 25 26 27 28 29 30 31
/// 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]
32
/// with a [ListBody] child.
33 34 35 36
///
/// 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
37
/// that a [SingleChildScrollView] containing a [ListBody] or [Column] with
38 39
/// many children.
///
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
/// ## 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.
///
88
/// {@tool dartpad --template=stateless_widget_material}
89 90
/// In this example, the children are spaced out equally, unless there's no more
/// room, in which case they stack vertically and scroll.
91 92 93 94 95
///
/// 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.
///
96
/// ```dart
97 98
///  Widget build(BuildContext context) {
///    return DefaultTextStyle(
99
///      style: Theme.of(context).textTheme.bodyText2!,
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
///      child: LayoutBuilder(
///        builder: (BuildContext context, BoxConstraints viewportConstraints) {
///          return SingleChildScrollView(
///            child: ConstrainedBox(
///              constraints: BoxConstraints(
///                minHeight: viewportConstraints.maxHeight,
///              ),
///              child: Column(
///                mainAxisSize: MainAxisSize.min,
///                mainAxisAlignment: MainAxisAlignment.spaceAround,
///                children: <Widget>[
///                  Container(
///                    // A fixed-height child.
///                    color: const Color(0xffeeee00), // Yellow
///                    height: 120.0,
///                    alignment: Alignment.center,
///                    child: const Text('Fixed Height Content'),
///                  ),
///                  Container(
///                    // Another fixed-height child.
///                    color: const Color(0xff008000), // Green
///                    height: 120.0,
///                    alignment: Alignment.center,
///                    child: const Text('Fixed Height Content'),
///                  ),
///                ],
///              ),
///            ),
///          );
///        },
///      ),
///    );
///  }
133 134 135
/// ```
/// {@end-tool}
///
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
/// ### 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.
///
162
/// {@tool dartpad --template=stateless_widget_material}
163 164 165 166
/// In this example, the column becomes either as big as viewport, or as big as
/// the contents, whichever is biggest.
///
/// ```dart
167 168
///  Widget build(BuildContext context) {
///    return DefaultTextStyle(
169
///      style: Theme.of(context).textTheme.bodyText2!,
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
///      child: LayoutBuilder(
///        builder: (BuildContext context, BoxConstraints viewportConstraints) {
///          return SingleChildScrollView(
///            child: ConstrainedBox(
///              constraints: BoxConstraints(
///                minHeight: viewportConstraints.maxHeight,
///              ),
///              child: IntrinsicHeight(
///                child: Column(
///                  children: <Widget>[
///                    Container(
///                      // A fixed-height child.
///                      color: const Color(0xffeeee00), // Yellow
///                      height: 120.0,
///                      alignment: Alignment.center,
///                      child: const Text('Fixed Height Content'),
///                    ),
///                    Expanded(
///                      // A flexible child that will grow to fit the viewport but
///                      // still be at least as big as necessary to fit its contents.
///                      child: Container(
///                        color: const Color(0xffee0000), // Red
///                        height: 120.0,
///                        alignment: Alignment.center,
///                        child: const Text('Flexible Content'),
///                      ),
///                    ),
///                  ],
///                ),
///              ),
///            ),
///          );
///        },
///      ),
///    );
///  }
206 207 208
/// ```
/// {@end-tool}
///
209 210
/// See also:
///
211 212 213 214
///  * [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.
215
class SingleChildScrollView extends StatelessWidget {
216
  /// Creates a box in which a single widget can be scrolled.
217
  const SingleChildScrollView({
218
    Key? key,
219 220
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
221
    this.padding,
222
    bool? primary,
223 224
    this.physics,
    this.controller,
225
    this.child,
226
    this.dragStartBehavior = DragStartBehavior.start,
227
    this.clipBehavior = Clip.hardEdge,
228
    this.restorationId,
229
    this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
230
  }) : assert(scrollDirection != null),
231
       assert(dragStartBehavior != null),
232
       assert(clipBehavior != null),
233 234
       assert(!(controller != null && primary == true),
          'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
235
          'You cannot both set primary to true and pass an explicit controller.',
236
       ),
237
       primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical),
238
       super(key: key);
239

240 241 242
  /// The axis along which the scroll view scrolls.
  ///
  /// Defaults to [Axis.vertical].
243 244
  final Axis scrollDirection;

245 246 247 248 249 250 251
  /// 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.
  ///
252
  /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
253 254 255 256
  /// scrolls from top to bottom when [reverse] is false and from bottom to top
  /// when [reverse] is true.
  ///
  /// Defaults to false.
257 258
  final bool reverse;

259
  /// The amount of space by which to inset the child.
260
  final EdgeInsetsGeometry? padding;
261

262 263 264 265
  /// An object that can be used to control the position to which this scroll
  /// view is scrolled.
  ///
  /// Must be null if [primary] is true.
266 267 268 269 270 271 272 273
  ///
  /// 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]).
274
  final ScrollController? controller;
275

276 277 278
  /// Whether this is the primary scroll view associated with the parent
  /// [PrimaryScrollController].
  ///
279 280 281 282 283
  /// When true, the scroll view is used for default [ScrollAction]s. If a
  /// ScrollAction is not handled by an otherwise focused part of the application,
  /// the ScrollAction will be evaluated using this scroll view, for example,
  /// when executing [Shortcuts] key events like page up and down.
  ///
284 285 286
  /// On iOS, this identifies the scroll view that will scroll to top in
  /// response to a tap in the status bar.
  ///
287
  /// Defaults to true when [scrollDirection] is vertical and [controller] is
288
  /// not specified.
289 290
  final bool primary;

291 292 293 294 295 296
  /// 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.
297
  final ScrollPhysics? physics;
298

299
  /// The widget that scrolls.
300
  ///
301
  /// {@macro flutter.widgets.ProxyWidget.child}
302
  final Widget? child;
303

304 305 306
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

307
  /// {@macro flutter.material.Material.clipBehavior}
308 309 310 311
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

312
  /// {@macro flutter.widgets.scrollable.restorationId}
313
  final String? restorationId;
314

315 316 317
  /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior}
  final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;

318
  AxisDirection _getDirection(BuildContext context) {
319
    return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse);
320 321 322 323
  }

  @override
  Widget build(BuildContext context) {
324
    final AxisDirection axisDirection = _getDirection(context);
325
    Widget? contents = child;
326
    if (padding != null)
327 328
      contents = Padding(padding: padding!, child: contents);
    final ScrollController? scrollController = primary
329 330
        ? PrimaryScrollController.of(context)
        : controller;
331
    Widget scrollable = Scrollable(
332
      dragStartBehavior: dragStartBehavior,
333
      axisDirection: axisDirection,
334
      controller: scrollController,
335
      physics: physics,
336
      restorationId: restorationId,
337
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
338
        return _SingleChildViewport(
339 340
          axisDirection: axisDirection,
          offset: offset,
341
          clipBehavior: clipBehavior,
342
          child: contents,
343 344
        );
      },
345
    );
346 347 348 349 350 351 352 353 354 355 356 357 358 359

    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;
        },
      );
    }

360
    return primary && scrollController != null
361
      ? PrimaryScrollController.none(child: scrollable)
362
      : scrollable;
363 364 365 366
  }
}

class _SingleChildViewport extends SingleChildRenderObjectWidget {
367
  const _SingleChildViewport({
368
    Key? key,
369
    this.axisDirection = AxisDirection.down,
370 371 372
    required this.offset,
    Widget? child,
    required this.clipBehavior,
373
  }) : assert(axisDirection != null),
374
       assert(clipBehavior != null),
375
       super(key: key, child: child);
376 377 378

  final AxisDirection axisDirection;
  final ViewportOffset offset;
379
  final Clip clipBehavior;
380 381 382

  @override
  _RenderSingleChildViewport createRenderObject(BuildContext context) {
383
    return _RenderSingleChildViewport(
384 385
      axisDirection: axisDirection,
      offset: offset,
386
      clipBehavior: clipBehavior,
387 388 389 390 391 392 393 394
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSingleChildViewport renderObject) {
    // Order dependency: The offset setter reads the axis direction.
    renderObject
      ..axisDirection = axisDirection
395 396
      ..offset = offset
      ..clipBehavior = clipBehavior;
397 398 399
  }
}

400
class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport {
401
  _RenderSingleChildViewport({
402
    AxisDirection axisDirection = AxisDirection.down,
403
    required ViewportOffset offset,
404
    double cacheExtent = RenderAbstractViewport.defaultCacheExtent,
405 406
    RenderBox? child,
    required Clip clipBehavior,
407 408
  }) : assert(axisDirection != null),
       assert(offset != null),
409
       assert(cacheExtent != null),
410
       assert(clipBehavior != null),
411
       _axisDirection = axisDirection,
412
       _offset = offset,
413 414
       _cacheExtent = cacheExtent,
       _clipBehavior = clipBehavior {
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
    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)
437
      _offset.removeListener(_hasScrolled);
438 439
    _offset = value;
    if (attached)
440
      _offset.addListener(_hasScrolled);
441
    markNeedsLayout();
442 443
  }

444
  /// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
445 446 447 448 449 450 451 452 453 454
  double get cacheExtent => _cacheExtent;
  double _cacheExtent;
  set cacheExtent(double value) {
    assert(value != null);
    if (value == _cacheExtent)
      return;
    _cacheExtent = value;
    markNeedsLayout();
  }

455
  /// {@macro flutter.material.Material.clipBehavior}
456 457 458 459 460 461 462 463 464 465 466 467 468
  ///
  /// 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();
    }
  }

469 470 471 472 473
  void _hasScrolled() {
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

474 475 476 477 478
  @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)
479
      child.parentData = ParentData();
480 481 482 483 484
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
485
    _offset.addListener(_hasScrolled);
486 487 488 489
  }

  @override
  void detach() {
490
    _offset.removeListener(_hasScrolled);
491 492 493 494 495 496
    super.detach();
  }

  @override
  bool get isRepaintBoundary => true;

497
  double get _viewportExtent {
498 499 500 501
    assert(hasSize);
    switch (axis) {
      case Axis.horizontal:
        return size.width;
502 503
      case Axis.vertical:
        return size.height;
504 505 506 507 508 509 510 511 512 513 514 515
    }
  }

  double get _minScrollExtent {
    assert(hasSize);
    return 0.0;
  }

  double get _maxScrollExtent {
    assert(hasSize);
    if (child == null)
      return 0.0;
516 517
    switch (axis) {
      case Axis.horizontal:
518
        return math.max(0.0, child!.size.width - size.width);
519
      case Axis.vertical:
520
        return math.max(0.0, child!.size.height - size.height);
521
    }
522 523 524 525 526 527 528 529 530 531 532 533 534 535
  }

  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)
536
      return child!.getMinIntrinsicWidth(height);
537 538 539 540 541 542
    return 0.0;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    if (child != null)
543
      return child!.getMaxIntrinsicWidth(height);
544 545 546 547 548 549
    return 0.0;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    if (child != null)
550
      return child!.getMinIntrinsicHeight(width);
551 552 553 554 555 556
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    if (child != null)
557
      return child!.getMaxIntrinsicHeight(width);
558 559 560 561 562 563 564 565
    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.

566 567 568 569 570 571 572 573 574
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    if (child == null) {
      return constraints.smallest;
    }
    final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints));
    return constraints.constrain(childSize);
  }

575 576
  @override
  void performLayout() {
577
    final BoxConstraints constraints = this.constraints;
578 579 580
    if (child == null) {
      size = constraints.smallest;
    } else {
581 582
      child!.layout(_getInnerConstraints(constraints), parentUsesSize: true);
      size = constraints.constrain(child!.size);
583 584
    }

585
    offset.applyViewportDimension(_viewportExtent);
586 587 588
    offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
  }

589 590 591
  Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);

  Offset _paintOffsetForPosition(double position) {
592 593 594
    assert(axisDirection != null);
    switch (axisDirection) {
      case AxisDirection.up:
595
        return Offset(0.0, position - child!.size.height + size.height);
596
      case AxisDirection.down:
597
        return Offset(0.0, -position);
598
      case AxisDirection.left:
599
        return Offset(position - child!.size.width + size.width, 0.0);
600
      case AxisDirection.right:
601
        return Offset(-position, 0.0);
602 603 604 605 606
    }
  }

  bool _shouldClipAtPaintOffset(Offset paintOffset) {
    assert(child != null);
607
    return paintOffset.dx < 0 ||
608
      paintOffset.dy < 0 ||
609 610
      paintOffset.dx + child!.size.width > size.width ||
      paintOffset.dy + child!.size.height > size.height;
611 612 613 614 615
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
616
      final Offset paintOffset = _paintOffset;
617 618

      void paintContents(PaintingContext context, Offset offset) {
619
        context.paintChild(child!, offset + paintOffset);
620 621
      }

622
      if (_shouldClipAtPaintOffset(paintOffset) && clipBehavior != Clip.none) {
623
        _clipRectLayer.layer = context.pushClipRect(
624 625 626 627 628
          needsCompositing,
          offset,
          Offset.zero & size,
          paintContents,
          clipBehavior: clipBehavior,
629
          oldLayer: _clipRectLayer.layer,
630
        );
631
      } else {
632
        _clipRectLayer.layer = null;
633 634 635 636 637
        paintContents(context, offset);
      }
    }
  }

638 639 640 641 642 643 644
  final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();

  @override
  void dispose() {
    _clipRectLayer.layer = null;
    super.dispose();
  }
645

646 647
  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
648
    final Offset paintOffset = _paintOffset;
649 650 651 652
    transform.translate(paintOffset.dx, paintOffset.dy);
  }

  @override
653
  Rect? describeApproximatePaintClip(RenderObject? child) {
654
    if (child != null && _shouldClipAtPaintOffset(_paintOffset))
655
      return Offset.zero & size;
656 657 658 659
    return null;
  }

  @override
660
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
661
    if (child != null) {
662 663 664
      return result.addWithPaintOffset(
        offset: _paintOffset,
        position: position,
665
        hitTest: (BoxHitTestResult result, Offset? transformed) {
666
          assert(transformed == position + -_paintOffset);
667
          return child!.hitTest(result, position: transformed!);
668 669
        },
      );
670 671 672
    }
    return false;
  }
673 674

  @override
675
  RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) {
676
    rect ??= target.paintBounds;
677
    if (target is! RenderBox)
678
      return RevealedOffset(offset: offset.pixels, rect: rect);
679

680
    final RenderBox targetBox = target;
681
    final Matrix4 transform = targetBox.getTransformTo(child);
682
    final Rect bounds = MatrixUtils.transformRect(transform, rect);
683
    final Size contentSize = child!.size;
684

685 686 687
    final double leadingScrollOffset;
    final double targetMainAxisExtent;
    final double mainAxisExtent;
688 689 690 691

    assert(axisDirection != null);
    switch (axisDirection) {
      case AxisDirection.up:
692 693 694
        mainAxisExtent = size.height;
        leadingScrollOffset = contentSize.height - bounds.bottom;
        targetMainAxisExtent = bounds.height;
695 696
        break;
      case AxisDirection.right:
697 698 699
        mainAxisExtent = size.width;
        leadingScrollOffset = bounds.left;
        targetMainAxisExtent = bounds.width;
700 701
        break;
      case AxisDirection.down:
702 703 704
        mainAxisExtent = size.height;
        leadingScrollOffset = bounds.top;
        targetMainAxisExtent = bounds.height;
705 706
        break;
      case AxisDirection.left:
707 708 709
        mainAxisExtent = size.width;
        leadingScrollOffset = contentSize.width - bounds.right;
        targetMainAxisExtent = bounds.width;
710 711 712
        break;
    }

713 714
    final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
    final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset));
715
    return RevealedOffset(offset: targetOffset, rect: targetRect);
716
  }
717 718

  @override
719
  void showOnScreen({
720 721
    RenderObject? descendant,
    Rect? rect,
722 723 724
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
725 726 727 728 729 730 731 732 733
    if (!offset.allowImplicitScrolling) {
      return super.showOnScreen(
        descendant: descendant,
        rect: rect,
        duration: duration,
        curve: curve,
      );
    }

734
    final Rect? newRect = RenderViewportBase.showInViewport(
735 736 737 738 739 740 741 742 743 744 745 746
      descendant: descendant,
      viewport: this,
      offset: offset,
      rect: rect,
      duration: duration,
      curve: curve,
    );
    super.showOnScreen(
      rect: newRect,
      duration: duration,
      curve: curve,
    );
747
  }
748 749 750 751 752 753

  @override
  Rect describeSemanticsClip(RenderObject child) {
    assert(axis != null);
    switch (axis) {
      case Axis.vertical:
754
        return Rect.fromLTRB(
755 756 757 758 759 760
          semanticBounds.left,
          semanticBounds.top - cacheExtent,
          semanticBounds.right,
          semanticBounds.bottom + cacheExtent,
        );
      case Axis.horizontal:
761
        return Rect.fromLTRB(
762 763 764 765 766 767 768
          semanticBounds.left - cacheExtent,
          semanticBounds.top,
          semanticBounds.right + cacheExtent,
          semanticBounds.bottom,
        );
    }
  }
769
}