single_child_scroll_view.dart 23.4 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
/// ## 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].
///
51
/// Doing so, however, usually results in a conflict between the [Column],
52 53 54 55 56
/// 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
57 58 59 60 61 62
/// 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 (in which case [ListView] may be a better choice than
/// [Column]).
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 88
///
/// ### 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.
///
89
/// {@tool dartpad}
90 91
/// In this example, the children are spaced out equally, unless there's no more
/// room, in which case they stack vertically and scroll.
92 93 94 95 96
///
/// 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.
///
97
/// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart **
98 99
/// {@end-tool}
///
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
/// ### 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.
///
126
/// {@tool dartpad}
127 128 129
/// In this example, the column becomes either as big as viewport, or as big as
/// the contents, whichever is biggest.
///
130
/// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart **
131 132
/// {@end-tool}
///
133 134
/// {@macro flutter.widgets.ScrollView.PageStorage}
///
135 136
/// See also:
///
137 138 139 140
///  * [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.
141
class SingleChildScrollView extends StatelessWidget {
142
  /// Creates a box in which a single widget can be scrolled.
143
  const SingleChildScrollView({
144
    super.key,
145 146
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
147
    this.padding,
148
    this.primary,
149 150
    this.physics,
    this.controller,
151
    this.child,
152
    this.dragStartBehavior = DragStartBehavior.start,
153
    this.clipBehavior = Clip.hardEdge,
154
    this.restorationId,
155
    this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
156
  }) : assert(
157 158 159 160 161
         !(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.',
       );
162

163
  /// {@macro flutter.widgets.scroll_view.scrollDirection}
164 165
  final Axis scrollDirection;

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

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

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

197 198
  /// {@macro flutter.widgets.scroll_view.primary}
  final bool? primary;
199

200 201 202 203 204 205
  /// 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.
206
  final ScrollPhysics? physics;
207

208
  /// The widget that scrolls.
209
  ///
210
  /// {@macro flutter.widgets.ProxyWidget.child}
211
  final Widget? child;
212

213 214 215
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

216
  /// {@macro flutter.material.Material.clipBehavior}
217 218 219 220
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

221
  /// {@macro flutter.widgets.scrollable.restorationId}
222
  final String? restorationId;
223

224 225 226
  /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior}
  final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;

227
  AxisDirection _getDirection(BuildContext context) {
228
    return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse);
229 230 231 232
  }

  @override
  Widget build(BuildContext context) {
233
    final AxisDirection axisDirection = _getDirection(context);
234
    Widget? contents = child;
235
    if (padding != null) {
236
      contents = Padding(padding: padding!, child: contents);
237
    }
238 239 240 241
    final bool effectivePrimary = primary
        ?? controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection);

    final ScrollController? scrollController = effectivePrimary
242
        ? PrimaryScrollController.maybeOf(context)
243
        : controller;
244

245
    Widget scrollable = Scrollable(
246
      dragStartBehavior: dragStartBehavior,
247
      axisDirection: axisDirection,
248
      controller: scrollController,
249
      physics: physics,
250
      restorationId: restorationId,
251
      clipBehavior: clipBehavior,
252
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
253
        return _SingleChildViewport(
254 255
          axisDirection: axisDirection,
          offset: offset,
256
          clipBehavior: clipBehavior,
257
          child: contents,
258 259
        );
      },
260
    );
261 262 263 264 265 266 267 268 269 270 271 272 273 274

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

275 276 277
    return effectivePrimary && scrollController != null
      // Further descendant ScrollViews will not inherit the same
      // PrimaryScrollController
278
      ? PrimaryScrollController.none(child: scrollable)
279
      : scrollable;
280 281 282 283
  }
}

class _SingleChildViewport extends SingleChildRenderObjectWidget {
284
  const _SingleChildViewport({
285
    this.axisDirection = AxisDirection.down,
286
    required this.offset,
287
    super.child,
288
    required this.clipBehavior,
289
  });
290 291 292

  final AxisDirection axisDirection;
  final ViewportOffset offset;
293
  final Clip clipBehavior;
294 295 296

