scrollable.dart 27.1 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6
import 'dart:math' as math;
7
import 'dart:ui';
8

9
import 'package:flutter/gestures.dart';
10
import 'package:flutter/rendering.dart';
11
import 'package:flutter/scheduler.dart';
12
import 'package:flutter/painting.dart';
13 14 15 16

import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
17
import 'notification_listener.dart';
18
import 'scroll_configuration.dart';
19
import 'scroll_context.dart';
20
import 'scroll_controller.dart';
21
import 'scroll_physics.dart';
22
import 'scroll_position.dart';
23
import 'scroll_position_with_single_context.dart';
24
import 'ticker_provider.dart';
25 26 27 28
import 'viewport.dart';

export 'package:flutter/physics.dart' show Tolerance;

29 30
/// Signature used by [Scrollable] to build the viewport through which the
/// scrollable content is displayed.
31
typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset position);
32

33 34 35 36 37 38 39 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
/// A widget that scrolls.
///
/// [Scrollable] implements the interaction model for a scrollable widget,
/// including gesture recognition, but does not have an opinion about how the
/// viewport, which actually displays the children, is constructed.
///
/// It's rare to construct a [Scrollable] directly. Instead, consider [ListView]
/// or [GridView], which combine scrolling, viewporting, and a layout model. To
/// combine layout models (or to use a custom layout mode), consider using
/// [CustomScrollView].
///
/// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are
/// often used to interact with the [Scrollable] widget inside a [ListView] or
/// a [GridView].
///
/// To further customize scrolling behavior with a [Scrollable]:
///
/// 1. You can provide a [viewportBuilder] to customize the child model. For
///    example, [SingleChildScrollView] uses a viewport that displays a single
///    box child whereas [CustomScrollView] uses a [Viewport] or a
///    [ShrinkWrappingViewport], both of which display a list of slivers.
///
/// 2. You can provide a custom [ScrollController] that creates a custom
///    [ScrollPosition] subclass. For example, [PageView] uses a
///    [PageController], which creates a page-oriented scroll position subclass
///    that keeps the same page visible when the [Scrollable] resizes.
///
/// See also:
///
///  * [ListView], which is a commonly used [ScrollView] that displays a
///    scrolling, linear list of child widgets.
///  * [PageView], which is a scrolling list of child widgets that are each the
///    size of the viewport.
///  * [GridView], which is a [ScrollView] that displays a scrolling, 2D array
///    of child widgets.
///  * [CustomScrollView], which is a [ScrollView] that creates custom scroll
///    effects using slivers.
///  * [SingleChildScrollView], which is a scrollable widget that has a single
///    child.
72
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
73
///    the scroll position without using a [ScrollController].
Adam Barth's avatar
Adam Barth committed
74
class Scrollable extends StatefulWidget {
75 76 77
  /// Creates a widget that scrolls.
  ///
  /// The [axisDirection] and [viewportBuilder] arguments must not be null.
78
  const Scrollable({
79
    Key key,
80
    this.axisDirection = AxisDirection.down,
81
    this.controller,
Adam Barth's avatar
Adam Barth committed
82
    this.physics,
83
    @required this.viewportBuilder,
84
    this.excludeFromSemantics = false,
85
    this.semanticChildCount,
86
    this.dragStartBehavior = DragStartBehavior.start,
87
  }) : assert(axisDirection != null),
88
       assert(dragStartBehavior != null),
89
       assert(viewportBuilder != null),
90
       assert(excludeFromSemantics != null),
91
       super (key: key);
92

93 94 95 96 97 98 99 100 101 102
  /// The direction in which this widget scrolls.
  ///
  /// For example, if the [axisDirection] is [AxisDirection.down], increasing
  /// the scroll position will cause content below the bottom of the viewport to
  /// become visible through the viewport. Similarly, if [axisDirection] is
  /// [AxisDirection.right], increasing the scroll position will cause content
  /// beyond the right edge of the viewport to become visible through the
  /// viewport.
  ///
  /// Defaults to [AxisDirection.down].
103 104
  final AxisDirection axisDirection;

105 106 107
  /// An object that can be used to control the position to which this widget is
  /// scrolled.
  ///
108 109 110 111 112 113 114 115
  /// 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]).
  ///
116 117 118 119
  /// See also:
  ///
  ///  * [ensureVisible], which animates the scroll position to reveal a given
  ///    [BuildContext].
120 121
  final ScrollController controller;

122 123 124 125 126
  /// How the widgets should respond to user input.
  ///
  /// For example, determines how the widget continues to animate after the
  /// user stops dragging the scroll view.
  ///
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
  /// Defaults to matching platform conventions via the physics provided from
  /// the ambient [ScrollConfiguration].
  ///
  /// The physics can be changed dynamically, but new physics will only take
  /// effect if the _class_ of the provided object changes. Merely constructing
  /// a new instance with a different configuration is insufficient to cause the
  /// physics to be reapplied. (This is because the final object used is
  /// generated dynamically, which can be relatively expensive, and it would be
  /// inefficient to speculatively create this object each frame to see if the
  /// physics should be updated.)
  ///
  /// See also:
  ///
  ///  * [AlwaysScrollableScrollPhysics], which can be used to indicate that the
  ///    scrollable should react to scroll requests (and possible overscroll)
  ///    even if the scrollable's contents fit without scrolling being necessary.
Adam Barth's avatar
Adam Barth committed
143 144
  final ScrollPhysics physics;

145 146 147 148 149 150 151 152 153 154
  /// Builds the viewport through which the scrollable content is displayed.
  ///
  /// A typical viewport uses the given [ViewportOffset] to determine which part
  /// of its content is actually visible through the viewport.
  ///
  /// See also:
  ///
  ///  * [Viewport], which is a viewport that displays a list of slivers.
  ///  * [ShrinkWrappingViewport], which is a viewport that displays a list of
  ///    slivers and sizes itself based on the size of the slivers.
155
  final ViewportBuilder viewportBuilder;
156

157 158 159 160 161 162 163 164 165 166 167 168 169
  /// Whether the scroll actions introduced by this [Scrollable] are exposed
  /// in the semantics tree.
  ///
  /// Text fields with an overflow are usually scrollable to make sure that the
  /// user can get to the beginning/end of the entered text. However, these
  /// scrolling actions are generally not exposed to the semantics layer.
  ///
  /// See also:
  ///
  ///  * [GestureDetector.excludeFromSemantics], which is used to accomplish the
  ///    exclusion.
  final bool excludeFromSemantics;

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
  /// The number of children that will contribute semantic information.
  ///
  /// The value will be null if the number of children is unknown or unbounded.
  ///
  /// Some subtypes of [ScrollView] can infer this value automatically. For
  /// example [ListView] will use the number of widgets in the child list,
  /// while the [new ListView.separated] constructor will use half that amount.
  ///
  /// For [CustomScrollView] and other types which do not receive a builder
  /// or list of widgets, the child count must be explicitly provided.
  ///
  /// See also:
  ///
  ///  * [CustomScrollView], for an explanation of scroll semantics.
  ///  * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property.
  final int semanticChildCount;

187
  // TODO(jslavitz): Set the DragStartBehavior default to be start across all widgets.
188 189 190 191 192 193 194 195 196 197 198
  /// {@template flutter.widgets.scrollable.dragStartBehavior}
  /// Determines the way that drag start behavior is handled.
  ///
  /// If set to [DragStartBehavior.start], scrolling drag behavior will
  /// begin upon the detection of a drag gesture. If set to
  /// [DragStartBehavior.down] it will begin when a down event is first detected.
  ///
  /// In general, setting this to [DragStartBehavior.start] will make drag
  /// animation smoother and setting it to [DragStartBehavior.down] will make
  /// drag behavior feel slightly more reactive.
  ///
199
  /// By default, the drag start behavior is [DragStartBehavior.start].
200 201 202
  ///
  /// See also:
  ///
203 204 205
  ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for
  ///    the different behaviors.
  ///
206 207 208
  /// {@endtemplate}
  final DragStartBehavior dragStartBehavior;

209 210 211
  /// The axis along which the scroll view scrolls.
  ///
  /// Determined by the [axisDirection].
212 213 214
  Axis get axis => axisDirectionToAxis(axisDirection);

