blob: f5155c2f303a8378895ff0241f7a82d672159a93 [file] [log] [blame]
// 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;
}
}