  @override
  _RenderSingleChildViewport createRenderObject(BuildContext context) {
297
    return _RenderSingleChildViewport(
298 299
      axisDirection: axisDirection,
      offset: offset,
300
      clipBehavior: clipBehavior,
301 302 303 304 305 306 307 308
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSingleChildViewport renderObject) {
    // Order dependency: The offset setter reads the axis direction.
    renderObject
      ..axisDirection = axisDirection
309 310
      ..offset = offset
      ..clipBehavior = clipBehavior;
311
  }
312 313 314 315 316 317 318 319

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

class _SingleChildViewportElement extends SingleChildRenderObjectElement with NotifiableElementMixin, ViewportElementMixin {
320
  _SingleChildViewportElement(_SingleChildViewport super.widget);
321 322
}

323
class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport {
324
  _RenderSingleChildViewport({
325
    AxisDirection axisDirection = AxisDirection.down,
326 327 328
    required ViewportOffset offset,
    RenderBox? child,
    required Clip clipBehavior,
329
  }) : _axisDirection = axisDirection,
330
       _offset = offset,
331
       _clipBehavior = clipBehavior {
332 333 334 335 336 337
    this.child = child;
  }

  AxisDirection get axisDirection => _axisDirection;
  AxisDirection _axisDirection;
  set axisDirection(AxisDirection value) {
338
    if (value == _axisDirection) {
339
      return;
340
    }
341 342 343 344 345 346 347 348 349
    _axisDirection = value;
    markNeedsLayout();
  }

  Axis get axis => axisDirectionToAxis(axisDirection);

  ViewportOffset get offset => _offset;
  ViewportOffset _offset;
  set offset(ViewportOffset value) {
350
    if (value == _offset) {
351
      return;
352 353
    }
    if (attached) {
354
      _offset.removeListener(_hasScrolled);
355
    }
356
    _offset = value;
357
    if (attached) {
358
      _offset.addListener(_hasScrolled);
359
    }
360
    markNeedsLayout();
361 362
  }

363
  /// {@macro flutter.material.Material.clipBehavior}
364
  ///
365
  /// Defaults to [Clip.none].
366 367 368 369 370 371 372 373 374 375
  Clip get clipBehavior => _clipBehavior;
  Clip _clipBehavior = Clip.none;
  set clipBehavior(Clip value) {
    if (value != _clipBehavior) {
      _clipBehavior = value;
      markNeedsPaint();
      markNeedsSemanticsUpdate();
    }
  }

376 377 378 379 380
  void _hasScrolled() {
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

381 382 383 384
  @override
  void setupParentData(RenderObject child) {
    // We don't actually use the offset argument in BoxParentData, so let's
    // avoid allocating it at all.
385
    if (child.parentData is! ParentData) {
386
      child.parentData = ParentData();
387
    }
388 389 390 391 392
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
393
    _offset.addListener(_hasScrolled);
394 395 396 397
  }

  @override
  void detach() {
398
    _offset.removeListener(_hasScrolled);
399 400 401 402 403 404
    super.detach();
  }

  @override
  bool get isRepaintBoundary => true;

405
  double get _viewportExtent {
406 407 408 409
    assert(hasSize);
    switch (axis) {
      case Axis.horizontal:
        return size.width;
410 411
      case Axis.vertical:
        return size.height;
412 413 414 415 416 417 418 419 420 421
    }
  }

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

  double get _maxScrollExtent {
    assert(hasSize);
422
    if (child == null) {
423
      return 0.0;
424
    }
425 426
    switch (axis) {
      case Axis.horizontal:
427
        return math.max(0.0, child!.size.width - size.width);
428
      case Axis.vertical:
429
        return math.max(0.0, child!.size.height - size.height);
430
    }
431 432 433 434 435 436 437 438 439 440 441 442 443
  }

  BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
    switch (axis) {
      case Axis.horizontal:
        return constraints.heightConstraints();
      case Axis.vertical:
        return constraints.widthConstraints();
    }
  }

  @override
  double computeMinIntrinsicWidth(double height) {
444
    if (child != null) {
445
      return child!.getMinIntrinsicWidth(height);
446
    }
447 448 449 450 451
    return 0.0;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
452
    if (child != null) {
453
      return child!.getMaxIntrinsicWidth(height);
454
    }
455 456 457 458 459
    return 0.0;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
460
    if (child != null) {
461
      return child!.getMinIntrinsicHeight(width);
462
    }
463 464 465 466 467
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
468
    if (child != null) {
469
      return child!.getMaxIntrinsicHeight(width);
470
    }
471 472 473 474 475 476 477 478
    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.

479 480 481 482 483 484 485 486 487
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    if (child == null) {
      return constraints.smallest;
    }
    final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints));
    return constraints.constrain(childSize);
  }

488 489
  @override
  void performLayout() {
490
    final BoxConstraints constraints = this.constraints;
491 492 493
    if (child == null) {
      size = constraints.smallest;
    } else {
494 495
      child!.layout(_getInnerConstraints(constraints), parentUsesSize: true);
      size = constraints.constrain(child!.size);
496 497
    }

498
    offset.applyViewportDimension(_viewportExtent);
499 500 501
    offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
  }

502 503 504
  Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);

  Offset _paintOffsetForPosition(double position) {
505 506
    switch (axisDirection) {
      case AxisDirection.up:
507
        return Offset(0.0, position - child!.size.height + size.height);
508
      case AxisDirection.down:
509
        return Offset(0.0, -position);
510
      case AxisDirection.left:
511
        return Offset(position - child!.size.width + size.width, 0.0);
512
      case AxisDirection.right:
513
        return Offset(-position, 0.0);
514 515 516 517 518
    }
  }

  bool _shouldClipAtPaintOffset(Offset paintOffset) {
    assert(child != null);
519 520 521 522 523 524 525 526 527 528 529
    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;
    }
530 531 532 533 534
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
535
      final Offset paintOffset = _paintOffset;
536 537

      void paintContents(PaintingContext context, Offset offset) {
538
        context.paintChild(child!, offset + paintOffset);
539 540
      }

