single_child_scroll_view.dart 23.9 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
/// than 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}
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
/// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart **
97 98
/// {@end-tool}
///
99 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
/// ### 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.
///
125
/// {@tool dartpad}
126 127 128
/// In this example, the column becomes either as big as viewport, or as big as
/// the contents, whichever is biggest.
///
129
/// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart **
130 131
/// {@end-tool}
///
132 133
/// See also:
///
134 135 136 137
///  * [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.
138
class SingleChildScrollView extends StatelessWidget {
139
  /// Creates a box in which a single widget can be scrolled.
140
  const SingleChildScrollView({
141
    super.key,
142 143
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
144
    this.padding,
145
    bool? primary,
146 147
    this.physics,
    this.controller,
148
    this.child,
149
    this.dragStartBehavior = DragStartBehavior.start,
150
    this.clipBehavior = Clip.hardEdge,
151
    this.restorationId,
152
    this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
153
  }) : assert(scrollDirection != null),
154
       assert(dragStartBehavior != null),
155
       assert(clipBehavior != null),
156
       assert(!(controller != null && (primary ?? false)),
157
          'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
158
          'You cannot both set primary to true and pass an explicit controller.',
159
       ),
160
       primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical);
161

162 163 164
  /// The axis along which the scroll view scrolls.
  ///
  /// Defaults to [Axis.vertical].
165 166
  final Axis scrollDirection;

167 168 169 170 171 172 173
  /// 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.
  ///
174
  /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
175 176 177 178
  /// scrolls from top to bottom when [reverse] is false and from bottom to top
  /// when [reverse] is true.
  ///
  /// Defaults to false.
179 180
  final bool reverse;

181
  /// The amount of space by which to inset the child.
182
  final EdgeInsetsGeometry? padding;
183

184 185 186 187
  /// An object that can be used to control the position to which this scroll
  /// view is scrolled.
  ///
  /// Must be null if [primary] is true.
188 189 190 191 192 193 194 195
  ///
  /// 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]).
196
  final ScrollController? controller;
197

198 199 200
  /// Whether this is the primary scroll view associated with the parent
  /// [PrimaryScrollController].
  ///
201 202 203 204 205
  /// 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.
  ///
206 207 208
  /// On iOS, this identifies the scroll view that will scroll to top in
  /// response to a tap in the status bar.
  ///
209
  /// Defaults to true when [scrollDirection] is vertical and [controller] is
210
  /// not specified.
211 212
  final bool primary;

213 214 215 216 217 218
  /// 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.
219
  final ScrollPhysics? physics;
220

221
  /// The widget that scrolls.
222
  ///
223
  /// {@macro flutter.widgets.ProxyWidget.child}
224
  final Widget? child;
225

226 227 228
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

229
  /// {@macro flutter.material.Material.clipBehavior}
230 231 232 233
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

234
  /// {@macro flutter.widgets.scrollable.restorationId}
235
  final String? restorationId;
236

237 238 239
  /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior}
  final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;

240
  AxisDirection _getDirection(BuildContext context) {
241
    return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse);
242 243 244 245
  }

  @override
  Widget build(BuildContext context) {
246
    final AxisDirection axisDirection = _getDirection(context);
247
    Widget? contents = child;
248
    if (padding != null)
249 250
      contents = Padding(padding: padding!, child: contents);
    final ScrollController? scrollController = primary
251 252
        ? PrimaryScrollController.of(context)
        : controller;
253
    Widget scrollable = Scrollable(
254
      dragStartBehavior: dragStartBehavior,
255
      axisDirection: axisDirection,
256
      controller: scrollController,
257
      physics: physics,
258
      restorationId: restorationId,
259
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
260
        return _SingleChildViewport(
261 262
          axisDirection: axisDirection,
          offset: offset,
263
          clipBehavior: clipBehavior,
264
          child: contents,
265 266
        );
      },
267
    );
268 269 270 271 272 273 274 275 276 277 278 279 280 281

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

282
    return primary && scrollController != null
283
      ? PrimaryScrollController.none(child: scrollable)
284
      : scrollable;
285 286 287 288
  }
}

class _SingleChildViewport extends SingleChildRenderObjectWidget {
289
  const _SingleChildViewport({
290
    this.axisDirection = AxisDirection.down,
291
    required this.offset,
292
    super.child,
293
    required this.clipBehavior,
294
  }) : assert(axisDirection != null),
295
       assert(clipBehavior != null);
296 297 298

  final AxisDirection axisDirection;
  final ViewportOffset offset;
299
  final Clip clipBehavior;
300 301 302

