Commit 90574b04 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Remove Scrollable1 (#8225)

All the clients have migrated to Scrollable2.
parent 9ec5330f
......@@ -321,7 +321,7 @@ class CardCollectionState extends State<CardCollection> {
Widget background = new Positioned.fill(
child: new Container(
margin: const EdgeInsets.all(4.0),
child: new Viewport(
child: new SingleChildScrollView(
child: new Container(
height: cardModel.height,
decoration: new BoxDecoration(backgroundColor: theme.primaryColor),
......
......@@ -53,7 +53,6 @@ export 'src/rendering/stack.dart';
export 'src/rendering/table.dart';
export 'src/rendering/tweens.dart';
export 'src/rendering/view.dart';
export 'src/rendering/viewport.dart';
export 'src/rendering/viewport_offset.dart';
export 'package:flutter/foundation.dart' show
......
......@@ -167,32 +167,6 @@ class MaterialApp extends StatefulWidget {
_MaterialAppState createState() => new _MaterialAppState();
}
class _ScrollLikeCupertinoDelegate extends ScrollConfigurationDelegate {
const _ScrollLikeCupertinoDelegate();
@override
TargetPlatform get platform => TargetPlatform.iOS;
@override
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: TargetPlatform.iOS);
@override
bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
}
class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate {
const _ScrollLikeMountainViewDelegate(this.platform);
@override
final TargetPlatform platform;
@override
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: TargetPlatform.android);
@override
bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
}
class _MaterialScrollBehavior extends ScrollBehavior2 {
@override
TargetPlatform getPlatform(BuildContext context) {
......@@ -233,18 +207,6 @@ class _MaterialAppState extends State<MaterialApp> {
return null;
}
ScrollConfigurationDelegate _getScrollDelegate(TargetPlatform platform) {
switch (platform) {
case TargetPlatform.android:
return const _ScrollLikeMountainViewDelegate(TargetPlatform.android);
case TargetPlatform.fuchsia:
return const _ScrollLikeMountainViewDelegate(TargetPlatform.fuchsia);
case TargetPlatform.iOS:
return const _ScrollLikeCupertinoDelegate();
}
return null;
}
@override
Widget build(BuildContext context) {
ThemeData theme = config.theme ?? new ThemeData.fallback();
......@@ -283,11 +245,6 @@ class _MaterialAppState extends State<MaterialApp> {
return true;
});
result = new ScrollConfiguration(
delegate: _getScrollDelegate(theme.platform),
child: result
);
return new ScrollConfiguration2(
behavior: new _MaterialScrollBehavior(),
child: result
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
......
// 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:ui' as ui show window;
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'object.dart';
/// The end of the viewport from which the paint offset is computed.
enum ViewportAnchor {
/// The start (e.g., top or left, depending on the axis) of the first item
/// should be aligned with the start (e.g., top or left, depending on the
/// axis) of the viewport.
start,
/// The end (e.g., bottom or right, depending on the axis) of the last item
/// should be aligned with the end (e.g., bottom or right, depending on the
/// axis) of the viewport.
end,
}
/// The interior and exterior dimensions of a viewport.
class ViewportDimensions {
/// Creates dimensions for a viewport.
///
/// By default, the content and container sizes are zero.
const ViewportDimensions({
this.contentSize: Size.zero,
this.containerSize: Size.zero
});
/// A viewport that has zero size, both inside and outside.
static const ViewportDimensions zero = const ViewportDimensions();
/// The size of the content inside the viewport.
final Size contentSize;
/// The size of the outside of the viewport.
final Size containerSize;
bool get _debugHasAtLeastOneCommonDimension {
return contentSize.width == containerSize.width
|| contentSize.height == containerSize.height;
}
/// Returns the offset at which to paint the content, accounting for the given
/// anchor and the dimensions of the viewport.
Offset getAbsolutePaintOffset({ Offset paintOffset, ViewportAnchor anchor }) {
assert(_debugHasAtLeastOneCommonDimension);
switch (anchor) {
case ViewportAnchor.start:
return paintOffset;
case ViewportAnchor.end:
return paintOffset + (containerSize - contentSize);
}
assert(anchor != null);
return null;
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! ViewportDimensions)
return false;
final ViewportDimensions typedOther = other;
return contentSize == typedOther.contentSize &&
containerSize == typedOther.containerSize;
}
@override
int get hashCode => hashValues(contentSize, containerSize);
@override
String toString() => 'ViewportDimensions(container: $containerSize, content: $contentSize)';
}
/// A base class for render objects that are bigger on the inside.
///
/// This class holds the common fields for viewport render objects but does not
/// have a child model. See [RenderViewport] for a viewport with a single child
/// and [RenderVirtualViewport] for a viewport with multiple children.
class RenderViewportBase extends RenderBox {
/// Initializes fields for subclasses.
///
/// The [paintOffset] and [mainAxis] arguments must not be null.
///
/// This constructor uses positional arguments rather than named arguments to
/// work around limitations of mixins.
RenderViewportBase(
Offset paintOffset,
Axis mainAxis,
ViewportAnchor anchor
) : _paintOffset = paintOffset,
_mainAxis = mainAxis,
_anchor = anchor {
assert(paintOffset != null);
assert(mainAxis != null);
assert(_offsetIsSane(_paintOffset, mainAxis));
}
bool _offsetIsSane(Offset offset, Axis direction) {
switch (direction) {
case Axis.horizontal:
return offset.dy == 0.0;
case Axis.vertical:
return offset.dx == 0.0;
}
assert(direction != null);
return null;
}
/// The offset at which to paint the child.
///
/// The offset can be non-zero only in the [mainAxis].
Offset get paintOffset => _paintOffset;
Offset _paintOffset;
set paintOffset(Offset value) {
assert(value != null);
if (value == _paintOffset)
return;
assert(_offsetIsSane(value, mainAxis));
_paintOffset = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The direction in which the child is permitted to be larger than the viewport.
///
/// The child is given layout constraints that are fully unconstrained along
/// the main axis (e.g., the child can be as tall as it wants if the main axis
/// is vertical).
Axis get mainAxis => _mainAxis;
Axis _mainAxis;
set mainAxis(Axis value) {
assert(value != null);
if (value == _mainAxis)
return;
assert(_offsetIsSane(_paintOffset, value));
_mainAxis = value;
markNeedsLayout();
}
/// The end of the viewport from which the paint offset is computed.
///
/// See [ViewportAnchor] for more detail.
ViewportAnchor get anchor => _anchor;
ViewportAnchor _anchor;
set anchor(ViewportAnchor value) {
assert(value != null);
if (value == _anchor)
return;
_anchor = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The interior and exterior extent of the viewport.
ViewportDimensions get dimensions => _dimensions;
ViewportDimensions _dimensions = ViewportDimensions.zero;
set dimensions(ViewportDimensions value) {
assert(debugDoingThisLayout);
_dimensions = value;
}
Offset get _effectivePaintOffset {
final double devicePixelRatio = ui.window.devicePixelRatio;
int dxInDevicePixels = (_paintOffset.dx * devicePixelRatio).round();
int dyInDevicePixels = (_paintOffset.dy * devicePixelRatio).round();
return _dimensions.getAbsolutePaintOffset(
paintOffset: new Offset(dxInDevicePixels / devicePixelRatio, dyInDevicePixels / devicePixelRatio),
anchor: _anchor
);
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final Offset effectivePaintOffset = _effectivePaintOffset;
super.applyPaintTransform(child, transform..translate(effectivePaintOffset.dx, effectivePaintOffset.dy));
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('paintOffset: $paintOffset');
description.add('mainAxis: $mainAxis');
description.add('anchor: $anchor');
}
}
/// Signature for notifications about [RenderViewport] dimensions changing.
///
/// Used by [RenderViewport.onPaintOffsetUpdateNeeded].
typedef Offset ViewportDimensionsChangeCallback(ViewportDimensions dimensions);
/// A render object that's bigger on the inside.
///
/// The child of a viewport can layout to a larger size along the viewport's
/// [mainAxis] than the viewport itself. If that happens, only a portion of the
/// child will be visible through the viewport. The portion of the child that is
/// visible can be controlled with the [paintOffset].
///
/// See also:
///
/// * [RenderVirtualViewport] (which works with more than one child)
class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<RenderBox> {
/// Creates a render object that's bigger on the inside.
///
/// The [paintOffset] and [mainAxis] arguments must not be null.
RenderViewport({
RenderBox child,
Offset paintOffset: Offset.zero,
Axis mainAxis: Axis.vertical,
ViewportAnchor anchor: ViewportAnchor.start,
this.onPaintOffsetUpdateNeeded
}) : super(paintOffset, mainAxis, anchor) {
this.child = child;
}
/// Called during [layout] to report the dimensions of the viewport
/// and its child.
///
/// The return value of this function is used as the new [paintOffset] and
/// must not be null.
ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
BoxConstraints innerConstraints;
switch (mainAxis) {
case Axis.horizontal:
innerConstraints = constraints.heightConstraints();
break;
case Axis.vertical:
innerConstraints = constraints.widthConstraints();
break;
}
return innerConstraints;
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null)
return child.getMinIntrinsicWidth(height);
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null)
return child.getMaxIntrinsicWidth(height);
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null)
return child.getMinIntrinsicHeight(width);
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null)
return child.getMaxIntrinsicHeight(width);
return 0.0;
}
// We don't override computeDistanceToActualBaseline(), because we
// want the default behavior (returning null). Otherwise, as you
// scroll the RenderViewport, it would shift in its parent if the
// parent was baseline-aligned, which makes no sense.
@override
void performLayout() {
final ViewportDimensions oldDimensions = dimensions;
if (child != null) {
child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
size = constraints.constrain(child.size);
final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset.zero;
dimensions = new ViewportDimensions(containerSize: size, contentSize: child.size);
} else {
performResize();
dimensions = new ViewportDimensions(containerSize: size);
}
if (onPaintOffsetUpdateNeeded != null && dimensions != oldDimensions)
paintOffset = onPaintOffsetUpdateNeeded(dimensions);
assert(paintOffset != null);
}
bool _shouldClipAtPaintOffset(Offset paintOffset) {
assert(child != null);
return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Offset effectivePaintOffset = _effectivePaintOffset;
void paintContents(PaintingContext context, Offset offset) {
context.paintChild(child, offset + effectivePaintOffset);
}
if (_shouldClipAtPaintOffset(effectivePaintOffset)) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
} else {
paintContents(context, offset);
}
}
}
@override
Rect describeApproximatePaintClip(RenderObject child) {
if (child != null && _shouldClipAtPaintOffset(_effectivePaintOffset))
return Point.origin & size;
return null;
}
// Workaround for https://github.com/dart-lang/sdk/issues/25232
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
super.applyPaintTransform(child, transform);
}
@override
bool hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) {
assert(child.parentData is BoxParentData);
Point transformed = position + -_effectivePaintOffset;
return child.hitTest(result, position: transformed);
}
return false;
}
}
......@@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
import 'debug.dart';
import 'framework.dart';
export 'package:flutter/foundation.dart' show TargetPlatform;
export 'package:flutter/animation.dart';
export 'package:flutter/painting.dart';
export 'package:flutter/rendering.dart' show
......@@ -43,10 +44,7 @@ export 'package:flutter/rendering.dart' show
SingleChildLayoutDelegate,
TextOverflow,
ValueChanged,
ValueGetter,
ViewportAnchor,
ViewportDimensions,
ViewportDimensionsChangeCallback;
ValueGetter;
// PAINTING NODES
......@@ -1375,72 +1373,6 @@ class Baseline extends SingleChildRenderObjectWidget {
}
}
/// A widget that's bigger on the inside.
///
/// The child of a viewport can layout to a larger size along the viewport's
/// [mainAxis] than the viewport itself. If that happens, only a portion of the
/// child will be visible through the viewport. The portion of the child that is
/// visible is controlled by the scroll offset.
///
/// Viewport is the core scrolling primitive in the system, but it can be used
/// in other situations.
class Viewport extends SingleChildRenderObjectWidget {
/// Creates a widget that's bigger on the inside.
///
/// The [mainAxis] and [paintOffset] arguments must not be null.
Viewport({
Key key,
this.paintOffset: Offset.zero,
this.mainAxis: Axis.vertical,
this.anchor: ViewportAnchor.start,
this.onPaintOffsetUpdateNeeded,
Widget child
}) : super(key: key, child: child) {
assert(mainAxis != null);
assert(paintOffset != null);
}
/// The offset at which to paint the child.
///
/// The offset can be non-zero only in the [mainAxis].
final Offset paintOffset;
/// The direction in which the child is permitted to be larger than the viewport.
///
/// The child is given layout constraints that are fully unconstrained along
/// the main axis (e.g., the child can be as tall as it wants if the main axis
/// is vertical).
final Axis mainAxis;
/// The end of the viewport from which the paint offset is computed.
///
/// See [ViewportAnchor] for more detail.
final ViewportAnchor anchor;
/// Called when the interior or exterior dimensions of the viewport change.
final ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
@override
RenderViewport createRenderObject(BuildContext context) {
return new RenderViewport(
paintOffset: paintOffset,
mainAxis: mainAxis,
anchor: anchor,
onPaintOffsetUpdateNeeded: onPaintOffsetUpdateNeeded
);
}
@override
void updateRenderObject(BuildContext context, RenderViewport renderObject) {
// Order dependency: RenderViewport validates scrollOffset based on mainAxis.
renderObject
..mainAxis = mainAxis
..anchor = anchor
..paintOffset = paintOffset
..onPaintOffsetUpdateNeeded = onPaintOffsetUpdateNeeded;
}
}
// SLIVERS
......
// Copyright 2016 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.
////////////////////////////////////////////////////////////////////////////////
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
import 'package:flutter/foundation.dart';
import 'framework.dart';
import 'scrollable.dart';
/// A widget that controls whether viewport descendants will overscroll their contents.
/// Overscrolling is clamped at the beginning or end or both according to the
/// [edge] parameter.
///
/// Scroll offset limits are defined by the enclosing Scrollable's [ScrollBehavior].
class ClampOverscrolls extends InheritedWidget {
/// Creates a widget that controls whether viewport descendants will overscroll
/// their contents.
///
/// The [edge] and [child] arguments must not be null.
ClampOverscrolls({
Key key,
this.edge: ScrollableEdge.none,
@required Widget child,
}) : super(key: key, child: child) {
assert(edge != null);
assert(child != null);
}
/// Creates a widget that controls whether viewport descendants will overscroll
/// based on the given [edge] and the inherited ClampOverscrolls widget for
/// the given [context]. For example if edge is ScrollableEdge.leading
/// and a ClampOverscrolls ancestor exists that specified ScrollableEdge.trailing,
/// then this widget would clamp both scrollable edges.
///
/// The [context], [edge] and [child] arguments must not be null.
factory ClampOverscrolls.inherit({
Key key,
@required BuildContext context,
@required ScrollableEdge edge: ScrollableEdge.none,
@required Widget child
}) {
assert(context != null);
assert(edge != null);
assert(child != null);
// The child's clamped edge is the union of the given edge and the
// parent's clamped edge.
ScrollableEdge parentEdge = ClampOverscrolls.of(context)?.edge ?? ScrollableEdge.none;
ScrollableEdge childEdge = edge;
switch (parentEdge) {
case ScrollableEdge.leading:
if (edge == ScrollableEdge.trailing || edge == ScrollableEdge.both)
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.trailing:
if (edge == ScrollableEdge.leading || edge == ScrollableEdge.both)
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.both:
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.none:
break;
}
return new ClampOverscrolls(
key: key,
edge: childEdge,
child: child
);
}
/// Defines when viewport scrollOffsets are clamped in terms of the scrollDirection.
/// If edge is `leading` the viewport's scrollOffset will be clamped at its minimum
/// value (often 0.0). If edge is `trailing` then the scrollOffset will be clamped
/// to its maximum value. If edge is `both` then both the leading and trailing
/// constraints are applied.
final ScrollableEdge edge;
/// Return the [newScrollOffset] clamped according to [edge] and [scrollable]'s
/// scroll behavior. The value of [newScrollOffset] defaults to `scrollable.scrollOffset`.
double clampScrollOffset(ScrollableState scrollable, [double newScrollOffset]) {
final double scrollOffset = newScrollOffset ?? scrollable.scrollOffset;
final double minScrollOffset = scrollable.scrollBehavior.minScrollOffset;
final double maxScrollOffset = scrollable.scrollBehavior.maxScrollOffset;
switch (edge) {
case ScrollableEdge.both:
return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
case ScrollableEdge.leading:
return scrollOffset.clamp(minScrollOffset, double.INFINITY);
case ScrollableEdge.trailing:
return scrollOffset.clamp(double.NEGATIVE_INFINITY, maxScrollOffset);
case ScrollableEdge.none:
return scrollOffset;
}
return scrollOffset;
}
/// The closest instance of this class that encloses the given context.
///
/// Typical usage is as follows:
///
/// ```dart
/// ScrollableEdge edge = ClampOverscrolls.of(context).edge;
/// ```
static ClampOverscrolls of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ClampOverscrolls);
}
@override
bool updateShouldNotify(ClampOverscrolls old) => edge != old.edge;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('edge: $edge');
}
}
......@@ -192,7 +192,6 @@ class Focus extends StatefulWidget {
_FocusScope focusScope = key.currentContext.ancestorWidgetOfExactType(_FocusScope);
if (focusScope != null) {
focusScope.focusState._setFocusedWidget(key);
Scrollable.ensureVisible(focusedContext); // ignore: DEPRECATED_MEMBER_USE
Scrollable2.ensureVisible(focusedContext);
}
}
......@@ -360,7 +359,6 @@ class _FocusState extends State<Focus> {
BuildContext focusedContext = _focusedWidget?.currentContext;
if (focusedContext == null)
return;
Scrollable.ensureVisible(focusedContext); // ignore: DEPRECATED_MEMBER_USE
Scrollable2.ensureVisible(focusedContext);
}
......
// 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.
////////////////////////////////////////////////////////////////////////////////
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';
import 'scroll_simulation.dart';
export 'package:flutter/foundation.dart' show TargetPlatform;
Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
return new FrictionSimulation.through(startOffset, endOffset, startVelocity, endVelocity);
}
// TODO(hansmuller): Simplify these classes. We're no longer using the ScrollBehavior<T, U>
// base class directly. Only LazyBlock uses BoundedBehavior's updateExtents minScrollOffset
// parameter; simpler to move that into ExtentScrollBehavior. All of the classes should
// be called FooScrollBehavior. See https://github.com/flutter/flutter/issues/5281
/// An interface for controlling the behavior of scrollable widgets.
///
/// The type argument T is the type that describes the scroll offset.
/// The type argument U is the type that describes the scroll velocity.
abstract class ScrollBehavior<T, U> {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
///
/// The [platform] must not be null.
const ScrollBehavior({
@required this.platform
});
/// The platform for which physics constants should be approximated.
///
/// This is what makes flings go further on iOS than Android.
///
/// Must not be null.
final TargetPlatform platform;
/// Returns a simulation that propels the scrollOffset.
///
/// This function is called when a drag gesture ends.
///
/// Returns `null` if the behavior is to do nothing.
Simulation createScrollSimulation(T position, U velocity) => null;
/// Returns an animation that ends at the snap offset.
///
/// This function is called when a drag gesture ends and a
/// [SnapOffsetCallback] is specified for the scrollable.
///
/// Returns `null` if the behavior is to do nothing.
Simulation createSnapScrollSimulation(T startOffset, T endOffset, U startVelocity, U endVelocity) => null;
/// Returns the scroll offset to use when the user attempts to scroll
/// from the given offset by the given delta.
T applyCurve(T scrollOffset, T scrollDelta) => scrollOffset;
/// Whether this scroll behavior currently permits scrolling.
bool get isScrollable => true;
@override
String toString() {
List<String> description = <String>[];
debugFillDescription(description);
return '$runtimeType(${description.join("; ")})';
}
/// Accumulates a list of strings describing the current node's fields, one
/// field per string. Subclasses should override this to have their
/// information included in [toString].
@protected
@mustCallSuper
void debugFillDescription(List<String> description) {
description.add(isScrollable ? 'scrollable' : 'not scrollable');
}
}
/// A scroll behavior for a scrollable widget with linear extent (i.e.
/// that only scrolls along one axis).
abstract class ExtentScrollBehavior extends ScrollBehavior<double, double> {
/// Creates a scroll behavior for a scrollable widget with linear extent.
/// We start with an INFINITE contentExtent so that we don't accidentally
/// clamp a scrollOffset until we receive an accurate value in updateExtents.
///
/// The extents and the [platform] must not be null.
ExtentScrollBehavior({
double contentExtent: double.INFINITY,
double containerExtent: 0.0,
@required TargetPlatform platform
}) : _contentExtent = contentExtent,
_containerExtent = containerExtent,
super(platform: platform);
/// The linear extent of the content inside the scrollable widget.
double get contentExtent => _contentExtent;
double _contentExtent;
/// The linear extent of the exterior of the scrollable widget.
double get containerExtent => _containerExtent;
double _containerExtent;
/// Updates either content or container extent (or both)
///
/// Returns the new scroll offset of the widget after the change in extent.
///
/// The [scrollOffset] parameter is the scroll offset of the widget before the
/// change in extent.
double updateExtents({
double contentExtent,
double containerExtent,
double scrollOffset: 0.0
}) {
assert(minScrollOffset <= maxScrollOffset);
if (contentExtent != null)
_contentExtent = contentExtent;
if (containerExtent != null)
_containerExtent = containerExtent;
return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
}
/// The minimum value the scroll offset can obtain.
double get minScrollOffset;
/// The maximum value the scroll offset can obtain.
double get maxScrollOffset;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('content: ${contentExtent.toStringAsFixed(1)}');
description.add('container: ${containerExtent.toStringAsFixed(1)}');
description.add('range: ${minScrollOffset?.toStringAsFixed(1)} .. ${maxScrollOffset?.toStringAsFixed(1)}');
}
}
/// A scroll behavior that prevents the user from exceeding scroll bounds.
class BoundedBehavior extends ExtentScrollBehavior {
/// Creates a scroll behavior that does not overscroll.
BoundedBehavior({
double contentExtent: double.INFINITY,
double containerExtent: 0.0,
double minScrollOffset: 0.0,
@required TargetPlatform platform
}) : _minScrollOffset = minScrollOffset,
super(
contentExtent: contentExtent,
containerExtent: containerExtent,
platform: platform
);
double _minScrollOffset;
@override
double updateExtents({
double contentExtent,
double containerExtent,
double minScrollOffset,
double scrollOffset: 0.0
}) {
if (minScrollOffset != null) {
_minScrollOffset = minScrollOffset;
assert(minScrollOffset <= maxScrollOffset);
}
return super.updateExtents(
contentExtent: contentExtent,
containerExtent: containerExtent,
scrollOffset: scrollOffset
);
}
@override
double get minScrollOffset => _minScrollOffset;
@override
double get maxScrollOffset => math.max(minScrollOffset, minScrollOffset + _contentExtent - _containerExtent);
@override
double applyCurve(double scrollOffset, double scrollDelta) {
return (scrollOffset + scrollDelta).clamp(minScrollOffset, maxScrollOffset);
}
}
/// A scroll behavior that does not prevent the user from exceeding scroll bounds.
class UnboundedBehavior extends ExtentScrollBehavior {
/// Creates a scroll behavior with no scrolling limits.
UnboundedBehavior({
double contentExtent: double.INFINITY,
double containerExtent: 0.0,
@required TargetPlatform platform
}) : super(
contentExtent: contentExtent,
containerExtent: containerExtent,
platform: platform
);
@override
Simulation createScrollSimulation(double position, double velocity) {
return new ScrollSimulation(
position: position,
velocity: velocity,
leadingExtent: double.NEGATIVE_INFINITY,
trailingExtent: double.INFINITY,
platform: platform,
);
}
@override
Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
}
@override
double get minScrollOffset => double.NEGATIVE_INFINITY;
@override
double get maxScrollOffset => double.INFINITY;
@override
double applyCurve(double scrollOffset, double scrollDelta) {
return scrollOffset + scrollDelta;
}
}
/// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance.
class OverscrollBehavior extends BoundedBehavior {
/// Creates a scroll behavior that resists, but does not prevent, scrolling beyond its limits.
OverscrollBehavior({
double contentExtent: double.INFINITY,
double containerExtent: 0.0,
double minScrollOffset: 0.0,
@required TargetPlatform platform
}) : super(
contentExtent: contentExtent,
containerExtent: containerExtent,
minScrollOffset: minScrollOffset,
platform: platform
);
@override
Simulation createScrollSimulation(double position, double velocity) {
return new ScrollSimulation(
position: position,
velocity: velocity,
leadingExtent: minScrollOffset,
trailingExtent: maxScrollOffset,
platform: platform,
);
}
@override
Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
}
@override
double applyCurve(double scrollOffset, double scrollDelta) {
double newScrollOffset = scrollOffset + scrollDelta;
// If we're overscrolling, we want move the scroll offset 2x
// slower than we would otherwise. Therefore, we "rewind" the
// newScrollOffset by half the amount that we moved it above.
// Notice that we clamp the "old" value to 0.0 so that we only
// reduce the portion of scrollDelta that's applied beyond 0.0. We
// do similar things for overscroll in the other direction.
if (newScrollOffset < minScrollOffset) {
newScrollOffset -= (newScrollOffset - math.min(minScrollOffset, scrollOffset)) / 2.0;
} else if (newScrollOffset > maxScrollOffset) {
newScrollOffset -= (newScrollOffset - math.max(maxScrollOffset, scrollOffset)) / 2.0;
}
return newScrollOffset;
}
}
/// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint.
class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
/// Creates a scroll behavior that allows overscrolling only when some amount of scrolling is already possible.
OverscrollWhenScrollableBehavior({
double contentExtent: double.INFINITY,
double containerExtent: 0.0,
double minScrollOffset: 0.0,
@required TargetPlatform platform
}) : super(
contentExtent: contentExtent,
containerExtent: containerExtent,
minScrollOffset: minScrollOffset,
platform: platform
);
@override
bool get isScrollable => contentExtent > containerExtent;
@override
Simulation createScrollSimulation(double position, double velocity) {
if ((isScrollable && velocity.abs() > 0) || position < minScrollOffset || position > maxScrollOffset) {
// If the triggering gesture starts at or beyond the contentExtent's limits
// then the simulation only serves to settle the scrollOffset back to its
// minimum or maximum value.
if (position < minScrollOffset || position > maxScrollOffset)
velocity = 0.0;
return super.createScrollSimulation(position, velocity);
}
return null;
}
@override
double applyCurve(double scrollOffset, double scrollDelta) {
if (isScrollable)
return super.applyCurve(scrollOffset, scrollDelta);
return minScrollOffset;
}
}
......@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'scroll_behavior.dart';
import 'scroll_physics.dart';
import 'overscroll_indicator.dart';
......@@ -78,101 +77,3 @@ class ScrollConfiguration2 extends InheritedWidget {
|| behavior.shouldNotify(old.behavior);
}
}
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
/// Controls how [Scrollable] widgets in a subtree behave.
///
/// Used by [ScrollConfiguration].
abstract class ScrollConfigurationDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ScrollConfigurationDelegate();
/// Returns the platform whose scroll physics should be approximated. See
/// [ScrollBehavior.platform].
TargetPlatform get platform;
/// Returns the ScrollBehavior to be used by generic scrolling containers like
/// [Block].
ExtentScrollBehavior createScrollBehavior();
/// Generic scrolling containers like [Block] will apply this function to the
/// Scrollable they create. It can be used to add widgets that wrap the
/// Scrollable, like scrollbars or overscroll indicators. By default the
/// [scrollWidget] parameter is returned unchanged.
Widget wrapScrollWidget(BuildContext context, Widget scrollWidget) => scrollWidget;
/// Overrides should return true if this ScrollConfigurationDelegate differs
/// from the provided old delegate in a way that requires rebuilding its
/// scrolling container descendants.
bool updateShouldNotify(@checked ScrollConfigurationDelegate old);
}
class _DefaultScrollConfigurationDelegate extends ScrollConfigurationDelegate {
const _DefaultScrollConfigurationDelegate();
@override
TargetPlatform get platform => defaultTargetPlatform;
@override
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: platform);
@override
bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
}
/// A widget that controls descendant [Scrollable] widgets.
///
/// Classes that create Scrollables are not required to depend on this
/// Widget. The following general purpose scrolling widgets do depend
/// on [ScrollConfiguration]: [Block], [LazyBlock], [ScrollableViewport],
/// [ScrollableList], [ScrollableLazyList]. The [Scrollable] base class uses
/// [ScrollConfiguration] to create its [ScrollBehavior].
class ScrollConfiguration extends InheritedWidget {
/// Creates a widget that controls descendant [Scrollable] widgets.
///
/// If the [delegate] argument is null, the scroll configuration for this
/// subtree is controlled by the default implementation of
/// [ScrollConfigurationDelegate].
ScrollConfiguration({
Key key,
this.delegate,
@required Widget child
}) : super(key: key, child: child);
static const ScrollConfigurationDelegate _defaultDelegate = const _DefaultScrollConfigurationDelegate();
/// Defines the ScrollBehavior and scrollable wrapper for descendants.
final ScrollConfigurationDelegate delegate;
/// The delegate property of the closest instance of this class that encloses
/// the given context.
///
/// If no such instance exists, returns a default
/// [ScrollConfigurationDelegate] that approximates the scrolling physics of
/// the current platform (see [defaultTargetPlatform]) using a
/// [OverscrollWhenScrollableBehavior] behavior model.
///
/// Typical usage is as follows:
///
/// ```dart
/// ScrollConfigurationDelegate scrollConfiguration = ScrollConfiguration.of(context);
/// ```
static ScrollConfigurationDelegate of(BuildContext context) {
ScrollConfiguration configuration = context.inheritFromWidgetOfExactType(ScrollConfiguration);
return configuration?.delegate ?? _defaultDelegate;
}
/// A utility function that calls [ScrollConfigurationDelegate.wrapScrollWidget].
static Widget wrap(BuildContext context, Widget scrollWidget) {
return ScrollConfiguration.of(context).wrapScrollWidget(context, scrollWidget);
}
@override
bool updateShouldNotify(ScrollConfiguration old) {
return delegate?.updateShouldNotify(old.delegate) ?? false;
}
}
......@@ -73,7 +73,7 @@ class BouncingScrollPhysics extends ScrollPhysics {
return new BouncingScrollSimulation(
spring: spring,
position: position.pixels,
velocity: velocity,
velocity: velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end.
leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent,
)..tolerance = tolerance;
......
......@@ -81,7 +81,7 @@ class BouncingScrollSimulation extends SimulationGroup {
_currentSimulation = new ScrollSpringSimulation(_spring, position, _leadingExtent, velocity);
return true;
} else if (_currentSimulation == null) {
_currentSimulation = new FrictionSimulation(0.135, position, velocity * 0.91);
_currentSimulation = new FrictionSimulation(0.135, position, velocity);
return true;
}
}
......@@ -196,139 +196,3 @@ class ClampingScrollSimulation extends Simulation {
return time >= _duration;
}
}
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
final SpringDescription _kScrollSpring = new SpringDescription.withDampingRatio(mass: 0.5, springConstant: 100.0, ratio: 1.1);
final double _kDrag = 0.025;
class _CupertinoSimulation extends FrictionSimulation {
static const double drag = 0.135;
_CupertinoSimulation({ double position, double velocity })
: super(drag, position, velocity * 0.91);
}
class _MountainViewSimulation extends ClampingScrollSimulation {
_MountainViewSimulation({
double position,
double velocity,
double friction: 0.015,
}) : super(
position: position,
velocity: velocity,
friction: friction,
);
}
/// Composite simulation for scrollable interfaces.
///
/// Simulates kinetic scrolling behavior between a leading and trailing
/// boundary. Friction is applied within the extents and a spring action is
/// applied at the boundaries. This simulation can only step forward.
class ScrollSimulation extends SimulationGroup {
/// Creates a [ScrollSimulation] with the given parameters.
///
/// The position and velocity arguments must use the same units as will be
/// expected from the [x] and [dx] methods respectively.
///
/// The leading and trailing extents must use the unit of length, the same
/// unit as used for the position argument and as expected from the [x]
/// method.
///
/// The units used with the provided [SpringDescription] must similarly be
/// consistent with the other arguments.
///
/// The final argument is the coefficient of friction, which is unitless.
ScrollSimulation({
@required double position,
@required double velocity,
@required double leadingExtent,
@required double trailingExtent,
SpringDescription spring,
double drag,
TargetPlatform platform,
}) : _leadingExtent = leadingExtent,
_trailingExtent = trailingExtent,
_spring = spring ?? _kScrollSpring,
_drag = drag ?? _kDrag,
_platform = platform {
assert(position != null);
assert(velocity != null);
assert(_leadingExtent != null);
assert(_trailingExtent != null);
assert(_spring != null);
_chooseSimulation(position, velocity, 0.0);
}
final double _leadingExtent;
final double _trailingExtent;
final SpringDescription _spring;
final double _drag;
final TargetPlatform _platform;
bool _isSpringing = false;
Simulation _currentSimulation;
double _offset = 0.0;
@override
bool step(double time) => _chooseSimulation(
_currentSimulation.x(time - _offset),
_currentSimulation.dx(time - _offset), time);
@override
Simulation get currentSimulation => _currentSimulation;
@override
double get currentIntervalOffset => _offset;
bool _chooseSimulation(double position, double velocity, double intervalOffset) {
if (_spring == null && (position > _trailingExtent || position < _leadingExtent))
return false;
// This simulation can only step forward.
if (!_isSpringing) {
if (position > _trailingExtent) {
_isSpringing = true;
_offset = intervalOffset;
_currentSimulation = new ScrollSpringSimulation(_spring, position, _trailingExtent, velocity);
return true;
} else if (position < _leadingExtent) {
_isSpringing = true;
_offset = intervalOffset;
_currentSimulation = new ScrollSpringSimulation(_spring, position, _leadingExtent, velocity);
return true;
}
}
if (_currentSimulation == null) {
switch (_platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
_currentSimulation = new _MountainViewSimulation(
position: position,
velocity: velocity,
);
break;
case TargetPlatform.iOS:
_currentSimulation = new _CupertinoSimulation(
position: position,
velocity: velocity,
);
break;
}
// No platform specified
_currentSimulation ??= new FrictionSimulation(_drag, position, velocity);
return true;
}
return false;
}
@override
String toString() {
return 'ScrollSimulation(leadingExtent: $_leadingExtent, trailingExtent: $_trailingExtent)';
}
}
......@@ -3,24 +3,16 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui show window;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'banner.dart';
import 'basic.dart';
import 'clamp_overscrolls.dart';
import 'debug.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'notification_listener.dart';
import 'page_storage.dart';
import 'scroll_behavior.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart';
import 'scroll_notification.dart';
......@@ -325,1131 +317,3 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
description.add('position: $position');
}
}
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
/// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection.
enum ScrollableEdge {
/// The top and bottom of the scrollable if its scrollDirection is vertical
/// or the left and right if its scrollDirection is horizontal.
both,
/// Only the top of the scrollable if its scrollDirection is vertical,
/// or only the left if its scrollDirection is horizontal.
leading,
/// Only the bottom of the scrollable if its scroll-direction is vertical,
/// or only the right if its scrollDirection is horizontal.
trailing,
/// The overscroll indicator should not appear at all.
none,
}
/// The accuracy to which scrolling is computed.
final Tolerance kPixelScrollTolerance = new Tolerance(
// TODO(ianh): Handle the case of the device pixel ratio changing.
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / ui.window.devicePixelRatio // logical pixels
);
/// Signature for building a widget based on [ScrollableState].
///
/// Used by [Scrollable.builder].
typedef Widget ScrollBuilder(BuildContext context, ScrollableState state);
/// Signature for callbacks that receive a scroll offset.
///
/// Used by [Scrollable.onScrollStart], [Scrollable.onScroll], and [Scrollable.onScrollEnd].
typedef void ScrollListener(double scrollOffset);
/// Signature for determining the offset at which scrolling should snap.
///
/// Used by [Scrollable.snapOffsetCallback].
typedef double SnapOffsetCallback(double scrollOffset, Size containerSize);
/// Will be removed soon.
class Scrollable extends StatefulWidget {
/// Initializes fields for subclasses.
///
/// The [scrollDirection] and [scrollAnchor] arguments must not be null.
Scrollable({
Key key,
this.initialScrollOffset,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback,
this.builder
}) : super(key: key) {
assert(scrollDirection == Axis.vertical || scrollDirection == Axis.horizontal);
assert(scrollAnchor == ViewportAnchor.start || scrollAnchor == ViewportAnchor.end);
}
// Warning: keep the dartdoc comments that follow in sync with the copies in
// ScrollableViewport and LazyBlock.
// And see: https://github.com/dart-lang/dartdoc/issues/1161.
/// The scroll offset this widget should use when first created.
final double initialScrollOffset;
/// The axis along which this widget should scroll.
final Axis scrollDirection;
/// Whether to place first child at the start of the container or
/// the last child at the end of the container, when the scrollable
/// has not been scrolled and has no initial scroll offset.
///
/// For example, if the [scrollDirection] is [Axis.vertical] and
/// there are enough items to overflow the container, then
/// [ViewportAnchor.start] means that the top of the first item
/// should be aligned with the top of the scrollable with the last
/// item below the bottom, and [ViewportAnchor.end] means the bottom
/// of the last item should be aligned with the bottom of the
/// scrollable, with the first item above the top.
///
/// This also affects whether, when an item is added or removed, the
/// displacement will be towards the first item or the last item.
/// Continuing the earlier example, if a new item is inserted in the
/// middle of the list, in the [ViewportAnchor.start] case the items
/// after it (with greater indices, down to the item with the
/// highest index) will be pushed down, while in the
/// [ViewportAnchor.end] case the items before it (with lower
/// indices, up to the item with the index 0) will be pushed up.
///
/// Subclasses may ignore this value if, for instance, they do not
/// have a concept of an anchor, or have more complicated behavior
/// (e.g. they would by default put the middle item in the middle of
/// the container).
final ViewportAnchor scrollAnchor;
/// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart;
/// Called whenever this widget's scroll offset changes.
final ScrollListener onScroll;
/// Called whenever this widget stops scrolling.
final ScrollListener onScrollEnd;
/// Called to determine the offset to which scrolling should snap,
/// when handling a fling.
///
/// This callback, if set, will be called with the offset that the
/// Scrollable would have scrolled to in the absence of this
/// callback, and a Size describing the size of the Scrollable
/// itself.
///
/// The callback's return value is used as the new scroll offset to
/// aim for.
///
/// If the callback simply returns its first argument (the offset),
/// then it is as if the callback was null.
final SnapOffsetCallback snapOffsetCallback;
/// Using to build the content of this widget.
///
/// See [buildContent] for details.
final ScrollBuilder builder;
/// 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) {
return context.ancestorStateOfType(const TypeMatcher<ScrollableState>());
}
/// Scrolls the closest enclosing scrollable to make the given context visible.
static Future<Null> ensureVisible(BuildContext context, { Duration duration, Curve curve: Curves.ease }) {
assert(context.findRenderObject() is RenderBox);
// TODO(abarth): This function doesn't handle nested scrollable widgets.
ScrollableState scrollable = Scrollable.of(context);
if (scrollable == null)
return new Future<Null>.value();
RenderBox targetBox = context.findRenderObject();
assert(targetBox.attached);
Size targetSize = targetBox.size;
RenderBox scrollableBox = scrollable.context.findRenderObject();
assert(scrollableBox.attached);
Size scrollableSize = scrollableBox.size;
double targetMin;
double targetMax;
double scrollableMin;
double scrollableMax;
switch (scrollable.config.scrollDirection) {
case Axis.vertical:
targetMin = targetBox.localToGlobal(Point.origin).y;
targetMax = targetBox.localToGlobal(new Point(0.0, targetSize.height)).y;
scrollableMin = scrollableBox.localToGlobal(Point.origin).y;
scrollableMax = scrollableBox.localToGlobal(new Point(0.0, scrollableSize.height)).y;
break;
case Axis.horizontal:
targetMin = targetBox.localToGlobal(Point.origin).x;
targetMax = targetBox.localToGlobal(new Point(targetSize.width, 0.0)).x;
scrollableMin = scrollableBox.localToGlobal(Point.origin).x;
scrollableMax = scrollableBox.localToGlobal(new Point(scrollableSize.width, 0.0)).x;
break;
}
double scrollOffsetDelta;
if (targetMin < scrollableMin) {
if (targetMax > scrollableMax) {
// The target is too big to fit inside the scrollable. The best we can do
// is to center the target.
double targetCenter = (targetMin + targetMax) / 2.0;
double scrollableCenter = (scrollableMin + scrollableMax) / 2.0;
scrollOffsetDelta = targetCenter - scrollableCenter;
} else {
scrollOffsetDelta = targetMin - scrollableMin;
}
} else if (targetMax > scrollableMax) {
scrollOffsetDelta = targetMax - scrollableMax;
} else {
return new Future<Null>.value();
}
ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
if (scrollOffset != scrollable.scrollOffset)
return scrollable.scrollTo(scrollOffset, duration: duration, curve: curve);
return new Future<Null>.value();
}
@override
ScrollableState createState() => new ScrollableState<Scrollable>();
}
/// Contains the state for common scrolling widgets that scroll only
/// along one axis.
///
/// Widgets that subclass [Scrollable] typically use state objects
/// that subclass [ScrollableState].
///
/// The main state of a ScrollableState is the "scroll offset", which
/// is the the logical description of the current scroll position and
/// is stored in [scrollOffset] as a double. The units of the scroll
/// offset are defined by the specific subclass. By default, the units
/// are logical pixels.
///
/// A "pixel offset" is a distance in logical pixels (or a velocity in
/// logical pixels per second). The pixel offset corresponding to the
/// current scroll position is typically used as the paint offset
/// argument to the underlying [Viewport] class (or equivalent); see
/// the [buildContent] method.
///
/// A "pixel delta" is an [Offset] that describes a two-dimensional
/// distance as reported by input events. If the scrolling convention
/// is axis-aligned (as in a vertical scrolling list or a horizontal
/// scrolling list), then the pixel delta will consist of a pixel
/// offset in the scroll axis, and a value in the other axis that is
/// either ignored (when converting to a scroll offset) or set to zero
/// (when converting a scroll offset to a pixel delta).
///
/// If the units of the scroll offset are not logical pixels, then a
/// mapping must be made from logical pixels (as used by incoming
/// input events) and the scroll offset (as stored internally). To
/// provide this mapping, override the [pixelOffsetToScrollOffset] and
/// [scrollOffsetToPixelOffset] methods.
///
/// If the scrollable is not providing axis-aligned scrolling, then,
/// to convert pixel deltas to scroll offsets and vice versa, override
/// the [pixelDeltaToScrollOffset] and [scrollOffsetToPixelOffset]
/// methods. By default, these assume an axis-aligned scroll behavior
/// along the [config.scrollDirection] axis and are implemented in
/// terms of the [pixelOffsetToScrollOffset] and
/// [scrollOffsetToPixelOffset] methods.
@optionalTypeArgs
class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
_controller = new AnimationController.unbounded(vsync: this)
..addListener(_handleAnimationChanged)
..addStatusListener(_handleAnimationStatusChanged);
_scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0;
_virtualScrollOffset = _scrollOffset;
}
Simulation _simulation; // if we're flinging, then this is the animation with which we're doing it
AnimationController _controller;
double _contentExtent;
double _containerExtent;
bool _scrollUnderway = false;
@override
void dispose() {
_controller.dispose();
_simulation = null;
super.dispose();
}
@override
void dependenciesChanged() {
_scrollBehavior = createScrollBehavior();
didUpdateScrollBehavior(_scrollBehavior.updateExtents(
contentExtent: _contentExtent,
containerExtent: _containerExtent,
scrollOffset: scrollOffset
));
super.dependenciesChanged();
}
/// The current scroll offset.
///
/// The scroll offset is applied to the child widget along the scroll
/// direction before painting. A positive scroll offset indicates that
/// more content in the preferred reading direction is visible.
///
/// The scroll offset's value may be above or below the limits defined
/// by the [scrollBehavior]. This is called "overscrolling" and it can be
/// prevented with the [ClampOverscrolls] widget.
///
/// See also:
///
/// * [virtualScrollOffset]
/// * [initialScrollOffset]
/// * [onScrollStart]
/// * [onScroll]
/// * [onScrollEnd]
/// * [ScrollNotification]
double get scrollOffset => _scrollOffset;
double _scrollOffset;
/// The current scroll offset, irrespective of the constraints defined
/// by any [ClampOverscrolls] widget ancestors.
///
/// See also:
///
/// * [scrollOffset]
double get virtualScrollOffset => _virtualScrollOffset;
double _virtualScrollOffset;
/// Convert a position or velocity measured in terms of pixels to a scrollOffset.
/// Scrollable gesture handlers convert their incoming values with this method.
/// Subclasses that define scrollOffset in units other than pixels must
/// override this method.
///
/// This function should be the inverse of [scrollOffsetToPixelOffset].
double pixelOffsetToScrollOffset(double pixelOffset) {
switch (config.scrollAnchor) {
case ViewportAnchor.start:
// We negate the delta here because a positive scroll offset moves the
// the content up (or to the left) rather than down (or the right).
return -pixelOffset;
case ViewportAnchor.end:
return pixelOffset;
}
assert(config.scrollAnchor != null);
return null;
}
/// Convert a scrollOffset value to the number of pixels to which it corresponds.
///
/// This function should be the inverse of [pixelOffsetToScrollOffset].
double scrollOffsetToPixelOffset(double scrollOffset) {
switch (config.scrollAnchor) {
case ViewportAnchor.start:
return -scrollOffset;
case ViewportAnchor.end:
return scrollOffset;
}
assert(config.scrollAnchor != null);
return null;
}
/// Returns the scroll offset component of the given pixel delta, accounting
/// for the scroll direction and scroll anchor.
///
/// A pixel delta is an [Offset] in pixels. Typically this function
/// is implemented in terms of [pixelOffsetToScrollOffset].
double pixelDeltaToScrollOffset(Offset pixelDelta) {
switch (config.scrollDirection) {
case Axis.horizontal:
return pixelOffsetToScrollOffset(pixelDelta.dx);
case Axis.vertical:
return pixelOffsetToScrollOffset(pixelDelta.dy);
}
assert(config.scrollDirection != null);
return null;
}
/// Returns a two-dimensional representation of the scroll offset, accounting
/// for the scroll direction and scroll anchor.
///
/// See the definition of [ScrollableState] for more details.
Offset scrollOffsetToPixelDelta(double scrollOffset) {
switch (config.scrollDirection) {
case Axis.horizontal:
return new Offset(scrollOffsetToPixelOffset(scrollOffset), 0.0);
case Axis.vertical:
return new Offset(0.0, scrollOffsetToPixelOffset(scrollOffset));
}
assert(config.scrollDirection != null);
return null;
}
/// The current scroll behavior of this widget.
///
/// Scroll behaviors control where the boundaries of the scrollable are placed
/// and how the scrolling physics should behave near those boundaries and
/// after the user stops directly manipulating the scrollable.
ExtentScrollBehavior get scrollBehavior => _scrollBehavior;
ExtentScrollBehavior _scrollBehavior;
/// Use the value returned by [ScrollConfiguration.createScrollBehavior].
/// If this widget doesn't have a ScrollConfiguration ancestor,
/// or its createScrollBehavior callback is null, then return a new instance
/// of [OverscrollWhenScrollableBehavior].
@protected
ExtentScrollBehavior createScrollBehavior() {
return ScrollConfiguration.of(context)?.createScrollBehavior();
}
bool _scrollOffsetIsInBounds(double scrollOffset) {
if (scrollBehavior is! ExtentScrollBehavior)
return false;
final ExtentScrollBehavior behavior = scrollBehavior;
return scrollOffset >= behavior.minScrollOffset && scrollOffset < behavior.maxScrollOffset;
}
void _handleAnimationChanged() {
_setScrollOffset(_controller.value);
}
void _handleAnimationStatusChanged(AnimationStatus status) {
// this is not called when stop() is called on the controller
setState(() {
if (!_controller.isAnimating) {
_simulation = null;
_scrollUnderway = false;
}
});
}
void _setScrollOffset(double newScrollOffset, { DragUpdateDetails details }) {
if (_scrollOffset == newScrollOffset)
return;
final ClampOverscrolls clampOverscrolls = ClampOverscrolls.of(context);
final double clampedScrollOffset = clampOverscrolls?.clampScrollOffset(this, newScrollOffset) ?? newScrollOffset;
_setStateMaybeDuringBuild(() {
_virtualScrollOffset = newScrollOffset;
_scrollUnderway = _scrollOffset != clampedScrollOffset;
_scrollOffset = clampedScrollOffset;
});
PageStorage.of(context)?.writeState(context, _scrollOffset);
_startScroll();
dispatchOnScroll();
new ScrollNotification(
scrollable: this,
kind: ScrollNotificationKind.updated,
details: details
).dispatch(context);
_endScroll();
}
/// Scroll this widget by the given scroll delta.
///
/// If a non-null [duration] is provided, the widget will animate to the new
/// scroll offset over the given duration with the given curve.
Future<Null> scrollBy(double scrollDelta, {
Duration duration,
Curve curve: Curves.ease,
DragUpdateDetails details
}) {
double newScrollOffset = scrollBehavior.applyCurve(virtualScrollOffset, scrollDelta);
return scrollTo(newScrollOffset, duration: duration, curve: curve, details: details);
}
/// Scroll this widget to the given scroll offset.
///
/// If a non-null [duration] is provided, the widget will animate to the new
/// scroll offset over the given duration with the given curve.
///
/// This function does not accept a zero duration. To jump-scroll to
/// the new offset, do not provide a duration, rather than providing
/// a zero duration.
///
/// The returned [Future] completes when the scrolling animation is complete.
Future<Null> scrollTo(double newScrollOffset, {
Duration duration,
Curve curve: Curves.ease,
DragUpdateDetails details
}) {
if (newScrollOffset == _scrollOffset)
return new Future<Null>.value();
if (duration == null) {
_stop();
_setScrollOffset(newScrollOffset, details: details);
return new Future<Null>.value();
}
assert(duration > Duration.ZERO);
return _animateTo(newScrollOffset, duration, curve);
}
Future<Null> _animateTo(double newScrollOffset, Duration duration, Curve curve) {
_stop();
_controller.value = virtualScrollOffset;
_startScroll();
return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then((Null _) {
_endScroll();
});
}
/// Update any in-progress scrolling physics to account for new scroll behavior.
///
/// The scrolling physics depends on the scroll behavior. When changing the
/// scrolling behavior, call this function to update any in-progress scrolling
/// physics to account for the new scroll behavior. This function preserves
/// the current velocity when updating the physics.
///
/// If there are no in-progress scrolling physics, this function scrolls to
/// the given offset instead.
void didUpdateScrollBehavior(double newScrollOffset) {
_setStateMaybeDuringBuild(() {
_contentExtent = scrollBehavior.contentExtent;
_containerExtent = scrollBehavior.containerExtent;
});
// This does not call setState, because if anything below actually
// changes our build, it will itself independently trigger a frame.
assert(_controller.isAnimating || _simulation == null);
if (_numberOfInProgressScrolls > 0) {
if (_simulation != null) {
double dx = _simulation.dx(_controller.lastElapsedDuration.inMicroseconds / Duration.MICROSECONDS_PER_SECOND);
_startToEndAnimation(dx); // dx - logical pixels / second
}
return;
}
scrollTo(newScrollOffset);
}
/// Updates the scroll behavior for the new content and container extent.
///
/// For convenience, this function combines three common operations:
///
/// 1. Updating the scroll behavior extents with
/// [ExtentScrollBehavior.updateExtents].
/// 2. Notifying this object that the scroll behavior was updated with
/// [didUpdateScrollBehavior].
/// 3. Updating this object's gesture detector with [updateGestureDetector].
void handleExtentsChanged(double contentExtent, double containerExtent) {
didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: contentExtent,
containerExtent: containerExtent,
scrollOffset: scrollOffset
));
updateGestureDetector();
}
/// If [scrollVelocity] is greater than [PixelScrollTolerance.velocity] then
/// fling the scroll offset with the given velocity in logical pixels/second.
/// Otherwise, if this scrollable is overscrolled or a [snapOffsetCallback]
/// was given, animate the scroll offset to its final value with [settleScrollOffset].
///
/// Calling this function starts a physics-based animation of the scroll
/// offset with the given value as the initial velocity. The physics
/// simulation is determined by the scroll behavior.
///
/// The returned [Future] completes when the scrolling animation is complete.
Future<Null> fling(double scrollVelocity) {
if (scrollVelocity.abs() > kPixelScrollTolerance.velocity)
return _startToEndAnimation(scrollVelocity);
// If a scroll animation isn't underway already and we're overscrolled or we're
// going to have to snap the scroll offset, then animate the scroll offset to its
// final value.
if (!_controller.isAnimating &&
(shouldSnapScrollOffset || !_scrollOffsetIsInBounds(scrollOffset)))
return settleScrollOffset();
return new Future<Null>.value();
}
/// Animate the scroll offset to a value with a local minima of energy.
///
/// Calling this function starts a physics-based animation of the scroll
/// offset either to a snap point or to within the scrolling bounds. The
/// physics simulation used is determined by the scroll behavior.
Future<Null> settleScrollOffset() {
return _startToEndAnimation(0.0);
}
Future<Null> _startToEndAnimation(double scrollVelocity) {
_stop();
_simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
if (_simulation == null)
return new Future<Null>.value();
_startScroll();
return _controller.animateWith(_simulation).then((Null _) {
_endScroll();
});
}
/// Whether this scrollable should attempt to snap scroll offsets.
bool get shouldSnapScrollOffset => config.snapOffsetCallback != null;
/// Returns the snapped offset closest to the given scroll offset.
double snapScrollOffset(double scrollOffset) {
return config.snapOffsetCallback == null ? scrollOffset : config.snapOffsetCallback(scrollOffset, context.size);
}
Simulation _createSnapSimulation(double scrollVelocity) {
if (!shouldSnapScrollOffset || scrollVelocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
return null;
Simulation simulation = _createFlingSimulation(scrollVelocity);
if (simulation == null)
return null;
final double endScrollOffset = simulation.x(double.INFINITY);
if (endScrollOffset.isNaN)
return null;
final double snappedScrollOffset = snapScrollOffset(endScrollOffset);
if (!_scrollOffsetIsInBounds(snappedScrollOffset))
return null;
final double snapVelocity = scrollVelocity.abs() * (snappedScrollOffset - scrollOffset).sign;
final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * (scrollVelocity < 0.0 ? -1.0 : 1.0);
Simulation toSnapSimulation = scrollBehavior.createSnapScrollSimulation(
virtualScrollOffset, snappedScrollOffset, snapVelocity, endVelocity
);
if (toSnapSimulation == null)
return null;
final double scrollOffsetMin = math.min(scrollOffset, snappedScrollOffset);
final double scrollOffsetMax = math.max(scrollOffset, snappedScrollOffset);
return new ClampedSimulation(toSnapSimulation, xMin: scrollOffsetMin, xMax: scrollOffsetMax);
}
Simulation _createFlingSimulation(double scrollVelocity) {
final Simulation simulation = scrollBehavior.createScrollSimulation(virtualScrollOffset, scrollVelocity);
if (simulation != null) {
final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs();
final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs();
simulation.tolerance = new Tolerance(velocity: endVelocity, distance: endDistance);
}
return simulation;
}
// When we start an scroll animation, we stop any previous scroll animation.
// However, the code that would deliver the onScrollEnd callback is watching
// for animations to end using a Future that resolves at the end of the
// microtask. That causes animations to "overlap" between the time we start a
// new animation and the end of the microtask. By the time the microtask is
// over and we check whether to deliver an onScrollEnd callback, we will have
// started the new animation (having skipped the onScrollStart) and therefore
// we won't deliver the onScrollEnd until the second animation is finished.
int _numberOfInProgressScrolls = 0;
/// Calls the onScroll callback.
///
/// Subclasses can override this method to hook the scroll callback.
void dispatchOnScroll() {
assert(_numberOfInProgressScrolls > 0);
if (config.onScroll != null)
config.onScroll(_scrollOffset);
}
void _handleDragDown(DragDownDetails details) {
setState(() {
_stop();
});
}
void _stop() {
assert(mounted);
assert(_controller.isAnimating || _simulation == null);
_controller.stop(); // this does not trigger a status notification
_simulation = null;
}
void _handleDragStart(DragStartDetails details) {
_startScroll(details: details);
}
void _startScroll({ DragStartDetails details }) {
_numberOfInProgressScrolls += 1;
if (_numberOfInProgressScrolls == 1) {
dispatchOnScrollStart();
new ScrollNotification(
scrollable: this,
kind: ScrollNotificationKind.started,
details: details
).dispatch(context);
}
}
/// Calls the onScrollStart callback.
///
/// Subclasses can override this method to hook the scroll start callback.
void dispatchOnScrollStart() {
assert(_numberOfInProgressScrolls == 1);
if (config.onScrollStart != null)
config.onScrollStart(_scrollOffset);
}
void _handleDragUpdate(DragUpdateDetails details) {
scrollBy(pixelOffsetToScrollOffset(details.primaryDelta), details: details);
}
void _handleDragEnd(DragEndDetails details) {
final double scrollVelocity = pixelDeltaToScrollOffset(details.velocity.pixelsPerSecond);
fling(scrollVelocity).then<Null>((Null value) {
_endScroll(details: details);
});
}
// Used for state changes that sometimes occur during a build phase. If so,
// we skip calling setState, as the changes will apply to the next build.
// TODO(ianh): This is ugly and hopefully temporary. Ideally this won't be
// needed after Scrollable is rewritten.
void _setStateMaybeDuringBuild(VoidCallback fn) {
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
fn();
} else {
setState(fn);
}
}
void _endScroll({ DragEndDetails details }) {
_numberOfInProgressScrolls -= 1;
if (_numberOfInProgressScrolls == 0) {
_simulation = null;
if (_scrollUnderway && mounted) {
// If the scroll hasn't already stopped because we've hit a clamped
// edge or the controller stopped animating, then rebuild the Scrollable
// with the IgnorePointer widget turned off.
_setStateMaybeDuringBuild(() {
_scrollUnderway = false;
});
}
dispatchOnScrollEnd();
if (mounted) {
new ScrollNotification(
scrollable: this,
kind: ScrollNotificationKind.ended,
details: details
).dispatch(context);
}
}
}
/// Calls the dispatchOnScrollEnd callback.
///
/// Subclasses can override this method to hook the scroll end callback.
void dispatchOnScrollEnd() {
assert(_numberOfInProgressScrolls == 0);
if (config.onScrollEnd != null)
config.onScrollEnd(_scrollOffset);
}
final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = new GlobalKey<RawGestureDetectorState>();
@override
Widget build(BuildContext context) {
Widget result = new RawGestureDetector(
key: _gestureDetectorKey,
gestures: buildGestureDetectors(),
behavior: HitTestBehavior.opaque,
child: new IgnorePointer(
ignoring: _scrollUnderway,
child: buildContent(context),
),
);
if (debugHighlightDeprecatedWidgets) {
result = new Banner(
message: 'OLD',
color: const Color(0xFF009000),
location: BannerLocation.bottomRight,
child: result,
);
}
return result;
}
/// Fixes up the gesture detector to listen to the appropriate
/// gestures based on the current information about the layout.
///
/// This method should be called from the
/// [onPaintOffsetUpdateNeeded] or [onExtentsChanged] handler given
/// to the [Viewport] or equivalent used by the subclass's
/// [buildContent] method. See the [buildContent] method's
/// description for details.
void updateGestureDetector() {
_gestureDetectorKey.currentState.replaceGestureRecognizers(buildGestureDetectors());
}
/// Return the gesture detectors, in the form expected by
/// [RawGestureDetector.gestures] and
/// [RawGestureDetectorState.replaceGestureRecognizers], that are
/// applicable to this [Scrollable] in its current state.
///
/// This is called by [build] and [updateGestureDetector].
Map<Type, GestureRecognizerFactory> buildGestureDetectors() {
if (scrollBehavior.isScrollable) {
switch (config.scrollDirection) {
case Axis.vertical:
return <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: (VerticalDragGestureRecognizer recognizer) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/5771
return (recognizer ??= new VerticalDragGestureRecognizer())
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
}
};
case Axis.horizontal:
return <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer: (HorizontalDragGestureRecognizer recognizer) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/5771
return (recognizer ??= new HorizontalDragGestureRecognizer())
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
}
};
}
}
return const <Type, GestureRecognizerFactory>{};
}
/// Calls the widget's [builder] by default.
///
/// Subclasses can override this method to build the interior of their
/// scrollable widget. Scrollable wraps the returned widget in a
/// [GestureDetector] to observe the user's interaction with this widget and
/// to adjust the scroll offset accordingly.
///
/// The widgets used by this method should be widgets that provide a
/// layout-time callback that reports the sizes that are relevant to
/// the scroll offset (typically the size of the scrollable
/// container and the scrolled contents). [Viewport] provides an
/// [onPaintOffsetUpdateNeeded] callback for this purpose; [GridViewport],
/// [ListViewport], [LazyListViewport], and [LazyBlockViewport] provide an
/// [onExtentsChanged] callback for this purpose.
///
/// This callback should be used to update the scroll behavior, if
/// necessary, and then to call [updateGestureDetector] to update
/// the gesture detectors accordingly.
Widget buildContent(BuildContext context) {
assert(config.builder != null);
return config.builder(context, this);
}
}
/// Indicates if a [ScrollNotification] indicates the start, end or the
/// middle of a scroll.
enum ScrollNotificationKind {
/// The [ScrollNotification] indicates that the scrollOffset has been changed
/// and no existing scroll is underway.
started,
/// The [ScrollNotification] indicates that the scrollOffset has been changed.
updated,
/// The [ScrollNotification] indicates that the scrollOffset has stopped changing.
/// This may be because the fling animation that follows a drag gesture has
/// completed or simply because the scrollOffset was reset.
ended
}
/// Indicates that a scrollable descendant is scrolling.
///
/// See also:
///
/// * [NotificationListener].
class ScrollNotification extends Notification {
/// Creates a notification about scrolling.
ScrollNotification({ this.scrollable, this.kind, dynamic details }) : _details = details {
assert(scrollable != null);
assert(kind != null);
assert(details == null
|| (kind == ScrollNotificationKind.started && details is DragStartDetails)
|| (kind == ScrollNotificationKind.updated && details is DragUpdateDetails)
|| (kind == ScrollNotificationKind.ended && details is DragEndDetails));
}
/// Indicates if we're at the start, middle, or end of a scroll.
final ScrollNotificationKind kind;
/// The scrollable that scrolled.
final ScrollableState scrollable;
/// The details from the underlying [DragGestureRecognizer] gesture, if the
/// notification ultimately came from a [DragGestureRecognizer.onStart]
/// handler; otherwise null.
DragStartDetails get dragStartDetails => kind == ScrollNotificationKind.started ? _details : null;
/// The details from the underlying [DragGestureRecognizer] gesture, if the
/// notification ultimately came from a [DragGestureRecognizer.onUpdate]
/// handler; otherwise null.
DragUpdateDetails get dragUpdateDetails => kind == ScrollNotificationKind.updated ? _details : null;
/// The details from the underlying [DragGestureRecognizer] gesture, if the
/// notification ultimately came from a [DragGestureRecognizer.onEnd]
/// handler; otherwise null.
DragEndDetails get dragEndDetails => kind == ScrollNotificationKind.ended ? _details : null;
final dynamic _details;
/// The number of scrollable widgets that have already received this
/// notification. Typically listeners only respond to notifications
/// with depth = 0.
int get depth => _depth;
int _depth = 0;
@override
bool visitAncestor(Element element) {
if (element is StatefulElement && element.state is ScrollableState)
_depth += 1;
return super.visitAncestor(element);
}
}
/// A simple scrolling widget that has a single child.
///
/// Use this widget if you are not worried about offscreen widgets consuming
/// resources.
///
/// See also:
///
/// * [Block], if your single child is a [Column].
/// * [ScrollableList], if you have many identically-sized children.
/// * [GridView], if your children are in a grid pattern.
/// * [LazyBlock], if you have many children of varying sizes.
@Deprecated('use SingleChildScrollView')
class ScrollableViewport extends StatelessWidget {
/// Creates a simple scrolling widget that has a single child.
///
/// The [scrollDirection] and [scrollAnchor] arguments must not be null.
ScrollableViewport({
Key key,
this.initialScrollOffset,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback,
this.scrollableKey,
this.child
}) : super(key: key) {
assert(scrollDirection != null);
assert(scrollAnchor != null);
}
// Warning: keep the dartdoc comments that follow in sync with the copies in
// Scrollable, LazyBlock, ScrollableLazyList, and ScrollableList.
// And see: https://github.com/dart-lang/dartdoc/issues/1161.
/// The scroll offset this widget should use when first created.
final double initialScrollOffset;
/// The axis along which this widget should scroll.
final Axis scrollDirection;
/// Whether to place first child at the start of the container or
/// the last child at the end of the container, when the scrollable
/// has not been scrolled and has no initial scroll offset.
///
/// For example, if the [scrollDirection] is [Axis.vertical] and
/// there are enough items to overflow the container, then
/// [ViewportAnchor.start] means that the top of the first item
/// should be aligned with the top of the scrollable with the last
/// item below the bottom, and [ViewportAnchor.end] means the bottom
/// of the last item should be aligned with the bottom of the
/// scrollable, with the first item above the top.
///
/// This also affects whether, when an item is added or removed, the
/// displacement will be towards the first item or the last item.
/// Continuing the earlier example, if a new item is inserted in the
/// middle of the list, in the [ViewportAnchor.start] case the items
/// after it (with greater indices, down to the item with the
/// highest index) will be pushed down, while in the
/// [ViewportAnchor.end] case the items before it (with lower
/// indices, up to the item with the index 0) will be pushed up.
final ViewportAnchor scrollAnchor;
/// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart;
/// Called whenever this widget's scroll offset changes.
final ScrollListener onScroll;
/// Called whenever this widget stops scrolling.
final ScrollListener onScrollEnd;
/// Called to determine the offset to which scrolling should snap,
/// when handling a fling.
///
/// This callback, if set, will be called with the offset that the
/// Scrollable would have scrolled to in the absence of this
/// callback, and a Size describing the size of the Scrollable
/// itself.
///
/// The callback's return value is used as the new scroll offset to
/// aim for.
///
/// If the callback simply returns its first argument (the offset),
/// then it is as if the callback was null.
final SnapOffsetCallback snapOffsetCallback;
/// The key for the Scrollable created by this widget.
final Key scrollableKey;
/// The widget that will be scrolled. It will become the child of a Scrollable.
final Widget child;
Widget _buildViewport(BuildContext context, ScrollableState state) {
return new Viewport(
paintOffset: state.scrollOffsetToPixelDelta(state.scrollOffset),
mainAxis: scrollDirection,
anchor: scrollAnchor,
onPaintOffsetUpdateNeeded: (ViewportDimensions dimensions) {
final double contentExtent = scrollDirection == Axis.vertical ? dimensions.contentSize.height : dimensions.contentSize.width;
final double containerExtent = scrollDirection == Axis.vertical ? dimensions.containerSize.height : dimensions.containerSize.width;
state.handleExtentsChanged(contentExtent, containerExtent);
return state.scrollOffsetToPixelDelta(state.scrollOffset);
},
child: child
);
}
@override
Widget build(BuildContext context) {
final Widget result = new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback,
builder: _buildViewport
);
return ScrollConfiguration.wrap(context, result);
}
}
/// A scrolling list of variably-sized children.
///
/// Useful when you have a small, fixed number of children that you wish to
/// arrange in a block layout and that might exceed the height of its container
/// (and therefore need to scroll).
///
/// If you have a large number of children, or if you always expect this to need
/// to scroll, consider using [LazyBlock] (if the children have variable height)
/// or [ScrollableList] (if the children all have the same fixed height), as
/// they avoid doing work for children that are not visible.
///
/// This widget is implemented using [ScrollableViewport] and [BlockBody]. If
/// you have a single child, consider using [ScrollableViewport] directly.
///
/// See also:
///
/// * [LazyBlock], if you have many children with varying heights.
/// * [ScrollableList], if all your children are the same height.
/// * [ScrollableViewport], if you only have one child.
@Deprecated('use ListView instead')
class Block extends StatelessWidget {
/// Creates a scrollable array of children.
Block({
Key key,
this.children: const <Widget>[],
this.padding,
this.initialScrollOffset,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.scrollableKey
}) : super(key: key) {
assert(children != null);
assert(!children.any((Widget child) => child == null));
}
/// The children, all of which are materialized.
final List<Widget> children;
/// The amount of space by which to inset the children inside the viewport.
final EdgeInsets padding;
/// The scroll offset this widget should use when first created.
final double initialScrollOffset;
/// The axis along which this widget should scroll.
final Axis scrollDirection;
/// Whether to place first child at the start of the container or
/// the last child at the end of the container, when the scrollable
/// has not been scrolled and has no initial scroll offset.
///
/// For example, if the [scrollDirection] is [Axis.vertical] and
/// there are enough items to overflow the container, then
/// [ViewportAnchor.start] means that the top of the first item
/// should be aligned with the top of the scrollable with the last
/// item below the bottom, and [ViewportAnchor.end] means the bottom
/// of the last item should be aligned with the bottom of the
/// scrollable, with the first item above the top.
///
/// This also affects whether, when an item is added or removed, the
/// displacement will be towards the first item or the last item.
/// Continuing the earlier example, if a new item is inserted in the
/// middle of the list, in the [ViewportAnchor.start] case the items
/// after it (with greater indices, down to the item with the
/// highest index) will be pushed down, while in the
/// [ViewportAnchor.end] case the items before it (with lower
/// indices, up to the item with the index 0) will be pushed up.
final ViewportAnchor scrollAnchor;
/// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart;
/// Called whenever this widget's scroll offset changes.
final ScrollListener onScroll;
/// Called whenever this widget stops scrolling.
final ScrollListener onScrollEnd;
/// The key to use for the underlying scrollable widget.
final Key scrollableKey;
@override
Widget build(BuildContext context) {
Widget contents = new BlockBody(children: children, mainAxis: scrollDirection);
if (padding != null)
contents = new Padding(padding: padding, child: contents);
return new ScrollableViewport(
scrollableKey: scrollableKey,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
child: contents
);
}
}
......@@ -14,7 +14,6 @@ export 'src/widgets/async.dart';
export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart';
export 'src/widgets/clamp_overscrolls.dart';
export 'src/widgets/container.dart';
export 'src/widgets/debug.dart';
export 'src/widgets/dismissable.dart';
......@@ -45,7 +44,6 @@ export 'src/widgets/placeholder.dart';
export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_controller.dart';
export 'src/widgets/scroll_notification.dart';
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
......
......@@ -78,8 +78,7 @@ void main() {
});
testWidgets('Drawer scrolling', (WidgetTester tester) async {
GlobalKey<ScrollableState<Scrollable>> drawerKey =
new GlobalKey<ScrollableState<Scrollable>>(debugLabel: 'drawer');
Key drawerKey = new UniqueKey();
const double appBarHeight = 256.0;
ScrollController scrollOffset = new ScrollController();
......
......@@ -313,7 +313,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 100));
expect(scrollableState.position.pixels, greaterThan(0.0));
}, skip: Scrollable != Scrollable2); // TODO(abarth): re-enable when ensureVisible is implemented
});
testWidgets('Stepper index test', (WidgetTester tester) async {
await tester.pumpWidget(
......
......@@ -33,7 +33,7 @@ void main() {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (_) {
return new Material(
child: new Viewport(
child: new SingleChildScrollView(
child: new TwoLevelList(
children: <Widget>[
new TwoLevelListItem(title: new Text('Top'), key: topKey),
......@@ -91,7 +91,7 @@ void main() {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (_) {
return new Material(
child: new Viewport(
child: new SingleChildScrollView(
child: new TwoLevelList(
children: <Widget>[
new TwoLevelSublist(
......
......@@ -206,26 +206,24 @@ void main() {
SpringDescription spring = new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 50.0, ratio: 0.5);
ScrollSimulation scroll = new ScrollSimulation(
BouncingScrollSimulation scroll = new BouncingScrollSimulation(
position: 100.0,
velocity: 800.0,
leadingExtent: 0.0,
trailingExtent: 300.0,
spring: spring,
drag: 0.3
);
scroll.tolerance = const Tolerance(velocity: 0.5, distance: 0.1);
expect(scroll.isDone(0.0), false);
expect(scroll.isDone(0.5), false); // switch from friction to spring
expect(scroll.isDone(3.5), true);
ScrollSimulation scroll2 = new ScrollSimulation(
BouncingScrollSimulation scroll2 = new BouncingScrollSimulation(
position: 100.0,
velocity: -800.0,
leadingExtent: 0.0,
trailingExtent: 300.0,
spring: spring,
drag: 0.3
);
scroll2.tolerance = const Tolerance(velocity: 0.5, distance: 0.1);
expect(scroll2.isDone(0.0), false);
......@@ -237,13 +235,12 @@ void main() {
SpringDescription spring = new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 50.0, ratio: 0.5);
ScrollSimulation scroll = new ScrollSimulation(
BouncingScrollSimulation scroll = new BouncingScrollSimulation(
position: 100.0,
velocity: 400.0,
leadingExtent: 0.0,
trailingExtent: double.INFINITY,
spring: spring,
drag: 0.3,
);
scroll.tolerance = const Tolerance(velocity: 1.0);
......@@ -251,15 +248,14 @@ void main() {
expect(scroll.x(0.0), 100);
expect(scroll.dx(0.0), 400.0);
expect(scroll.x(1.0) > 330 && scroll.x(1.0) < 335, true);
expect(scroll.x(1.0), closeTo(272.0, 1.0));
expect(scroll.dx(1.0), 120.0);
expect(scroll.dx(2.0), 36.0);
expect(scroll.dx(3.0), 10.8);
expect(scroll.dx(4.0) < 3.5, true);
expect(scroll.dx(1.0), closeTo(54.0, 1.0));
expect(scroll.dx(2.0), closeTo(7.0, 1.0));
expect(scroll.dx(3.0), lessThan(1.0));
expect(scroll.isDone(5.0), true);
expect(scroll.x(5.0) > 431 && scroll.x(5.0) < 432, true);
expect(scroll.x(5.0), closeTo(300.0, 1.0));
// We should never switch
expect(scroll.currentIntervalOffset, 0.0);
......@@ -267,13 +263,12 @@ void main() {
test('over/under scroll spring', () {
SpringDescription spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
ScrollSimulation scroll = new ScrollSimulation(
BouncingScrollSimulation scroll = new BouncingScrollSimulation(
position: 500.0,
velocity: -7500.0,
leadingExtent: 0.0,
trailingExtent: 1000.0,
spring: spring,
drag: 0.025,
);
scroll.tolerance = const Tolerance(velocity: 45.0, distance: 1.5);
......@@ -282,8 +277,8 @@ void main() {
expect(scroll.dx(0.0), closeTo(-7500.0, .0001));
expect(scroll.isDone(0.025), false);
expect(scroll.x(0.025), closeTo(320.0, 1.0));
expect(scroll.dx(0.25), closeTo(-2982, 1.0));
expect(scroll.x(0.025), closeTo(317.0, 1.0));
expect(scroll.dx(0.25), closeTo(-4546, 1.0));
expect(scroll.isDone(2.0), true);
expect(scroll.x(2.0), 0.0);
......
......@@ -10,17 +10,19 @@ import 'rendering_tester.dart';
class TestLayout {
TestLayout() {
// viewport incoming constraints are tight 800x600
// viewport is vertical by default
root = new RenderViewport(
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; }
// incoming constraints are tight 800x600
root = new RenderPositionedBox(
child: new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 800.0),
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; }
),
child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 10.0, width: 10.0),
),
),
child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 10.0, width: 10.0)
)
)
),
);
}
RenderBox root;
......
......@@ -12,19 +12,21 @@ void main() {
test("offstage", () {
RenderBox child;
bool painted = false;
// viewport incoming constraints are tight 800x600
// viewport is vertical by default
RenderBox root = new RenderViewport(
child: new RenderOffstage(
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; }
// incoming constraints are tight 800x600
RenderBox root = new RenderPositionedBox(
child: new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 800.0),
child: new RenderOffstage(
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; },
),
child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 10.0, width: 10.0),
),
),
child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 10.0, width: 10.0)
)
)
)
),
),
);
expect(child.hasSize, isFalse);
expect(painted, isFalse);
......
......@@ -10,30 +10,32 @@ import 'rendering_tester.dart';
class TestTree {
TestTree() {
// viewport incoming constraints are tight 800x600
// viewport is vertical by default
root = new RenderViewport(
// Place the child to be evaluated within both a repaint boundary and a
// layout-root element (in this case a tightly constrained box). Otherwise
// the act of transplanting the root into a new container will cause the
// relayout/repaint of the new parent node to satisfy the test.
child: new RenderRepaintBoundary(
child: new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0),
child: new RenderRepaintBoundary(
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; }
// incoming constraints are tight 800x600
root = new RenderPositionedBox(
child: new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 800.0),
// Place the child to be evaluated within both a repaint boundary and a
// layout-root element (in this case a tightly constrained box). Otherwise
// the act of transplanting the root into a new container will cause the
// relayout/repaint of the new parent node to satisfy the test.
child: new RenderRepaintBoundary(
child: new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0),
child: new RenderRepaintBoundary(
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; },
),
child: new RenderPositionedBox(
child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0),
),
),
),
child: new RenderPositionedBox(
child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0)
)
)
)
)
)
)
),
),
),
),
);
}
RenderObject root;
......@@ -50,24 +52,26 @@ class MutableCompositor extends RenderProxyBox {
class TestCompositingBitsTree {
TestCompositingBitsTree() {
// viewport incoming constraints are tight 800x600
// viewport is vertical by default
root = new RenderViewport(
// Place the child to be evaluated within a repaint boundary. Otherwise
// the act of transplanting the root into a new container will cause the
// repaint of the new parent node to satisfy the test.
child: new RenderRepaintBoundary(
child: compositor = new MutableCompositor(
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; }
// incoming constraints are tight 800x600
root = new RenderPositionedBox(
child: new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 800.0),
// Place the child to be evaluated within a repaint boundary. Otherwise
// the act of transplanting the root into a new container will cause the
// repaint of the new parent node to satisfy the test.
child: new RenderRepaintBoundary(
child: compositor = new MutableCompositor(
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; },
),
child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0)
),
),
child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0)
)
)
)
)
),
),
),
);
}
RenderObject root;
......
// 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 'package:flutter/rendering.dart';
import 'package:test/test.dart';
import 'rendering_tester.dart';
void main() {
test('Should be able to hit with positive paint offset', () {
RenderBox green = new RenderDecoratedBox(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF00FF00)
));
RenderBox size = new RenderConstrainedBox(
additionalConstraints: new BoxConstraints.tight(const Size(100.0, 100.0)),
child: green);
RenderBox red = new RenderDecoratedBox(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFFFF0000)
),
child: size);
RenderViewport viewport = new RenderViewport(child: red, paintOffset: const Offset(0.0, 10.0));
layout(viewport);
HitTestResult result;
result = new HitTestResult();
renderer.renderView.hitTest(result, position: const Point(15.0, 0.0));
expect(result.path.first.target.runtimeType, equals(RenderView));
result = new HitTestResult();
renderer.renderView.hitTest(result, position: const Point(15.0, 15.0));
expect(result.path.first.target, equals(green));
});
}
......@@ -35,8 +35,8 @@ void main() {
Key childKey = new UniqueKey();
await tester.pumpWidget(
new Center(
child: new Viewport(
mainAxis: Axis.horizontal,
child: new SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: new AspectRatio(
aspectRatio: 2.0,
child: new Container(
......
......@@ -248,7 +248,6 @@ class _FlutterDriverExtension {
Future<ScrollResult> _scrollIntoView(Command command) async {
ScrollIntoView scrollIntoViewCommand = command;
Finder target = await _waitForElement(_createFinder(scrollIntoViewCommand.finder));
await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100));
await Scrollable2.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment ?? 0.0);
return new ScrollResult();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment