scrollable.dart 9.84 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

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/gestures.dart';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/scheduler.dart';
11 12 13 14

import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
15
import 'notification_listener.dart';
16
import 'scroll_configuration.dart';
17
import 'scroll_controller.dart';
18
import 'scroll_notification.dart';
19
import 'scroll_position.dart';
20
import 'ticker_provider.dart';
21 22 23 24
import 'viewport.dart';

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

25 26
typedef Widget ViewportBuilder(BuildContext context, ViewportOffset position);

Adam Barth's avatar
Adam Barth committed
27 28
class Scrollable extends StatefulWidget {
  Scrollable({
29 30
    Key key,
    this.axisDirection: AxisDirection.down,
31
    this.controller,
Adam Barth's avatar
Adam Barth committed
32
    this.physics,
33 34 35 36 37 38 39 40
    @required this.viewportBuilder,
  }) : super (key: key) {
    assert(axisDirection != null);
    assert(viewportBuilder != null);
  }

  final AxisDirection axisDirection;

41 42
  final ScrollController controller;

Adam Barth's avatar
Adam Barth committed
43 44
  final ScrollPhysics physics;

45
  final ViewportBuilder viewportBuilder;
46 47 48 49

  Axis get axis => axisDirectionToAxis(axisDirection);

  @override
Adam Barth's avatar
Adam Barth committed
50
  ScrollableState createState() => new ScrollableState();
51 52 53 54 55

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$axisDirection');
56 57
    if (physics != null)
      description.add('physics: $physics');
58
  }
59 60 61 62 63 64

  /// 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
65
  /// ScrollableState scrollable = Scrollable.of(context);
66
  /// ```
Adam Barth's avatar
Adam Barth committed
67 68
  static ScrollableState of(BuildContext context) {
    return context.ancestorStateOfType(const TypeMatcher<ScrollableState>());
69 70 71 72 73 74 75 76 77 78
  }

  /// Scrolls the closest enclosing scrollable to make the given context visible.
  static Future<Null> ensureVisible(BuildContext context, {
    double alignment: 0.0,
    Duration duration: Duration.ZERO,
    Curve curve: Curves.ease,
  }) {
    final List<Future<Null>> futures = <Future<Null>>[];

Adam Barth's avatar
Adam Barth committed
79
    ScrollableState scrollable = Scrollable.of(context);
80
    while (scrollable != null) {
81 82 83 84 85 86
      futures.add(scrollable.position.ensureVisible(
        context.findRenderObject(),
        alignment: alignment,
        duration: duration,
        curve: curve,
      ));
87
      context = scrollable.context;
Adam Barth's avatar
Adam Barth committed
88
      scrollable = Scrollable.of(context);
89 90 91 92 93 94 95 96
    }

    if (futures.isEmpty || duration == Duration.ZERO)
      return new Future<Null>.value();
    if (futures.length == 1)
      return futures.first;
    return Future.wait<Null>(futures);
  }
97 98
}