  @override
215
  ScrollableState createState() => ScrollableState();
216 217

  @override
218 219
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
220 221
    properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
    properties.add(DiagnosticsProperty<ScrollPhysics>('physics', physics));
222
  }
223 224 225 226 227 228

  /// The state from the closest instance of this class that encloses the given context.
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
Adam Barth's avatar
Adam Barth committed
229
  /// ScrollableState scrollable = Scrollable.of(context);
230
  /// ```
Adam Barth's avatar
Adam Barth committed
231
  static ScrollableState of(BuildContext context) {
232 233
    final _ScrollableScope widget = context.inheritFromWidgetOfExactType(_ScrollableScope);
    return widget?.scrollable;
234 235
  }

236 237
  /// Scrolls the scrollables that enclose the given context so as to make the
  /// given context visible.
238 239
  static Future<void> ensureVisible(
    BuildContext context, {
240 241 242
    double alignment = 0.0,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
243
  }) {
244
    final List<Future<void>> futures = <Future<void>>[];
245

Adam Barth's avatar
Adam Barth committed
246
    ScrollableState scrollable = Scrollable.of(context);
247
    while (scrollable != null) {
248 249 250 251 252 253
      futures.add(scrollable.position.ensureVisible(
        context.findRenderObject(),
        alignment: alignment,
        duration: duration,
        curve: curve,
      ));
254
      context = scrollable.context;
Adam Barth's avatar
Adam Barth committed
255
      scrollable = Scrollable.of(context);
256 257
    }

258
    if (futures.isEmpty || duration == Duration.zero)
259
      return Future<void>.value();
260
    if (futures.length == 1)
261
      return futures.single;
262
    return Future.wait<void>(futures).then<void>((List<void> _) => null);
263
  }
