// 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/foundation.dart'; 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 description) { super.debugFillProperties(description); description.add(new EnumProperty<AxisDirection>('axisDirection', axisDirection)); description.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 description) { super.debugFillProperties(description); description.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; } }