Adam Barth's avatar
Adam Barth committed
99
/// State object for a [Scrollable] widget.
100
///
Adam Barth's avatar
Adam Barth committed
101
/// To manipulate a [Scrollable] widget's scroll position, use the object
102 103
/// obtained from the [position] property.
///
Adam Barth's avatar
Adam Barth committed
104 105
/// To be informed of when a [Scrollable] widget is scrolling, use a
/// [NotificationListener] to listen for [ScrollNotification] notifications.
106 107
///
/// This class is not intended to be subclassed. To specialize the behavior of a
Adam Barth's avatar
Adam Barth committed
108 109
/// [Scrollable], provide it with a [ScrollPhysics].
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
110
    implements AbstractScrollState {
Adam Barth's avatar
Adam Barth committed
111
  /// The controller for this [Scrollable] widget's viewport position.
112
  ///
Adam Barth's avatar
Adam Barth committed
113
  /// To control what kind of [ScrollPosition] is created for a [Scrollable],
114 115 116
  /// provide it with custom [ScrollPhysics] that creates the appropriate
  /// [ScrollPosition] controller in its [ScrollPhysics.createScrollPosition]
  /// method.
117 118 119
  ScrollPosition get position => _position;
  ScrollPosition _position;

Adam Barth's avatar
Adam Barth committed
120
  ScrollBehavior _configuration;
121 122 123

  // only call this from places that will definitely trigger a rebuild
  void _updatePosition() {
Adam Barth's avatar
Adam Barth committed
124
    _configuration = ScrollConfiguration.of(context);
125 126 127
    ScrollPhysics physics = _configuration.getScrollPhysics(context);
    if (config.physics != null)
      physics = config.physics.applyTo(physics);
128
    final ScrollController controller = config.controller;
129 130
    final ScrollPosition oldPosition = position;
    if (oldPosition != null) {
131
      controller?.detach(oldPosition);
132 133 134
      // 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.
135 136
      scheduleMicrotask(oldPosition.dispose);
    }
137 138 139

    _position = controller?.createScrollPosition(physics, this, oldPosition)
      ?? ScrollController.createDefaultScrollPosition(physics, this, oldPosition);
140
    assert(position != null);
141
    controller?.attach(position);
142 143 144 145 146 147 148 149
  }

  @override
  void dependenciesChanged() {
    super.dependenciesChanged();
    _updatePosition();
  }

Adam Barth's avatar
Adam Barth committed
150
  bool _shouldUpdatePosition(Scrollable oldConfig) {
151
    return config.physics?.runtimeType != oldConfig.physics?.runtimeType
152
        || config.controller?.runtimeType != oldConfig.controller?.runtimeType;
153 154
  }

155
  @override
Adam Barth's avatar
Adam Barth committed
156
  void didUpdateConfig(Scrollable oldConfig) {
157
    super.didUpdateConfig(oldConfig);
158 159 160 161 162 163

    if (config.controller != oldConfig.controller) {
      oldConfig.controller?.detach(position);
      config.controller?.attach(position);
    }

164
    if (_shouldUpdatePosition(oldConfig))
165 166 167 168 169
      _updatePosition();
  }

  @override
  void dispose() {
170
    config.controller?.detach(position);
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
    position.dispose();
    super.dispose();
  }


  // GESTURE RECOGNITION AND POINTER IGNORING

  final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = new GlobalKey<RawGestureDetectorState>();
  final GlobalKey _ignorePointerKey = new GlobalKey();

  // 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;

188 189 190
  @override
  @protected
  void setCanDrag(bool canDrag) {
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
    if (canDrag == _lastCanDrag && (!canDrag || config.axis == _lastAxisDirection))
      return;
    if (!canDrag) {
      _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
    } else {
      switch (config.axis) {
        case Axis.vertical:
          _gestureRecognizers = <Type, GestureRecognizerFactory>{
            VerticalDragGestureRecognizer: (VerticalDragGestureRecognizer recognizer) {  // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/7173
              return (recognizer ??= new VerticalDragGestureRecognizer())
                ..onDown = _handleDragDown
                ..onStart = _handleDragStart
                ..onUpdate = _handleDragUpdate
                ..onEnd = _handleDragEnd;
            }
          };
          break;
        case Axis.horizontal:
          _gestureRecognizers = <Type, GestureRecognizerFactory>{
            HorizontalDragGestureRecognizer: (HorizontalDragGestureRecognizer recognizer) {  // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/7173
              return (recognizer ??= new HorizontalDragGestureRecognizer())
                ..onDown = _handleDragDown
                ..onStart = _handleDragStart
                ..onUpdate = _handleDragUpdate
                ..onEnd = _handleDragEnd;
            }
          };
          break;
      }
    }
    _lastCanDrag = canDrag;
    _lastAxisDirection = config.axis;
    if (_gestureDetectorKey.currentState != null)
      _gestureDetectorKey.currentState.replaceGestureRecognizers(_gestureRecognizers);
  }

227 228 229 230 231 232
  @override
  TickerProvider get vsync => this;

  @override
  @protected
  void setIgnorePointer(bool value) {
233 234 235 236
    if (_shouldIgnorePointer == value)
      return;
    _shouldIgnorePointer = value;
    if (_ignorePointerKey.currentContext != null) {
237
      final RenderIgnorePointer renderBox = _ignorePointerKey.currentContext.findRenderObject();
238 239 240 241
      renderBox.ignoring = _shouldIgnorePointer;
    }
  }

242 243 244 245 246 247
  @override
  @protected
  void dispatchNotification(Notification notification) {
    assert(mounted);
    notification.dispatch(_gestureDetectorKey.currentContext);
  }
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273

  // TOUCH HANDLERS

  DragScrollActivity _drag;

  bool get _reverseDirection {
    assert(config.axisDirection != null);
    switch (config.axisDirection) {
      case AxisDirection.up:
      case AxisDirection.left:
        return true;
      case AxisDirection.down:
      case AxisDirection.right:
        return false;
    }
    return null;
  }

  void _handleDragDown(DragDownDetails details) {
    assert(_drag == null);
    position.touched();
  }

  void _handleDragStart(DragStartDetails details) {
    assert(_drag == null);
    _drag = position.beginDragActivity(details);
274
    assert(_drag != null);
275 276 277
  }

  void _handleDragUpdate(DragUpdateDetails details) {
278 279
    // _drag might be null if the drag activity ended and called didEndDrag.
    _drag?.update(details, reverse: _reverseDirection);
280 281 282
  }

  void _handleDragEnd(DragEndDetails details) {
283 284
    // _drag might be null if the drag activity ended and called didEndDrag.
    _drag?.end(details, reverse: _reverseDirection);
285 286 287
    assert(_drag == null);
  }

288 289 290 291 292 293
  @override
  @protected
  void didEndDrag() {
    _drag = null;
  }

294 295 296 297 298 299 300

  // DESCRIPTION

  @override
  Widget build(BuildContext context) {
    assert(position != null);
    // TODO(ianh): Having all these global keys is sad.
301
    final Widget result = new RawGestureDetector(
302 303 304 305 306 307
      key: _gestureDetectorKey,
      gestures: _gestureRecognizers,
      behavior: HitTestBehavior.opaque,
      child: new IgnorePointer(
        key: _ignorePointerKey,
        ignoring: _shouldIgnorePointer,
308
        child: config.viewportBuilder(context, position),
309 310
      ),
    );
311
    return _configuration.buildViewportChrome(context, result, config.axisDirection);
312 313 314 315 316 317 318 319
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('position: $position');
  }
}