single_child_scroll_view.dart 23.7 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
    this.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 157 158 159 160 161
       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.',
       );
162

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

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

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

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

199 200
  /// {@macro flutter.widgets.scroll_view.primary}
  final bool? primary;
201

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

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

215 216 217
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

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

223
  /// {@macro flutter.widgets.scrollable.restorationId}
224
  final String? restorationId;
225

226 227 228
  /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior}
  final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;

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

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

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

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

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

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

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

  final AxisDirection axisDirection;
  final ViewportOffset offset;
295
  final Clip clipBehavior;
296 297 298

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

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

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

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

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

  AxisDirection get axisDirection => _axisDirection;
  AxisDirection _axisDirection;
  set axisDirection(AxisDirection value) {
    assert(value != null);
344
    if (value == _axisDirection) {
345
      return;
346
    }
347 348 349 350 351 352 353 354 355 356
    _axisDirection = value;
    markNeedsLayout();
  }

  Axis get axis => axisDirectionToAxis(axisDirection);

  ViewportOffset get offset => _offset;
  ViewportOffset _offset;
  set offset(ViewportOffset value) {
    assert(value != null);
357
    if (value == _offset) {
358
      return;
359 360
    }
    if (attached) {
361
      _offset.removeListener(_hasScrolled);
362
    }
363
    _offset = value;
364
    if (attached) {
365
      _offset.addListener(_hasScrolled);
366
    }
367
    markNeedsLayout();
368 369
  }

370
  /// {@macro flutter.material.Material.clipBehavior}
371 372 373 374 375 376 377 378 379 380 381 382 383
  ///
  /// 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();
    }
  }

384 385 386 387 388
  void _hasScrolled() {
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

389 390 391 392
  @override
  void setupParentData(RenderObject child) {
    // We don't actually use the offset argument in BoxParentData, so let's
    // avoid allocating it at all.
393
    if (child.parentData is! ParentData) {
394
      child.parentData = ParentData();
395
    }
396 397 398 399 400
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
401
    _offset.addListener(_hasScrolled);
402 403 404 405
  }

  @override
  void detach() {
406
    _offset.removeListener(_hasScrolled);
407 408 409 410 411 412
    super.detach();
  }

  @override
  bool get isRepaintBoundary => true;

413
  double get _viewportExtent {
414 415 416 417
    assert(hasSize);
    switch (axis) {
      case Axis.horizontal:
        return size.width;
418 419
      case Axis.vertical:
        return size.height;
420 421 422 423 424 425 426 427 428 429
    }
  }

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

  double get _maxScrollExtent {
    assert(hasSize);
430
    if (child == null) {
431
      return 0.0;
432
    }
433 434
    switch (axis) {
      case Axis.horizontal:
435
        return math.max(0.0, child!.size.width - size.width);
436
      case Axis.vertical:
437
        return math.max(0.0, child!.size.height - size.height);
438
    }
439 440 441 442 443 444 445 446 447 448 449 450 451
  }

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

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

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

  @override
  double computeMinIntrinsicHeight(double width) {
468
    if (child != null) {
469
      return child!.getMinIntrinsicHeight(width);
470
    }
471 472 473 474 475
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
476
    if (child != null) {
477
      return child!.getMaxIntrinsicHeight(width);
478
    }
479 480 481 482 483 484 485 486
    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.

487 488 489 490 491 492 493 494 495
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    if (child == null) {
      return constraints.smallest;
    }
    final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints));
    return constraints.constrain(childSize);
  }

496 497
  @override
  void performLayout() {
498
    final BoxConstraints constraints = this.constraints;
499 500 501
    if (child == null) {
      size = constraints.smallest;
    } else {
502 503
      child!.layout(_getInnerConstraints(constraints), parentUsesSize: true);
      size = constraints.constrain(child!.size);
504 505
    }

506
    offset.applyViewportDimension(_viewportExtent);
507 508 509
    offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
  }

510 511 512
  Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);

  Offset _paintOffsetForPosition(double position) {
513 514 515
    assert(axisDirection != null);
    switch (axisDirection) {
      case AxisDirection.up:
516
        return Offset(0.0, position - child!.size.height + size.height);
517
      case AxisDirection.down:
518
        return Offset(0.0, -position);
519
      case AxisDirection.left:
520
        return Offset(position - child!.size.width + size.width, 0.0);
521
      case AxisDirection.right:
522
        return Offset(-position, 0.0);
523 524 525 526 527
    }
  }

  bool _shouldClipAtPaintOffset(Offset paintOffset) {
    assert(child != null);
528 529 530 531 532 533 534 535 536 537 538
    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;
    }
539 540 541 542 543
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
544
      final Offset paintOffset = _paintOffset;
545 546

      void paintContents(PaintingContext context, Offset offset) {
547
        context.paintChild(child!, offset + paintOffset);
548 549
      }

550
      if (_shouldClipAtPaintOffset(paintOffset)) {
551
        _clipRectLayer.layer = context.pushClipRect(
552 553 554 555 556
          needsCompositing,
          offset,
          Offset.zero & size,
          paintContents,
          clipBehavior: clipBehavior,
557
          oldLayer: _clipRectLayer.layer,
558
        );
559
      } else {
560
        _clipRectLayer.layer = null;
561 562 563 564 565
        paintContents(context, offset);
      }
    }
  }

566 567 568 569 570 571 572
  final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();

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

574 575
  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
576
    final Offset paintOffset = _paintOffset;
577 578 579 580
    transform.translate(paintOffset.dx, paintOffset.dy);
  }

  @override
581
  Rect? describeApproximatePaintClip(RenderObject? child) {
582
    if (child != null && _shouldClipAtPaintOffset(_paintOffset)) {
583
      return Offset.zero & size;
584
    }
585 586 587 588
    return null;
  }

  @override
589
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
590
    if (child != null) {
591 592 593
      return result.addWithPaintOffset(
        offset: _paintOffset,
        position: position,
594
        hitTest: (BoxHitTestResult result, Offset transformed) {
595
          assert(transformed == position + -_paintOffset);
596
          return child!.hitTest(result, position: transformed);
597 598
        },
      );
599 600 601
    }
    return false;
  }
602 603

  @override
604
  RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) {
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 621

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

643 644
    final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
    final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset));
645
    return RevealedOffset(offset: targetOffset, rect: targetRect);
646
  }
647 648

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

664
    final Rect? newRect = RenderViewportBase.showInViewport(
665 666 667 668 669 670 671 672 673 674 675 676
      descendant: descendant,
      viewport: this,
      offset: offset,
      rect: rect,
      duration: duration,
      curve: curve,
    );
    super.showOnScreen(
      rect: newRect,
      duration: duration,
      curve: curve,
    );
677
  }
678

679 680 681 682 683 684
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Offset>('offset', _paintOffset));
  }

685 686 687
  @override
  Rect describeSemanticsClip(RenderObject child) {
    assert(axis != null);
688 689 690
    final double remainingOffset = _maxScrollExtent - offset.pixels;
    switch (axisDirection) {
      case AxisDirection.up:
691
        return Rect.fromLTRB(
692
          semanticBounds.left,
693
          semanticBounds.top - remainingOffset,
694
          semanticBounds.right,
695
          semanticBounds.bottom + offset.pixels,
696
        );
697 698 699 700 701 702 703 704 705 706 707 708 709 710 711
      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:
712
        return Rect.fromLTRB(
713
          semanticBounds.left - remainingOffset,
714
          semanticBounds.top,
715
          semanticBounds.right + offset.pixels,
716 717 718 719
          semanticBounds.bottom,
        );
    }
  }
720
}