264 265
}

266 267 268
// Enable Scrollable.of() to work as if ScrollableState was an inherited widget.
// ScrollableState.build() always rebuilds its _ScrollableScope.
class _ScrollableScope extends InheritedWidget {
269
  const _ScrollableScope({
270 271 272
    Key key,
    @required this.scrollable,
    @required this.position,
273
    @required Widget child,
274 275 276
  }) : assert(scrollable != null),
       assert(child != null),
       super(key: key, child: child);
277 278 279 280 281 282 283 284 285 286

  final ScrollableState scrollable;
  final ScrollPosition position;

  @override
  bool updateShouldNotify(_ScrollableScope old) {
    return position != old.position;
  }
}

Adam Barth's avatar
Adam Barth committed
287
/// State object for a [Scrollable] widget.
288
///
Adam Barth's avatar
Adam Barth committed
289
/// To manipulate a [Scrollable] widget's scroll position, use the object
290 291
/// obtained from the [position] property.
///
Adam Barth's avatar
Adam Barth committed
292 293
/// To be informed of when a [Scrollable] widget is scrolling, use a
/// [NotificationListener] to listen for [ScrollNotification] notifications.
294 295
///
/// This class is not intended to be subclassed. To specialize the behavior of a
Adam Barth's avatar
Adam Barth committed
296 297
/// [Scrollable], provide it with a [ScrollPhysics].
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
298 299
    implements ScrollContext {
  /// The manager for this [Scrollable] widget's viewport position.
300
  ///
Adam Barth's avatar
Adam Barth committed
301
  /// To control what kind of [ScrollPosition] is created for a [Scrollable],
302 303
  /// provide it with custom [ScrollController] that creates the appropriate
  /// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
304 305 306
  ScrollPosition get position => _position;
  ScrollPosition _position;

307 308 309
  @override
  AxisDirection get axisDirection => widget.axisDirection;

Adam Barth's avatar
Adam Barth committed
310
  ScrollBehavior _configuration;
311
  ScrollPhysics _physics;
312

313
  // Only call this from places that will definitely trigger a rebuild.
314
  void _updatePosition() {
Adam Barth's avatar
Adam Barth committed
315
    _configuration = ScrollConfiguration.of(context);
316
    _physics = _configuration.getScrollPhysics(context);
317 318 319
    if (widget.physics != null)
      _physics = widget.physics.applyTo(_physics);
    final ScrollController controller = widget.controller;
320 321
    final ScrollPosition oldPosition = position;
    if (oldPosition != null) {
322
      controller?.detach(oldPosition);
323 324 325
      // It's important that we not dispose the old position until after the
      // viewport has had a chance to unregister its listeners from the old
      // position. So, schedule a microtask to do it.
326 327
      scheduleMicrotask(oldPosition.dispose);
    }
328

329
    _position = controller?.createScrollPosition(_physics, this, oldPosition)
330
      ?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
331
    assert(position != null);
332
    controller?.attach(position);
333 334 335
  }

  @override
336 337
  void didChangeDependencies() {
    super.didChangeDependencies();
338 339 340
    _updatePosition();
  }

341
  bool _shouldUpdatePosition(Scrollable oldWidget) {
342 343 344 345 346 347 348 349 350 351
    ScrollPhysics newPhysics = widget.physics;
    ScrollPhysics oldPhysics = oldWidget.physics;
    do {
      if (newPhysics?.runtimeType != oldPhysics?.runtimeType)
        return true;
      newPhysics = newPhysics?.parent;
      oldPhysics = oldPhysics?.parent;
    } while (newPhysics != null || oldPhysics != null);

    return widget.controller?.runtimeType != oldWidget.controller?.runtimeType;
352 353
  }

354
  @override
355 356
  void didUpdateWidget(Scrollable oldWidget) {
    super.didUpdateWidget(oldWidget);
357

358 359 360
    if (widget.controller != oldWidget.controller) {
      oldWidget.controller?.detach(position);
      widget.controller?.attach(position);
361 362
    }

363
    if (_shouldUpdatePosition(oldWidget))
364 365 366 367 368
      _updatePosition();
  }

  @override
  void dispose() {
369
    widget.controller?.detach(position);
370 371 372 373 374
    position.dispose();
    super.dispose();
  }


375 376
  // SEMANTICS

377
  final GlobalKey _scrollSemanticsKey = GlobalKey();
378 379 380 381 382 383 384 385 386

  @override
  @protected
  void setSemanticsActions(Set<SemanticsAction> actions) {
    if (_gestureDetectorKey.currentState != null)
      _gestureDetectorKey.currentState.replaceSemanticsActions(actions);
  }


387 388
  // GESTURE RECOGNITION AND POINTER IGNORING

389 390
  final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = GlobalKey<RawGestureDetectorState>();
  final GlobalKey _ignorePointerKey = GlobalKey();
391 392 393 394 395 396 397 398

  // This field is set during layout, and then reused until the next time it is set.
  Map<Type, GestureRecognizerFactory> _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
  bool _shouldIgnorePointer = false;

  bool _lastCanDrag;
  Axis _lastAxisDirection;

399 400 401
  @override
  @protected
  void setCanDrag(bool canDrag) {
402
    if (canDrag == _lastCanDrag && (!canDrag || widget.axis == _lastAxisDirection))
403 404 405 406
      return;
    if (!canDrag) {
      _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
    } else {
407
      switch (widget.axis) {
408 409
        case Axis.vertical:
          _gestureRecognizers = <Type, GestureRecognizerFactory>{
410 411
            VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
              () => VerticalDragGestureRecognizer(),
412 413 414 415 416 417 418 419 420
              (VerticalDragGestureRecognizer instance) {
                instance
                  ..onDown = _handleDragDown
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel
                  ..minFlingDistance = _physics?.minFlingDistance
                  ..minFlingVelocity = _physics?.minFlingVelocity
421 422
                  ..maxFlingVelocity = _physics?.maxFlingVelocity
                  ..dragStartBehavior = widget.dragStartBehavior;
423 424
              },
            ),
425 426 427 428
          };
          break;
        case Axis.horizontal:
          _gestureRecognizers = <Type, GestureRecognizerFactory>{
429 430
            HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
              () => HorizontalDragGestureRecognizer(),
431 432 433 434 435 436 437 438 439
              (HorizontalDragGestureRecognizer instance) {
                instance
                  ..onDown = _handleDragDown
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel
                  ..minFlingDistance = _physics?.minFlingDistance
                  ..minFlingVelocity = _physics?.minFlingVelocity
440 441
                  ..maxFlingVelocity = _physics?.maxFlingVelocity
                  ..dragStartBehavior = widget.dragStartBehavior;
442 443
              },
            ),
444 445 446 447 448
          };
          break;
      }
    }
    _lastCanDrag = canDrag;
