| // 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. |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| |
| import 'basic.dart'; |
| import 'framework.dart'; |
| import 'gesture_detector.dart'; |
| import 'notification_listener.dart'; |
| import 'scroll_configuration.dart'; |
| import 'scroll_context.dart'; |
| import 'scroll_controller.dart'; |
| import 'scroll_physics.dart'; |
| import 'scroll_position.dart'; |
| import 'scroll_position_with_single_context.dart'; |
| import 'ticker_provider.dart'; |
| import 'viewport.dart'; |
| |
| export 'package:flutter/physics.dart' show Tolerance; |
| |
| /// Signature used by [Scrollable] to build the viewport through which the |
| /// scrollable content is displayed. |
| typedef Widget ViewportBuilder(BuildContext context, ViewportOffset position); |
| |
| /// 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. |
| /// * [ScrollNotification] and [NotificationListener], which can be used to watch |
| /// the scroll position without using a [ScrollController]. |
| class Scrollable extends StatefulWidget { |
| /// Creates a widget that scrolls. |
| /// |
| /// The [axisDirection] and [viewportBuilder] arguments must not be null. |
| const Scrollable({ |
| Key key, |
| this.axisDirection: AxisDirection.down, |
| this.controller, |
| this.physics, |
| @required this.viewportBuilder, |
| this.excludeFromSemantics: false, |
| }) : assert(axisDirection != null), |
| assert(viewportBuilder != null), |
| assert(excludeFromSemantics != null), |
| super (key: key); |
| |
| /// 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]. |
| final AxisDirection axisDirection; |
| |
| /// An object that can be used to control the position to which this widget is |
| /// scrolled. |
| /// |
| /// 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]). |
| /// |
| /// See also: |
| /// |
| /// * [ensureVisible], which animates the scroll position to reveal a given |
| /// [BuildContext]. |
| final ScrollController controller; |
| |
| /// 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. |
| /// |
| /// 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. |
| final ScrollPhysics physics; |
| |
| /// 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. |
| final ViewportBuilder viewportBuilder; |
| |
| /// 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; |
| |
| /// The axis along which the scroll view scrolls. |
| /// |
| /// Determined by the [axisDirection]. |
| Axis get axis => axisDirectionToAxis(axisDirection); |
| |
| @override |
| ScrollableState createState() => new ScrollableState(); |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(new EnumProperty<AxisDirection>('axisDirection', axisDirection)); |
| properties.add(new DiagnosticsProperty<ScrollPhysics>('physics', physics)); |
| } |
| |
| /// The state from the closest instance of this class that encloses the given context. |
| /// |
| /// Typical usage is as follows: |
| /// |
| /// ```dart |
| /// ScrollableState scrollable = Scrollable.of(context); |
| /// ``` |
| static ScrollableState of(BuildContext context) { |
| final _ScrollableScope widget = context.inheritFromWidgetOfExactType(_ScrollableScope); |
| return widget?.scrollable; |
| } |
| |
| /// Scrolls the scrollables that enclose the given context so as 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>>[]; |
| |
| ScrollableState scrollable = Scrollable.of(context); |
| while (scrollable != null) { |
| futures.add(scrollable.position.ensureVisible( |
| context.findRenderObject(), |
| alignment: alignment, |
| duration: duration, |
| curve: curve, |
| )); |
| context = scrollable.context; |
| scrollable = Scrollable.of(context); |
| } |
| |
| if (futures.isEmpty || duration == Duration.zero) |
| return new Future<Null>.value(); |
| if (futures.length == 1) |
| return futures.single; |
| return Future.wait<Null>(futures).then((List<Null> _) => null); |
| } |
| } |
| |
| // Enable Scrollable.of() to work as if ScrollableState was an inherited widget. |
| // ScrollableState.build() always rebuilds its _ScrollableScope. |
| class _ScrollableScope extends InheritedWidget { |
| const _ScrollableScope({ |
| Key key, |
| @required this.scrollable, |
| @required this.position, |
| @required Widget child |
| }) : assert(scrollable != null), |
| assert(child != null), |
| super(key: key, child: child); |
| |
| final ScrollableState scrollable; |
| final ScrollPosition position; |
| |
| @override |
| bool updateShouldNotify(_ScrollableScope old) { |
| return position != old.position; |
| } |
| } |
| |
| /// State object for a [Scrollable] widget. |
| /// |
| /// To manipulate a [Scrollable] widget's scroll position, use the object |
| /// obtained from the [position] property. |
| /// |
| /// To be informed of when a [Scrollable] widget is scrolling, use a |
| /// [NotificationListener] to listen for [ScrollNotification] notifications. |
| /// |
| /// This class is not intended to be subclassed. To specialize the behavior of a |
| /// [Scrollable], provide it with a [ScrollPhysics]. |
| class ScrollableState extends State<Scrollable> with TickerProviderStateMixin |
| implements ScrollContext { |
| /// The manager for this [Scrollable] widget's viewport position. |
| /// |
| /// To control what kind of [ScrollPosition] is created for a [Scrollable], |
| /// provide it with custom [ScrollController] that creates the appropriate |
| /// [ScrollPosition] in its [ScrollController.createScrollPosition] method. |
| ScrollPosition get position => _position; |
| ScrollPosition _position; |
| |
| @override |
| AxisDirection get axisDirection => widget.axisDirection; |
| |
| ScrollBehavior _configuration; |
| ScrollPhysics _physics; |
| |
| // Only call this from places that will definitely trigger a rebuild. |
| void _updatePosition() { |
| _configuration = ScrollConfiguration.of(context); |
| _physics = _configuration.getScrollPhysics(context); |
| if (widget.physics != null) |
| _physics = widget.physics.applyTo(_physics); |
| final ScrollController controller = widget.controller; |
| final ScrollPosition oldPosition = position; |
| if (oldPosition != null) { |
| controller?.detach(oldPosition); |
| // 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. |
| scheduleMicrotask(oldPosition.dispose); |
| } |
| |
| _position = controller?.createScrollPosition(_physics, this, oldPosition) |
| ?? new ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition); |
| assert(position != null); |
| controller?.attach(position); |
| } |
| |
| @override |
| void didChangeDependencies() { |
| super.didChangeDependencies(); |
| _updatePosition(); |
| } |
| |
| bool _shouldUpdatePosition(Scrollable oldWidget) { |
| 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; |
| } |
| |
| @override |
| void didUpdateWidget(Scrollable oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| |
| if (widget.controller != oldWidget.controller) { |
| oldWidget.controller?.detach(position); |
| widget.controller?.attach(position); |
| } |
| |
| if (_shouldUpdatePosition(oldWidget)) |
| _updatePosition(); |
| } |
| |
| @override |
| void dispose() { |
| widget.controller?.detach(position); |
| position.dispose(); |
| super.dispose(); |
| } |
| |
| |
| // SEMANTICS |
| |
| final GlobalKey _excludableScrollSemanticsKey = new GlobalKey(); |
| |
| @override |
| @protected |
| void setSemanticsActions(Set<SemanticsAction> actions) { |
| if (_gestureDetectorKey.currentState != null) |
| _gestureDetectorKey.currentState.replaceSemanticsActions(actions); |
| } |
| |
| |
| // 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; |
| |
| @override |
| @protected |
| void setCanDrag(bool canDrag) { |
| if (canDrag == _lastCanDrag && (!canDrag || widget.axis == _lastAxisDirection)) |
| return; |
| if (!canDrag) { |
| _gestureRecognizers = const <Type, GestureRecognizerFactory>{}; |
| } else { |
| switch (widget.axis) { |
| case Axis.vertical: |
| _gestureRecognizers = <Type, GestureRecognizerFactory>{ |
| VerticalDragGestureRecognizer: new GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>( |
| () => new VerticalDragGestureRecognizer(), |
| (VerticalDragGestureRecognizer instance) { |
| instance |
| ..onDown = _handleDragDown |
| ..onStart = _handleDragStart |
| ..onUpdate = _handleDragUpdate |
| ..onEnd = _handleDragEnd |
| ..onCancel = _handleDragCancel |
| ..minFlingDistance = _physics?.minFlingDistance |
| ..minFlingVelocity = _physics?.minFlingVelocity |
| ..maxFlingVelocity = _physics?.maxFlingVelocity; |
| }, |
| ), |
| }; |
| break; |
| case Axis.horizontal: |
| _gestureRecognizers = <Type, GestureRecognizerFactory>{ |
| HorizontalDragGestureRecognizer: new GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>( |
| () => new HorizontalDragGestureRecognizer(), |
| (HorizontalDragGestureRecognizer instance) { |
| instance |
| ..onDown = _handleDragDown |
| ..onStart = _handleDragStart |
| ..onUpdate = _handleDragUpdate |
| ..onEnd = _handleDragEnd |
| ..onCancel = _handleDragCancel |
| ..minFlingDistance = _physics?.minFlingDistance |
| ..minFlingVelocity = _physics?.minFlingVelocity |
| ..maxFlingVelocity = _physics?.maxFlingVelocity; |
| }, |
| ), |
| }; |
| break; |
| } |
| } |
| _lastCanDrag = canDrag; |
| _lastAxisDirection = widget.axis; |
| if (_gestureDetectorKey.currentState != null) |
| _gestureDetectorKey.currentState.replaceGestureRecognizers(_gestureRecognizers); |
| } |
| |
| @override |
| TickerProvider get vsync => this; |
| |
| @override |
| @protected |
| void setIgnorePointer(bool value) { |
| if (_shouldIgnorePointer == value) |
| return; |
| _shouldIgnorePointer = value; |
| if (_ignorePointerKey.currentContext != null) { |
| final RenderIgnorePointer renderBox = _ignorePointerKey.currentContext.findRenderObject(); |
| renderBox.ignoring = _shouldIgnorePointer; |
| } |
| } |
| |
| @override |
| BuildContext get notificationContext => _gestureDetectorKey.currentContext; |
| |
| @override |
| BuildContext get storageContext => context; |
| |
| // TOUCH HANDLERS |
| |
| Drag _drag; |
| ScrollHoldController _hold; |
| |
| void _handleDragDown(DragDownDetails details) { |
| assert(_drag == null); |
| assert(_hold == null); |
| _hold = position.hold(_disposeHold); |
| } |
| |
| void _handleDragStart(DragStartDetails details) { |
| // 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. |
| assert(_drag == null); |
| _drag = position.drag(details, _disposeDrag); |
| assert(_drag != null); |
| assert(_hold == null); |
| } |
| |
| void _handleDragUpdate(DragUpdateDetails details) { |
| // _drag might be null if the drag activity ended and called _disposeDrag. |
| assert(_hold == null || _drag == null); |
| _drag?.update(details); |
| } |
| |
| void _handleDragEnd(DragEndDetails details) { |
| // _drag might be null if the drag activity ended and called _disposeDrag. |
| assert(_hold == null || _drag == null); |
| _drag?.end(details); |
| assert(_drag == null); |
| } |
| |
| void _handleDragCancel() { |
| // _hold might be null if the drag started. |
| // _drag might be null if the drag activity ended and called _disposeDrag. |
| assert(_hold == null || _drag == null); |
| _hold?.cancel(); |
| _drag?.cancel(); |
| assert(_hold == null); |
| assert(_drag == null); |
| } |
| |
| void _disposeHold() { |
| _hold = null; |
| } |
| |
| void _disposeDrag() { |
| _drag = null; |
| } |
| |
| |
| // DESCRIPTION |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(position != null); |
| // TODO(ianh): Having all these global keys is sad. |
| Widget result = new RawGestureDetector( |
| key: _gestureDetectorKey, |
| gestures: _gestureRecognizers, |
| behavior: HitTestBehavior.opaque, |
| excludeFromSemantics: widget.excludeFromSemantics, |
| child: new Semantics( |
| explicitChildNodes: !widget.excludeFromSemantics, |
| child: new IgnorePointer( |
| key: _ignorePointerKey, |
| ignoring: _shouldIgnorePointer, |
| ignoringSemantics: false, |
| child: new _ScrollableScope( |
| scrollable: this, |
| position: position, |
| child: widget.viewportBuilder(context, position), |
| ), |
| ), |
| ), |
| ); |
| |
| if (!widget.excludeFromSemantics) { |
| result = new _ExcludableScrollSemantics( |
| key: _excludableScrollSemanticsKey, |
| child: result, |
| position: position, |
| ); |
| } |
| |
| return _configuration.buildViewportChrome(context, result, widget.axisDirection); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(new DiagnosticsProperty<ScrollPosition>('position', position)); |
| } |
| } |
| |
| /// With [_ExcludableScrollSemantics] certain child [SemanticsNode]s can be |
| /// 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. |
| class _ExcludableScrollSemantics extends SingleChildRenderObjectWidget { |
| const _ExcludableScrollSemantics({ |
| Key key, |
| @required this.position, |
| Widget child |
| }) : assert(position != null), super(key: key, child: child); |
| |
| final ScrollPosition position; |
| |
| @override |
| _RenderExcludableScrollSemantics createRenderObject(BuildContext context) => new _RenderExcludableScrollSemantics(position: position); |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderExcludableScrollSemantics renderObject) { |
| renderObject.position = position; |
| } |
| } |
| |
| class _RenderExcludableScrollSemantics extends RenderProxyBox { |
| _RenderExcludableScrollSemantics({ |
| @required ScrollPosition position, |
| RenderBox child, |
| }) : _position = position, assert(position != null), super(child) { |
| 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(); |
| } |
| |
| @override |
| void describeSemanticsConfiguration(SemanticsConfiguration config) { |
| super.describeSemanticsConfiguration(config); |
| config.isSemanticBoundary = true; |
| if (position.haveDimensions) { |
| config |
| ..scrollPosition = _position.pixels |
| ..scrollExtentMax = _position.maxScrollExtent |
| ..scrollExtentMin = _position.minScrollExtent; |
| } |
| } |
| |
| 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; |
| } |
| |
| _innerNode ??= new SemanticsNode(showOnScreen: showOnScreen); |
| _innerNode |
| ..isMergedIntoParent = node.isPartOfNodeMerging |
| ..rect = Offset.zero & node.rect.size; |
| |
| final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode]; |
| final List<SemanticsNode> included = <SemanticsNode>[]; |
| for (SemanticsNode child in children) { |
| assert(child.isTagged(RenderViewport.useTwoPaneSemantics)); |
| if (child.isTagged(RenderViewport.excludeFromScrolling)) |
| excluded.add(child); |
| else |
| included.add(child); |
| } |
| node.updateWith(config: null, childrenInInversePaintOrder: excluded); |
| _innerNode.updateWith(config: config, childrenInInversePaintOrder: included); |
| } |
| |
| @override |
| void clearSemantics() { |
| super.clearSemantics(); |
| _innerNode = null; |
| } |
| } |