  @override
  _RenderSingleChildViewport createRenderObject(BuildContext context) {
303
    return _RenderSingleChildViewport(
304 305
      axisDirection: axisDirection,
      offset: offset,
306
      clipBehavior: clipBehavior,
307 308 309 310 311 312 313 314
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSingleChildViewport renderObject) {
    // Order dependency: The offset setter reads the axis direction.
    renderObject
      ..axisDirection = axisDirection
315 316
      ..offset = offset
      ..clipBehavior = clipBehavior;
317
  }
318 319 320 321 322 323 324 325

  @override
  SingleChildRenderObjectElement createElement() {
    return _SingleChildViewportElement(this);
  }
}

class _SingleChildViewportElement extends SingleChildRenderObjectElement with NotifiableElementMixin, ViewportElementMixin {
326
  _SingleChildViewportElement(_SingleChildViewport super.widget);
327 328
}

329
class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport {
330
  _RenderSingleChildViewport({
331
    AxisDirection axisDirection = AxisDirection.down,
332
    required ViewportOffset offset,
333
    double cacheExtent = RenderAbstractViewport.defaultCacheExtent,
334 335
    RenderBox? child,
    required Clip clipBehavior,
336 337
  }) : assert(axisDirection != null),
       assert(offset != null),
338
       assert(cacheExtent != null),
339
       assert(clipBehavior != null),
340
       _axisDirection = axisDirection,
341
       _offset = offset,
342 343
       _cacheExtent = cacheExtent,
       _clipBehavior = clipBehavior {
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
    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)
366
      _offset.removeListener(_hasScrolled);
367 368
    _offset = value;
    if (attached)
369
      _offset.addListener(_hasScrolled);
370
    markNeedsLayout();
371 372
  }

373
  /// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
374 375 376 377 378 379 380 381 382 383
  double get cacheExtent => _cacheExtent;
  double _cacheExtent;
  set cacheExtent(double value) {
    assert(value != null);
    if (value == _cacheExtent)
      return;
    _cacheExtent = value;
    markNeedsLayout();
  }

384
  /// {@macro flutter.material.Material.clipBehavior}
385 386 387 388 389 390 391 392 393 394 395 396 397
  ///
  /// 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();
    }
  }

398 399 400 401 402
  void _hasScrolled() {
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

403 404 405 406 407
  @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)
408
      child.parentData = ParentData();
409 410 411 412 413
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
414
    _offset.addListener(_hasScrolled);
415 416 417 418
  }

  @override
  void detach() {
419
    _offset.removeListener(_hasScrolled);
420 421 422 423 424 425
    super.detach();
  }

  @override
  bool get isRepaintBoundary => true;

426
  double get _viewportExtent {
427 428 429 430
    assert(hasSize);
    switch (axis) {
      case Axis.horizontal:
        return size.width;
431 432
      case Axis.vertical:
        return size.height;
433 434 435 436 437 438 439 440 441 442 443 444
    }
  }

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

  double get _maxScrollExtent {
    assert(hasSize);
    if (child == null)
      return 0.0;
445 446
    switch (axis) {
      case Axis.horizontal:
447
        return math.max(0.0, child!.size.width - size.width);
448
      case Axis.vertical:
449
        return math.max(0.0, child!.size.height - size.height);
450
    }
451 452 453 454 455 456 457 458 459 460 461 462 463 464
  }

  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)
465
      return child!.getMinIntrinsicWidth(height);
466 467 468 469 470 471
    return 0.0;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    if (child != null)
472
      return child!.getMaxIntrinsicWidth(height);
473 474 475 476 477 478
    return 0.0;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    if (child != null)
479
      return child!.getMinIntrinsicHeight(width);
480 481 482 483 484 485
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    if (child != null)
486
      return child!.getMaxIntrinsicHeight(width);
487 488 489 490 491 492 493 494
    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.

495 496 497 498 499 500 501 502 503
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    if (child == null) {
      return constraints.smallest;
    }
    final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints));
    return constraints.constrain(childSize);
  }

504 505
  @override
  void performLayout() {
506
    final BoxConstraints constraints = this.constraints;
507 508 509
    if (child == null) {
      size = constraints.smallest;
    } else {
510 511
      child!.layout(_getInnerConstraints(constraints), parentUsesSize: true);
      size = constraints.constrain(child!.size);
512 513
    }

514
    offset.applyViewportDimension(_viewportExtent);
515 516 517
    offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
  }

518 519 520
  Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);

  Offset _paintOffsetForPosition(double position) {
521 522 523
    assert(axisDirection != null);
    switch (axisDirection) {
      case AxisDirection.up:
524
        return Offset(0.0, position - child!.size.height + size.height);
525
      case AxisDirection.down:
526
        return Offset(0.0, -position);
527
      case AxisDirection.left:
528
        return Offset(position - child!.size.width + size.width, 0.0);
529
      case AxisDirection.right:
530
        return Offset(-position, 0.0);
531 532 533 534 535
    }
  }

  bool _shouldClipAtPaintOffset(Offset paintOffset) {
    assert(child != null);
536 537 538 539 540 541 542 543 544 545 546
    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;
    }
547 548 549 550 551
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
552
      final Offset paintOffset = _paintOffset;
553 554

      void paintContents(PaintingContext context, Offset offset) {
555
        context.paintChild(child!, offset + paintOffset);
556 557
      }

558
      if (_shouldClipAtPaintOffset(paintOffset)) {
559
        _clipRectLayer.layer = context.pushClipRect(
560 561 562 563 564
          needsCompositing,
          offset,
          Offset.zero & size,
          paintContents,
          clipBehavior: clipBehavior,
565
          oldLayer: _clipRectLayer.layer,
566
        );
567
      } else {
568
        _clipRectLayer.layer = null;
569 570 571 572 573
        paintContents(context, offset);
      }
    }
  }

574 575 576 577 578 579 580
  final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();

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

582 583
  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
584
    final Offset paintOffset = _paintOffset;
585 586 587 588
    transform.translate(paintOffset.dx, paintOffset.dy);
  }

  @override
589
  Rect? describeApproximatePaintClip(RenderObject? child) {
590
    if (child != null && _shouldClipAtPaintOffset(_paintOffset))
591
      return Offset.zero & size;
592 593 594 595
    return null;
  }

  @override
596
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
597
    if (child != null) {
598 599 600
      return result.addWithPaintOffset(
        offset: _paintOffset,
        position: position,
601
        hitTest: (BoxHitTestResult result, Offset transformed) {
602
          assert(transformed == position + -_paintOffset);
603
          return child!.hitTest(result, position: transformed);
604 605
        },
      );
606 607 608
    }
    return false;
  }
609 610

  @override
611
  RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) {
612
    rect ??= target.paintBounds;
613
    if (target is! RenderBox)
614
      return RevealedOffset(offset: offset.pixels, rect: rect);
615

616
    final RenderBox targetBox = target;
617
    final Matrix4 transform = targetBox.getTransformTo(child);
618
    final Rect bounds = MatrixUtils.transformRect(transform, rect);
619
    final Size contentSize = child!.size;
620

621 622 623
    final double leadingScrollOffset;
    final double targetMainAxisExtent;
    final double mainAxisExtent;
624 625 626 627

    assert(axisDirection != null);
    switch (axisDirection) {
      case AxisDirection.up:
628 629 630
        mainAxisExtent = size.height;
        leadingScrollOffset = contentSize.height - bounds.bottom;
        targetMainAxisExtent = bounds.height;
631 632
        break;
      case AxisDirection.right:
633 634 635
        mainAxisExtent = size.width;
        leadingScrollOffset = bounds.left;
        targetMainAxisExtent = bounds.width;
636 637
        break;
      case AxisDirection.down:
638 639 640
        mainAxisExtent = size.height;
        leadingScrollOffset = bounds.top;
        targetMainAxisExtent = bounds.height;
641 642
        break;
      case AxisDirection.left:
643 644 645
        mainAxisExtent = size.width;
        leadingScrollOffset = contentSize.width - bounds.right;
        targetMainAxisExtent = bounds.width;
646 647 648
        break;
    }

649 650
    final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
    final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset));
651
    return RevealedOffset(offset: targetOffset, rect: targetRect);
652
  }
653 654

  @override
655
  void showOnScreen({
656 657
    RenderObject? descendant,
    Rect? rect,
658 659 660
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
661 662 663 664 665 666 667 668 669
    if (!offset.allowImplicitScrolling) {
      return super.showOnScreen(
        descendant: descendant,
        rect: rect,
        duration: duration,
        curve: curve,
      );
    }

670
    final Rect? newRect = RenderViewportBase.showInViewport(
671 672 673 674 675 676 677 678 679 680 681 682
      descendant: descendant,
      viewport: this,
      offset: offset,
      rect: rect,
      duration: duration,
      curve: curve,
    );
    super.showOnScreen(
      rect: newRect,
      duration: duration,
      curve: curve,
    );
683
  }
684

685 686 687 688 689 690
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Offset>('offset', _paintOffset));
  }

691 692 693 694 695
  @override
  Rect describeSemanticsClip(RenderObject child) {
    assert(axis != null);
    switch (axis) {
      case Axis.vertical:
696
        return Rect.fromLTRB(
697 698 699 700 701 702
          semanticBounds.left,
          semanticBounds.top - cacheExtent,
          semanticBounds.right,
          semanticBounds.bottom + cacheExtent,
        );
      case Axis.horizontal:
703
        return Rect.fromLTRB(
704 705 706 707 708 709 710
          semanticBounds.left - cacheExtent,
          semanticBounds.top,
          semanticBounds.right + cacheExtent,
          semanticBounds.bottom,
        );
    }
  }
711
}