449
    _lastAxisDirection = widget.axis;
450 451 452 453
    if (_gestureDetectorKey.currentState != null)
      _gestureDetectorKey.currentState.replaceGestureRecognizers(_gestureRecognizers);
  }

454 455 456 457 458 459
  @override
  TickerProvider get vsync => this;

  @override
  @protected
  void setIgnorePointer(bool value) {
460 461 462 463
    if (_shouldIgnorePointer == value)
      return;
    _shouldIgnorePointer = value;
    if (_ignorePointerKey.currentContext != null) {
464
      final RenderIgnorePointer renderBox = _ignorePointerKey.currentContext.findRenderObject();
465 466 467 468
      renderBox.ignoring = _shouldIgnorePointer;
    }
  }

469
  @override
470
  BuildContext get notificationContext => _gestureDetectorKey.currentContext;
471

472 473 474
  @override
  BuildContext get storageContext => context;

475 476
  // TOUCH HANDLERS

477
  Drag _drag;
478
  ScrollHoldController _hold;
479 480 481

  void _handleDragDown(DragDownDetails details) {
    assert(_drag == null);
482 483
    assert(_hold == null);
    _hold = position.hold(_disposeHold);
484 485 486
  }

  void _handleDragStart(DragStartDetails details) {
Ian Hickson's avatar
Ian Hickson committed
487 488 489
    // It's possible for _hold to become null between _handleDragDown and
    // _handleDragStart, for example if some user code calls jumpTo or otherwise
    // triggers a new activity to begin.
490
    assert(_drag == null);
491
    _drag = position.drag(details, _disposeDrag);
492
    assert(_drag != null);
493
    assert(_hold == null);
494 495 496
  }

  void _handleDragUpdate(DragUpdateDetails details) {
497
    // _drag might be null if the drag activity ended and called _disposeDrag.
498
    assert(_hold == null || _drag == null);
499
    _drag?.update(details);
500 501 502
  }

  void _handleDragEnd(DragEndDetails details) {
503
    // _drag might be null if the drag activity ended and called _disposeDrag.
504
    assert(_hold == null || _drag == null);
505
    _drag?.end(details);
506 507 508
    assert(_drag == null);
  }

509
  void _handleDragCancel() {
510
    // _hold might be null if the drag started.
511
    // _drag might be null if the drag activity ended and called _disposeDrag.
512 513
    assert(_hold == null || _drag == null);
    _hold?.cancel();
514
    _drag?.cancel();
515
    assert(_hold == null);
516 517 518
    assert(_drag == null);
  }

519 520 521 522
  void _disposeHold() {
    _hold = null;
  }

523
  void _disposeDrag() {
524 525 526
    _drag = null;
  }

527 528 529 530 531
  // SCROLL WHEEL

  // Returns the offset that should result from applying [event] to the current
  // position, taking min/max scroll extent into account.
  double _targetScrollOffsetForPointerScroll(PointerScrollEvent event) {
532
    double delta = widget.axis == Axis.horizontal
533 534
        ? event.scrollDelta.dx
        : event.scrollDelta.dy;
535 536 537 538 539

    if (axisDirectionIsReversed(widget.axisDirection)) {
      delta *= -1;
    }

540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
    return math.min(math.max(position.pixels + delta, position.minScrollExtent),
        position.maxScrollExtent);
  }

  void _receivedPointerSignal(PointerSignalEvent event) {
    if (event is PointerScrollEvent && position != null) {
      final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event);
      // Only express interest in the event if it would actually result in a scroll.
      if (targetScrollOffset != position.pixels) {
        GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
      }
    }
  }

  void _handlePointerScroll(PointerEvent event) {
    assert(event is PointerScrollEvent);
556 557 558
    if (_physics != null && !_physics.shouldAcceptUserOffset(position)) {
      return;
    }
559 560 561 562 563
    final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event);
    if (targetScrollOffset != position.pixels) {
      position.jumpTo(targetScrollOffset);
    }
  }
564 565 566 567 568 569

  // DESCRIPTION

  @override
  Widget build(BuildContext context) {
    assert(position != null);
570 571 572 573 574 575 576 577 578 579 580 581
    // _ScrollableScope must be placed above the BuildContext returned by notificationContext
    // so that we can get this ScrollableState by doing the following:
    //
    // ScrollNotification notification;
    // Scrollable.of(notification.context)
    //
    // Since notificationContext is pointing to _gestureDetectorKey.context, _ScrollableScope
    // must be placed above the widget using it: RawGestureDetector
    Widget result = _ScrollableScope(
      scrollable: this,
      position: position,
      // TODO(ianh): Having all these global keys is sad.
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
      child: Listener(
        onPointerSignal: _receivedPointerSignal,
        child: RawGestureDetector(
          key: _gestureDetectorKey,
          gestures: _gestureRecognizers,
          behavior: HitTestBehavior.opaque,
          excludeFromSemantics: widget.excludeFromSemantics,
          child: Semantics(
            explicitChildNodes: !widget.excludeFromSemantics,
            child: IgnorePointer(
              key: _ignorePointerKey,
              ignoring: _shouldIgnorePointer,
              ignoringSemantics: false,
              child: widget.viewportBuilder(context, position),
            ),
597
          ),
598
        ),
599 600
      ),
    );
601 602

    if (!widget.excludeFromSemantics) {
603
      result = _ScrollSemantics(
Ian Hickson's avatar
Ian Hickson committed
604
        key: _scrollSemanticsKey,
605
        child: result,
606
        position: position,
607
        allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? _physics.allowImplicitScrolling,
608
        semanticChildCount: widget.semanticChildCount,
609 610 611
      );
    }

612
    return _configuration.buildViewportChrome(context, result, widget.axisDirection);
613 614 615
  }

  @override
616 617
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
618
    properties.add(DiagnosticsProperty<ScrollPosition>('position', position));
619 620
  }
}
621

Ian Hickson's avatar
Ian Hickson committed
622
/// With [_ScrollSemantics] certain child [SemanticsNode]s can be
623 624 625 626 627 628 629 630 631 632 633 634 635
/// excluded from the scrollable area for semantics purposes.
///
/// Nodes, that are to be excluded, have to be tagged with
/// [RenderViewport.excludeFromScrolling] and the [RenderAbstractViewport] in
/// use has to add the [RenderViewport.useTwoPaneSemantics] tag to its
/// [SemanticsConfiguration] by overriding
/// [RenderObject.describeSemanticsConfiguration].
///
/// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport,
/// two semantics nodes will be used to represent the [Scrollable]: The outer
/// node will contain all children, that are excluded from scrolling. The inner
/// node, which is annotated with the scrolling actions, will house the
/// scrollable children.
Ian Hickson's avatar
Ian Hickson committed
636 637
class _ScrollSemantics extends SingleChildRenderObjectWidget {
  const _ScrollSemantics({
638 639
    Key key,
    @required this.position,
640
    @required this.allowImplicitScrolling,
641
    @required this.semanticChildCount,
642
    Widget child,
643 644
  }) : assert(position != null),
       super(key: key, child: child);
645 646

  final ScrollPosition position;
647
  final bool allowImplicitScrolling;
648
  final int semanticChildCount;
649 650