541
      if (_shouldClipAtPaintOffset(paintOffset)) {
542
        _clipRectLayer.layer = context.pushClipRect(
543 544 545 546 547
          needsCompositing,
          offset,
          Offset.zero & size,
          paintContents,
          clipBehavior: clipBehavior,
548
          oldLayer: _clipRectLayer.layer,
549
        );
550
      } else {
551
        _clipRectLayer.layer = null;
552 553 554 555 556
        paintContents(context, offset);
      }
    }
  }

557 558 559 560 561 562 563
  final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();

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

565 566
  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
567
    final Offset paintOffset = _paintOffset;
568 569 570 571
    transform.translate(paintOffset.dx, paintOffset.dy);
  }

  @override
572
  Rect? describeApproximatePaintClip(RenderObject? child) {
573
    if (child != null && _shouldClipAtPaintOffset(_paintOffset)) {
574
      return Offset.zero & size;
575
    }
576 577 578 579
    return null;
  }

  @override
580
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
581
    if (child != null) {
582 583 584
      return result.addWithPaintOffset(
        offset: _paintOffset,
        position: position,
585
        hitTest: (BoxHitTestResult result, Offset transformed) {
586
          assert(transformed == position + -_paintOffset);
587
          return child!.hitTest(result, position: transformed);
588 589
        },
      );
590 591 592
    }
    return false;
  }
593 594

  @override
595 596 597 598 599 600
  RevealedOffset getOffsetToReveal(
    RenderObject target,
    double alignment, {
    Rect? rect,
    Axis? axis,
  }) {
601 602 603
    // One dimensional viewport has only one axis, override if it was
    // provided/may be mismatched.
    axis = this.axis;
604

605
    rect ??= target.paintBounds;
606
    if (target is! RenderBox) {
607
      return RevealedOffset(offset: offset.pixels, rect: rect);
608
    }
609

610
    final RenderBox targetBox = target;
611
    final Matrix4 transform = targetBox.getTransformTo(child);
612
    final Rect bounds = MatrixUtils.transformRect(transform, rect);
613
    final Size contentSize = child!.size;
614

615 616 617
    final double leadingScrollOffset;
    final double targetMainAxisExtent;
    final double mainAxisExtent;
618 619 620

    switch (axisDirection) {
      case AxisDirection.up:
621 622 623
        mainAxisExtent = size.height;
        leadingScrollOffset = contentSize.height - bounds.bottom;
        targetMainAxisExtent = bounds.height;
624
      case AxisDirection.right:
625 626 627
        mainAxisExtent = size.width;
        leadingScrollOffset = bounds.left;
        targetMainAxisExtent = bounds.width;
628
      case AxisDirection.down:
629 630 631
        mainAxisExtent = size.height;
        leadingScrollOffset = bounds.top;
        targetMainAxisExtent = bounds.height;
632
      case AxisDirection.left:
633 634 635
        mainAxisExtent = size.width;
        leadingScrollOffset = contentSize.width - bounds.right;
        targetMainAxisExtent = bounds.width;
636 637
    }

638 639
    final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
    final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset));
640
    return RevealedOffset(offset: targetOffset, rect: targetRect);
641
  }
642 643

  @override
644
  void showOnScreen({
645 646
    RenderObject? descendant,
    Rect? rect,
647 648 649
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
650 651 652 653 654 655 656 657 658
    if (!offset.allowImplicitScrolling) {
      return super.showOnScreen(
        descendant: descendant,
        rect: rect,
        duration: duration,
        curve: curve,
      );
    }

659
    final Rect? newRect = RenderViewportBase.showInViewport(
660 661 662 663 664 665 666 667 668 669 670 671
      descendant: descendant,
      viewport: this,
      offset: offset,
      rect: rect,
      duration: duration,
      curve: curve,
    );
    super.showOnScreen(
      rect: newRect,
      duration: duration,
      curve: curve,
    );
672
  }
673

674 675 676 677 678 679
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Offset>('offset', _paintOffset));
  }

680 681
  @override
  Rect describeSemanticsClip(RenderObject child) {
682 683 684
    final double remainingOffset = _maxScrollExtent - offset.pixels;
    switch (axisDirection) {
      case AxisDirection.up:
685
        return Rect.fromLTRB(
686
          semanticBounds.left,
687
          semanticBounds.top - remainingOffset,
688
          semanticBounds.right,
689
          semanticBounds.bottom + offset.pixels,
690
        );
691 692 693 694 695 696 697 698 699 700 701 702 703 704 705
      case AxisDirection.right:
        return Rect.fromLTRB(
          semanticBounds.left - offset.pixels,
          semanticBounds.top,
          semanticBounds.right + remainingOffset,
          semanticBounds.bottom,
        );
      case AxisDirection.down:
        return Rect.fromLTRB(
          semanticBounds.left,
          semanticBounds.top - offset.pixels,
          semanticBounds.right,
          semanticBounds.bottom + remainingOffset,
        );
      case AxisDirection.left:
706
        return Rect.fromLTRB(
707
          semanticBounds.left - remainingOffset,
708
          semanticBounds.top,
709
          semanticBounds.right + offset.pixels,
710 711 712 713
          semanticBounds.bottom,
        );
    }
  }
714
}