  @override
Ian Hickson's avatar
Ian Hickson committed
651
  _RenderScrollSemantics createRenderObject(BuildContext context) {
652
    return _RenderScrollSemantics(
653 654
      position: position,
      allowImplicitScrolling: allowImplicitScrolling,
655
      semanticChildCount: semanticChildCount,
656 657 658
    );
  }

659
  @override
Ian Hickson's avatar
Ian Hickson committed
660
  void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) {
661 662
    renderObject
      ..allowImplicitScrolling = allowImplicitScrolling
663 664
      ..position = position
      ..semanticChildCount = semanticChildCount;
665
  }
666 667
}

Ian Hickson's avatar
Ian Hickson committed
668 669
class _RenderScrollSemantics extends RenderProxyBox {
  _RenderScrollSemantics({
670
    @required ScrollPosition position,
671
    @required bool allowImplicitScrolling,
672
    @required int semanticChildCount,
673
    RenderBox child,
674 675
  }) : _position = position,
       _allowImplicitScrolling = allowImplicitScrolling,
676
       _semanticChildCount = semanticChildCount,
677 678
       assert(position != null),
       super(child) {
679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
    position.addListener(markNeedsSemanticsUpdate);
  }

  /// Whether this render object is excluded from the semantic tree.
  ScrollPosition get position => _position;
  ScrollPosition _position;
  set position(ScrollPosition value) {
    assert(value != null);
    if (value == _position)
      return;
    _position.removeListener(markNeedsSemanticsUpdate);
    _position = value;
    _position.addListener(markNeedsSemanticsUpdate);
    markNeedsSemanticsUpdate();
  }
694

695 696 697 698 699 700 701 702 703 704
  /// Whether this node can be scrolled implicitly.
  bool get allowImplicitScrolling => _allowImplicitScrolling;
  bool _allowImplicitScrolling;
  set allowImplicitScrolling(bool value) {
    if (value == _allowImplicitScrolling)
      return;
    _allowImplicitScrolling = value;
    markNeedsSemanticsUpdate();
  }

705 706 707 708 709 710 711 712 713
  int get semanticChildCount => _semanticChildCount;
  int _semanticChildCount;
  set semanticChildCount(int value) {
    if (value == semanticChildCount)
      return;
    _semanticChildCount = value;
    markNeedsSemanticsUpdate();
  }

714 715 716 717
  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.isSemanticBoundary = true;
718 719
    if (position.haveDimensions) {
      config
720
          ..hasImplicitScrolling = allowImplicitScrolling
721 722
          ..scrollPosition = _position.pixels
          ..scrollExtentMax = _position.maxScrollExtent
723 724
          ..scrollExtentMin = _position.minScrollExtent
          ..scrollChildCount = semanticChildCount;
725
    }
726 727 728 729 730 731 732 733 734 735 736
  }

  SemanticsNode _innerNode;

  @override
  void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
    if (children.isEmpty || !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) {
      super.assembleSemanticsNode(node, config, children);
      return;
    }

737
    _innerNode ??= SemanticsNode(showOnScreen: showOnScreen);
738 739 740 741
    _innerNode
      ..isMergedIntoParent = node.isPartOfNodeMerging
      ..rect = Offset.zero & node.rect.size;

742
    int firstVisibleIndex;
743 744 745 746
    final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
    final List<SemanticsNode> included = <SemanticsNode>[];
    for (SemanticsNode child in children) {
      assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
747
      if (child.isTagged(RenderViewport.excludeFromScrolling)) {
748
        excluded.add(child);
749
      } else {
750 751
        if (!child.hasFlag(SemanticsFlag.isHidden))
          firstVisibleIndex ??= child.indexInParent;
752
        included.add(child);
753
      }
754
    }
755
    config.scrollIndex = firstVisibleIndex;
756 757 758 759
    node.updateWith(config: null, childrenInInversePaintOrder: excluded);
    _innerNode.updateWith(config: config, childrenInInversePaintOrder: included);
  }

760 761 762 763
  @override
  void clearSemantics() {
    super.clearSemantics();
    _innerNode = null;
764 765
  }
}