Commit 7458ed22 authored by Adam Barth's avatar Adam Barth

Merge pull request #1710 from abarth/scroll_anchor

Preliminaries for scroll anchoring
parents f1cc388f 3c8cbef9
...@@ -777,7 +777,7 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect ...@@ -777,7 +777,7 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
onSizeChanged: _handleViewportSizeChanged, onSizeChanged: _handleViewportSizeChanged,
child: new Viewport( child: new Viewport(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
scrollOffset: new Offset(scrollOffset, 0.0), paintOffset: scrollOffsetToPixelDelta(scrollOffset),
child: contents child: contents
) )
); );
...@@ -929,12 +929,12 @@ class _TabBarViewState extends PageableListState<TabBarView> implements TabBarSe ...@@ -929,12 +929,12 @@ class _TabBarViewState extends PageableListState<TabBarView> implements TabBarSe
controller.value = scrollOffset / 2.0; controller.value = scrollOffset / 2.0;
} }
Future fling(Offset scrollVelocity) { Future fling(double scrollVelocity) {
if (_selection == null || _selection.valueIsChanging) if (_selection == null || _selection.valueIsChanging)
return new Future.value(); return new Future.value();
if (scrollVelocity.dx.abs() > _kMinFlingVelocity) { if (scrollVelocity.abs() > _kMinFlingVelocity) {
final int selectionDelta = scrollVelocity.dx > 0 ? -1 : 1; final int selectionDelta = scrollVelocity.sign.truncate();
final int targetIndex = (_selection.index + selectionDelta).clamp(0, _tabCount - 1); final int targetIndex = (_selection.index + selectionDelta).clamp(0, _tabCount - 1);
_selection.value = _selection.values[targetIndex]; _selection.value = _selection.values[targetIndex];
return new Future.value(); return new Future.value();
......
...@@ -314,10 +314,12 @@ class RenderGrid extends RenderVirtualViewport<GridParentData> { ...@@ -314,10 +314,12 @@ class RenderGrid extends RenderVirtualViewport<GridParentData> {
int virtualChildBase: 0, int virtualChildBase: 0,
int virtualChildCount, int virtualChildCount,
Offset paintOffset: Offset.zero, Offset paintOffset: Offset.zero,
Painter overlayPainter,
LayoutCallback callback LayoutCallback callback
}) : _delegate = delegate, _virtualChildBase = virtualChildBase, super( }) : _delegate = delegate, _virtualChildBase = virtualChildBase, super(
virtualChildCount: virtualChildCount, virtualChildCount: virtualChildCount,
paintOffset: paintOffset, paintOffset: paintOffset,
overlayPainter: overlayPainter,
callback: callback callback: callback
) { ) {
assert(delegate != null); assert(delegate != null);
...@@ -338,6 +340,15 @@ class RenderGrid extends RenderVirtualViewport<GridParentData> { ...@@ -338,6 +340,15 @@ class RenderGrid extends RenderVirtualViewport<GridParentData> {
_delegate = newDelegate; _delegate = newDelegate;
} }
void set scrollDirection(Axis value) {
assert(() {
if (value != Axis.vertical)
throw new RenderingError('RenderGrid doesn\'t yet support horizontal scrolling.');
return true;
});
super.scrollDirection = value;
}
int get virtualChildCount => super.virtualChildCount ?? childCount; int get virtualChildCount => super.virtualChildCount ?? childCount;
/// The virtual index of the first child. /// The virtual index of the first child.
......
...@@ -11,7 +11,7 @@ import 'viewport.dart'; ...@@ -11,7 +11,7 @@ import 'viewport.dart';
/// Parent data for use with [RenderList]. /// Parent data for use with [RenderList].
class ListParentData extends ContainerBoxParentDataMixin<RenderBox> { } class ListParentData extends ContainerBoxParentDataMixin<RenderBox> { }
class RenderList extends RenderVirtualViewport<ListParentData> implements HasScrollDirection { class RenderList extends RenderVirtualViewport<ListParentData> {
RenderList({ RenderList({
List<RenderBox> children, List<RenderBox> children,
double itemExtent, double itemExtent,
...@@ -19,13 +19,15 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr ...@@ -19,13 +19,15 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr
int virtualChildCount, int virtualChildCount,
Offset paintOffset: Offset.zero, Offset paintOffset: Offset.zero,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
Painter overlayPainter,
LayoutCallback callback LayoutCallback callback
}) : _itemExtent = itemExtent, }) : _itemExtent = itemExtent,
_padding = padding, _padding = padding,
_scrollDirection = scrollDirection,
super( super(
virtualChildCount: virtualChildCount, virtualChildCount: virtualChildCount,
paintOffset: paintOffset, paintOffset: paintOffset,
scrollDirection: scrollDirection,
overlayPainter: overlayPainter,
callback: callback callback: callback
) { ) {
addAll(children); addAll(children);
...@@ -50,15 +52,6 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr ...@@ -50,15 +52,6 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr
markNeedsLayout(); markNeedsLayout();
} }
Axis get scrollDirection => _scrollDirection;
Axis _scrollDirection;
void set scrollDirection (Axis newValue) {
if (_scrollDirection == newValue)
return;
_scrollDirection = newValue;
markNeedsLayout();
}
void setupParentData(RenderBox child) { void setupParentData(RenderBox child) {
if (child.parentData is! ListParentData) if (child.parentData is! ListParentData)
child.parentData = new ListParentData(); child.parentData = new ListParentData();
......
...@@ -9,30 +9,31 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -9,30 +9,31 @@ import 'package:vector_math/vector_math_64.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
enum ViewportAnchor {
start,
end,
}
abstract class HasScrollDirection { abstract class HasScrollDirection {
Axis get scrollDirection; Axis get scrollDirection;
} }
/// A render object that's bigger on the inside. /// A base class for render objects that are bigger on the inside.
///
/// The child of a viewport can layout to a larger size 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 /// This class holds the common fields for viewport render objects but does not
/// in other situations. /// have a child model. See [RenderViewport] for a viewport with a single child
class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> /// and [RenderVirtualViewport] for a viewport with multiple children.
implements HasScrollDirection { class RenderViewportBase extends RenderBox implements HasScrollDirection {
RenderViewportBase(
RenderViewport({ Offset paintOffset,
RenderBox child, Axis scrollDirection,
Offset scrollOffset: Offset.zero, Painter overlayPainter
Axis scrollDirection: Axis.vertical ) : _paintOffset = paintOffset,
}) : _scrollOffset = scrollOffset, _scrollDirection = scrollDirection,
_scrollDirection = scrollDirection { _overlayPainter = overlayPainter {
assert(_offsetIsSane(scrollOffset, scrollDirection)); assert(paintOffset != null);
this.child = child; assert(scrollDirection != null);
assert(_offsetIsSane(_paintOffset, scrollDirection));
} }
bool _offsetIsSane(Offset offset, Axis direction) { bool _offsetIsSane(Offset offset, Axis direction) {
...@@ -47,13 +48,14 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox ...@@ -47,13 +48,14 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
/// The offset at which to paint the child. /// The offset at which to paint the child.
/// ///
/// The offset can be non-zero only in the [scrollDirection]. /// The offset can be non-zero only in the [scrollDirection].
Offset get scrollOffset => _scrollOffset; Offset get paintOffset => _paintOffset;
Offset _scrollOffset; Offset _paintOffset;
void set scrollOffset(Offset value) { void set paintOffset(Offset value) {
if (value == _scrollOffset) assert(value != null);
if (value == _paintOffset)
return; return;
assert(_offsetIsSane(value, scrollDirection)); assert(_offsetIsSane(value, scrollDirection));
_scrollOffset = value; _paintOffset = value;
markNeedsPaint(); markNeedsPaint();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
...@@ -66,13 +68,69 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox ...@@ -66,13 +68,69 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
Axis get scrollDirection => _scrollDirection; Axis get scrollDirection => _scrollDirection;
Axis _scrollDirection; Axis _scrollDirection;
void set scrollDirection(Axis value) { void set scrollDirection(Axis value) {
assert(value != null);
if (value == _scrollDirection) if (value == _scrollDirection)
return; return;
assert(_offsetIsSane(scrollOffset, value)); assert(_offsetIsSane(_paintOffset, value));
_scrollDirection = value; _scrollDirection = value;
markNeedsLayout(); markNeedsLayout();
} }
Painter get overlayPainter => _overlayPainter;
Painter _overlayPainter;
void set overlayPainter(Painter value) {
if (_overlayPainter == value)
return;
if (attached)
_overlayPainter?.detach();
_overlayPainter = value;
if (attached)
_overlayPainter?.attach(this);
markNeedsPaint();
}
void attach() {
super.attach();
_overlayPainter?.attach(this);
}
void detach() {
super.detach();
_overlayPainter?.detach();
}
Offset get _paintOffsetRoundedToIntegerDevicePixels {
final double devicePixelRatio = ui.window.devicePixelRatio;
int dxInDevicePixels = (_paintOffset.dx * devicePixelRatio).round();
int dyInDevicePixels = (_paintOffset.dy * devicePixelRatio).round();
return new Offset(dxInDevicePixels / devicePixelRatio,
dyInDevicePixels / devicePixelRatio);
}
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final Offset effectivePaintOffset = _paintOffsetRoundedToIntegerDevicePixels;
super.applyPaintTransform(child, transform.translate(effectivePaintOffset.dx, effectivePaintOffset.dy));
}
}
/// A render object that's bigger on the inside.
///
/// The child of a viewport can layout to a larger size 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
/// paint offset.
class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<RenderBox> {
RenderViewport({
RenderBox child,
Offset paintOffset: Offset.zero,
Axis scrollDirection: Axis.vertical,
Painter overlayPainter
}) : super(paintOffset, scrollDirection, overlayPainter) {
this.child = child;
}
BoxConstraints _getInnerConstraints(BoxConstraints constraints) { BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
BoxConstraints innerConstraints; BoxConstraints innerConstraints;
switch (scrollDirection) { switch (scrollDirection) {
...@@ -130,41 +188,30 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox ...@@ -130,41 +188,30 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
} }
} }
Offset get _scrollOffsetRoundedToIntegerDevicePixels { bool _shouldClipAtPaintOffset(Offset paintOffset) {
double devicePixelRatio = ui.window.devicePixelRatio;
int dxInDevicePixels = (scrollOffset.dx * devicePixelRatio).round();
int dyInDevicePixels = (scrollOffset.dy * devicePixelRatio).round();
return new Offset(dxInDevicePixels / devicePixelRatio,
dyInDevicePixels / devicePixelRatio);
}
bool _wouldNeedClipAtOffset(Offset offset) {
assert(child != null); assert(child != null);
return offset < Offset.zero || !(Offset.zero & size).contains(((Offset.zero - offset) & child.size).bottomRight); return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight);
} }
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
Offset roundedScrollOffset = _scrollOffsetRoundedToIntegerDevicePixels; final Offset effectivePaintOffset = _paintOffsetRoundedToIntegerDevicePixels;
bool _needsClip = _wouldNeedClipAtOffset(roundedScrollOffset);
if (_needsClip) { void paintContents(PaintingContext context, Offset offset) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, (PaintingContext context, Offset offset) { context.paintChild(child, offset + effectivePaintOffset);
context.paintChild(child, offset - roundedScrollOffset); _overlayPainter?.paint(context, offset);
});
} else {
context.paintChild(child, offset - roundedScrollOffset);
} }
if (_shouldClipAtPaintOffset(effectivePaintOffset)) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
} else {
paintContents(context, offset);
} }
} }
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.translate(-scrollOffset.dx, -scrollOffset.dy);
super.applyPaintTransform(child, transform);
} }
Rect describeApproximatePaintClip(RenderObject child) { Rect describeApproximatePaintClip(RenderObject child) {
if (child != null && if (child != null && _shouldClipAtPaintOffset(_paintOffsetRoundedToIntegerDevicePixels))
_wouldNeedClipAtOffset(_scrollOffsetRoundedToIntegerDevicePixels))
return Point.origin & size; return Point.origin & size;
return null; return null;
} }
...@@ -172,7 +219,7 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox ...@@ -172,7 +219,7 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
bool hitTestChildren(HitTestResult result, { Point position }) { bool hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) { if (child != null) {
assert(child.parentData is BoxParentData); assert(child.parentData is BoxParentData);
Point transformed = position + _scrollOffsetRoundedToIntegerDevicePixels; Point transformed = position + -_paintOffsetRoundedToIntegerDevicePixels;
return child.hitTest(result, position: transformed); return child.hitTest(result, position: transformed);
} }
return false; return false;
...@@ -180,17 +227,17 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox ...@@ -180,17 +227,17 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
} }
abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<RenderBox>> abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<RenderBox>>
extends RenderBox with ContainerRenderObjectMixin<RenderBox, T>, extends RenderViewportBase with ContainerRenderObjectMixin<RenderBox, T>,
RenderBoxContainerDefaultsMixin<RenderBox, T> { RenderBoxContainerDefaultsMixin<RenderBox, T> {
RenderVirtualViewport({ RenderVirtualViewport({
int virtualChildCount, int virtualChildCount,
Offset paintOffset,
LayoutCallback callback, LayoutCallback callback,
Offset paintOffset: Offset.zero,
Axis scrollDirection: Axis.vertical,
Painter overlayPainter Painter overlayPainter
}) : _virtualChildCount = virtualChildCount, }) : _virtualChildCount = virtualChildCount,
_paintOffset = paintOffset,
_callback = callback, _callback = callback,
_overlayPainter = overlayPainter; super(paintOffset, scrollDirection, overlayPainter);
int get virtualChildCount => _virtualChildCount; int get virtualChildCount => _virtualChildCount;
int _virtualChildCount; int _virtualChildCount;
...@@ -201,21 +248,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende ...@@ -201,21 +248,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
markNeedsLayout(); markNeedsLayout();
} }
/// The offset at which to paint the first item. /// Called during [layout] to determine the render object's children.
///
/// Note: you can modify this property from within [callback], if necessary.
Offset get paintOffset => _paintOffset;
Offset _paintOffset;
void set paintOffset(Offset value) {
assert(value != null);
if (value == _paintOffset)
return;
_paintOffset = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Called during [layout] to determine the grid's children.
/// ///
/// Typically the callback will mutate the child list appropriately, for /// Typically the callback will mutate the child list appropriately, for
/// example so the child list contains only visible children. /// example so the child list contains only visible children.
...@@ -228,39 +261,12 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende ...@@ -228,39 +261,12 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
markNeedsLayout(); markNeedsLayout();
} }
Painter get overlayPainter => _overlayPainter;
Painter _overlayPainter;
void set overlayPainter(Painter value) {
if (_overlayPainter == value)
return;
if (attached)
_overlayPainter?.detach();
_overlayPainter = value;
if (attached)
_overlayPainter?.attach(this);
markNeedsPaint();
}
void attach() {
super.attach();
_overlayPainter?.attach(this);
}
void detach() {
super.detach();
_overlayPainter?.detach();
}
void applyPaintTransform(RenderBox child, Matrix4 transform) {
super.applyPaintTransform(child, transform.translate(paintOffset.dx, paintOffset.dy));
}
bool hitTestChildren(HitTestResult result, { Point position }) { bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position + -paintOffset); return defaultHitTestChildren(result, position: position + -_paintOffsetRoundedToIntegerDevicePixels);
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
defaultPaint(context, offset + paintOffset); defaultPaint(context, offset + _paintOffsetRoundedToIntegerDevicePixels);
_overlayPainter?.paint(context, offset); _overlayPainter?.paint(context, offset);
} }
......
...@@ -797,11 +797,12 @@ class Viewport extends OneChildRenderObjectWidget { ...@@ -797,11 +797,12 @@ class Viewport extends OneChildRenderObjectWidget {
Viewport({ Viewport({
Key key, Key key,
this.scrollDirection: Axis.vertical, this.scrollDirection: Axis.vertical,
this.scrollOffset: Offset.zero, this.paintOffset: Offset.zero,
this.overlayPainter,
Widget child Widget child
}) : super(key: key, child: child) { }) : super(key: key, child: child) {
assert(scrollDirection != null); assert(scrollDirection != null);
assert(scrollOffset != null); assert(paintOffset != null);
} }
/// The direction in which the child is permitted to be larger than the viewport /// The direction in which the child is permitted to be larger than the viewport
...@@ -814,14 +815,25 @@ class Viewport extends OneChildRenderObjectWidget { ...@@ -814,14 +815,25 @@ class Viewport extends OneChildRenderObjectWidget {
/// The offset at which to paint the child. /// The offset at which to paint the child.
/// ///
/// The offset can be non-zero only in the [scrollDirection]. /// The offset can be non-zero only in the [scrollDirection].
final Offset scrollOffset; final Offset paintOffset;
RenderViewport createRenderObject() => new RenderViewport(scrollDirection: scrollDirection, scrollOffset: scrollOffset); /// Paints an overlay over the viewport.
///
/// Often used to paint scroll bars.
final Painter overlayPainter;
RenderViewport createRenderObject() => new RenderViewport(
scrollDirection: scrollDirection,
paintOffset: paintOffset,
overlayPainter: overlayPainter
);
void updateRenderObject(RenderViewport renderObject, Viewport oldWidget) { void updateRenderObject(RenderViewport renderObject, Viewport oldWidget) {
// Order dependency: RenderViewport validates scrollOffset based on scrollDirection. // Order dependency: RenderViewport validates scrollOffset based on scrollDirection.
renderObject.scrollDirection = scrollDirection; renderObject
renderObject.scrollOffset = scrollOffset; ..scrollDirection = scrollDirection
..paintOffset = paintOffset
..overlayPainter = overlayPainter;
} }
} }
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
...@@ -65,12 +64,25 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> { ...@@ -65,12 +64,25 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
int get itemCount => config.children?.length ?? 0; int get itemCount => config.children?.length ?? 0;
int _previousItemCount; int _previousItemCount;
double pixelToScrollOffset(double value) { double get _pixelsPerScrollUnit {
final RenderBox box = context.findRenderObject(); final RenderBox box = context.findRenderObject();
if (box == null || !box.hasSize) if (box == null || !box.hasSize)
return 0.0; return 0.0;
final double pixelScrollExtent = config.scrollDirection == Axis.vertical ? box.size.height : box.size.width; switch (config.scrollDirection) {
return pixelScrollExtent == 0.0 ? 0.0 : value / pixelScrollExtent; case Axis.horizontal:
return box.size.width;
case Axis.vertical:
return box.size.height;
}
}
double pixelOffsetToScrollOffset(double pixelOffset) {
final double pixelsPerScrollUnit = _pixelsPerScrollUnit;
return super.pixelOffsetToScrollOffset(pixelsPerScrollUnit == 0.0 ? 0.0 : pixelOffset / pixelsPerScrollUnit);
}
double scrollOffsetToPixelOffset(double scrollOffset) {
return super.scrollOffsetToPixelOffset(scrollOffset * _pixelsPerScrollUnit);
} }
void initState() { void initState() {
...@@ -143,7 +155,7 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> { ...@@ -143,7 +155,7 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
ScrollBehavior createScrollBehavior() => scrollBehavior; ScrollBehavior createScrollBehavior() => scrollBehavior;
bool get snapScrollOffsetChanges => config.itemsSnapAlignment == ItemsSnapAlignment.item; bool get shouldSnapScrollOffset => config.itemsSnapAlignment == ItemsSnapAlignment.item;
double snapScrollOffset(double newScrollOffset) { double snapScrollOffset(double newScrollOffset) {
final double previousItemOffset = newScrollOffset.floorToDouble(); final double previousItemOffset = newScrollOffset.floorToDouble();
...@@ -152,20 +164,19 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> { ...@@ -152,20 +164,19 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset); .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
} }
Future _flingToAdjacentItem(Offset velocity) { Future _flingToAdjacentItem(double scrollVelocity) {
final double scrollVelocity = scrollDirectionVelocity(velocity);
final double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign) final double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign)
.clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5)); .clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5));
return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve) return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve)
.then(_notifyPageChanged); .then(_notifyPageChanged);
} }
Future fling(Offset velocity) { Future fling(double scrollVelocity) {
switch(config.itemsSnapAlignment) { switch(config.itemsSnapAlignment) {
case ItemsSnapAlignment.adjacentItem: case ItemsSnapAlignment.adjacentItem:
return _flingToAdjacentItem(velocity); return _flingToAdjacentItem(scrollVelocity);
default: default:
return super.fling(velocity).then(_notifyPageChanged); return super.fling(scrollVelocity).then(_notifyPageChanged);
} }
} }
......
...@@ -19,11 +19,6 @@ import 'notification_listener.dart'; ...@@ -19,11 +19,6 @@ import 'notification_listener.dart';
import 'page_storage.dart'; import 'page_storage.dart';
import 'scroll_behavior.dart'; import 'scroll_behavior.dart';
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
const double _kMillisecondsPerSecond = 1000.0;
const double _kMinFlingVelocity = -kMaxFlingVelocity * _kMillisecondsPerSecond;
const double _kMaxFlingVelocity = kMaxFlingVelocity * _kMillisecondsPerSecond;
/// The accuracy to which scrolling is computed. /// The accuracy to which scrolling is computed.
final Tolerance kPixelScrollTolerance = new Tolerance( final Tolerance kPixelScrollTolerance = new Tolerance(
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
...@@ -45,14 +40,16 @@ abstract class Scrollable extends StatefulComponent { ...@@ -45,14 +40,16 @@ abstract class Scrollable extends StatefulComponent {
Key key, Key key,
this.initialScrollOffset, this.initialScrollOffset,
this.scrollDirection: Axis.vertical, this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.onScrollStart, this.onScrollStart,
this.onScroll, this.onScroll,
this.onScrollEnd, this.onScrollEnd,
this.snapOffsetCallback, this.snapOffsetCallback,
this.snapAlignmentOffset: 0.0 this.snapAlignmentOffset: 0.0
}) : super(key: key) { }) : super(key: key) {
assert(scrollDirection == Axis.vertical || assert(scrollDirection == Axis.vertical || scrollDirection == Axis.horizontal);
scrollDirection == Axis.horizontal); assert(scrollAnchor == ViewportAnchor.start || scrollAnchor == ViewportAnchor.end);
assert(snapAlignmentOffset != null);
} }
/// The scroll offset this widget should use when first created. /// The scroll offset this widget should use when first created.
...@@ -61,6 +58,8 @@ abstract class Scrollable extends StatefulComponent { ...@@ -61,6 +58,8 @@ abstract class Scrollable extends StatefulComponent {
/// The axis along which this widget should scroll. /// The axis along which this widget should scroll.
final Axis scrollDirection; final Axis scrollDirection;
final ViewportAnchor scrollAnchor;
/// Called whenever this widget starts to scroll. /// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart; final ScrollListener onScrollStart;
...@@ -172,13 +171,46 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -172,13 +171,46 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// Scrollable gesture handlers convert their incoming values with this method. /// Scrollable gesture handlers convert their incoming values with this method.
/// Subclasses that define scrollOffset in units other than pixels must /// Subclasses that define scrollOffset in units other than pixels must
/// override this method. /// override this method.
double pixelToScrollOffset(double pixelValue) => pixelValue; 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;
}
}
/// Returns the component of the given velocity in the scroll direction. double scrollOffsetToPixelOffset(double scrollOffset) {
double scrollDirectionVelocity(Offset scrollVelocity) { switch (config.scrollAnchor) {
return config.scrollDirection == Axis.horizontal case ViewportAnchor.start:
? -scrollVelocity.dx return -scrollOffset;
: -scrollVelocity.dy; case ViewportAnchor.end:
return scrollOffset;
}
}
/// Returns the scroll offset component of the given pixel delta, accounting
/// for the scroll direction and scroll anchor.
double pixelDeltaToScrollOffset(Offset pixelDelta) {
switch (config.scrollDirection) {
case Axis.horizontal:
return pixelOffsetToScrollOffset(pixelDelta.dx);
case Axis.vertical:
return pixelOffsetToScrollOffset(pixelDelta.dy);
}
}
/// Returns a two-dimensional representation of the scroll offset, accounting
/// for the scroll direction and scroll anchor.
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));
}
} }
ScrollBehavior _scrollBehavior; ScrollBehavior _scrollBehavior;
...@@ -244,19 +276,19 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -244,19 +276,19 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return _controller.animateTo(newScrollOffset, duration: duration, curve: curve); return _controller.animateTo(newScrollOffset, duration: duration, curve: curve);
} }
bool _scrollOffsetIsInBounds(double offset) { bool _scrollOffsetIsInBounds(double scrollOffset) {
if (scrollBehavior is! ExtentScrollBehavior) if (scrollBehavior is! ExtentScrollBehavior)
return false; return false;
ExtentScrollBehavior behavior = scrollBehavior; ExtentScrollBehavior behavior = scrollBehavior;
return offset >= behavior.minScrollOffset && offset < behavior.maxScrollOffset; return scrollOffset >= behavior.minScrollOffset && scrollOffset < behavior.maxScrollOffset;
} }
Simulation _createFlingSimulation(double velocity) { Simulation _createFlingSimulation(double scrollVelocity) {
final Simulation simulation = scrollBehavior.createFlingScrollSimulation(scrollOffset, velocity); final Simulation simulation = scrollBehavior.createFlingScrollSimulation(scrollOffset, scrollVelocity);
if (simulation != null) { if (simulation != null) {
final double endVelocity = pixelToScrollOffset(kPixelScrollTolerance.velocity); final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity) * scrollVelocity.sign;
final double endDistance = pixelToScrollOffset(kPixelScrollTolerance.distance); final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs();
simulation.tolerance = new Tolerance(velocity: endVelocity.abs(), distance: endDistance); simulation.tolerance = new Tolerance(velocity: endVelocity, distance: endDistance);
} }
return simulation; return simulation;
} }
...@@ -267,41 +299,40 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -267,41 +299,40 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
} }
/// Whether this scrollable should attempt to snap scroll offsets. /// Whether this scrollable should attempt to snap scroll offsets.
bool get snapScrollOffsetChanges => config.snapOffsetCallback != null; bool get shouldSnapScrollOffset => config.snapOffsetCallback != null;
Simulation _createSnapSimulation(double velocity) { Simulation _createSnapSimulation(double scrollVelocity) {
if (!snapScrollOffsetChanges || velocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset)) if (!shouldSnapScrollOffset || scrollVelocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
return null; return null;
Simulation simulation = _createFlingSimulation(velocity); Simulation simulation = _createFlingSimulation(scrollVelocity);
if (simulation == null) if (simulation == null)
return null; return null;
double endScrollOffset = simulation.x(double.INFINITY); final double endScrollOffset = simulation.x(double.INFINITY);
if (endScrollOffset.isNaN) if (endScrollOffset.isNaN)
return null; return null;
double snappedScrollOffset = snapScrollOffset(endScrollOffset + config.snapAlignmentOffset); final double snappedScrollOffset = snapScrollOffset(endScrollOffset + config.snapAlignmentOffset);
double alignedScrollOffset = snappedScrollOffset - config.snapAlignmentOffset; final double alignedScrollOffset = snappedScrollOffset - config.snapAlignmentOffset;
if (!_scrollOffsetIsInBounds(alignedScrollOffset)) if (!_scrollOffsetIsInBounds(alignedScrollOffset))
return null; return null;
double snapVelocity = velocity.abs() * (alignedScrollOffset - scrollOffset).sign; final double snapVelocity = scrollVelocity.abs() * (alignedScrollOffset - scrollOffset).sign;
double endVelocity = pixelToScrollOffset(kPixelScrollTolerance.velocity * velocity.sign); final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * scrollVelocity.sign;
Simulation toSnapSimulation = Simulation toSnapSimulation =
scrollBehavior.createSnapScrollSimulation(scrollOffset, alignedScrollOffset, snapVelocity, endVelocity); scrollBehavior.createSnapScrollSimulation(scrollOffset, alignedScrollOffset, snapVelocity, endVelocity);
if (toSnapSimulation == null) if (toSnapSimulation == null)
return null; return null;
double offsetMin = math.min(scrollOffset, alignedScrollOffset); final double scrollOffsetMin = math.min(scrollOffset, alignedScrollOffset);
double offsetMax = math.max(scrollOffset, alignedScrollOffset); final double scrollOffsetMax = math.max(scrollOffset, alignedScrollOffset);
return new ClampedSimulation(toSnapSimulation, xMin: offsetMin, xMax: offsetMax); return new ClampedSimulation(toSnapSimulation, xMin: scrollOffsetMin, xMax: scrollOffsetMax);
} }
Future _startToEndAnimation(Offset scrollVelocity) { Future _startToEndAnimation(double scrollVelocity) {
double velocity = scrollDirectionVelocity(scrollVelocity);
_controller.stop(); _controller.stop();
Simulation simulation = _createSnapSimulation(velocity) ?? _createFlingSimulation(velocity); Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
if (simulation == null) if (simulation == null)
return new Future.value(); return new Future.value();
return _controller.animateWith(simulation); return _controller.animateWith(simulation);
...@@ -358,8 +389,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -358,8 +389,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// Calling this function starts a physics-based animation of the scroll /// Calling this function starts a physics-based animation of the scroll
/// offset with the given value as the initial velocity. The physics /// offset with the given value as the initial velocity. The physics
/// simulation used is determined by the scroll behavior. /// simulation used is determined by the scroll behavior.
Future fling(Offset scrollVelocity) { Future fling(double scrollVelocity) {
if (scrollVelocity != Offset.zero) if (scrollVelocity != 0.0)
return _startToEndAnimation(scrollVelocity); return _startToEndAnimation(scrollVelocity);
if (!_controller.isAnimating) if (!_controller.isAnimating)
return settleScrollOffset(); return settleScrollOffset();
...@@ -372,7 +403,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -372,7 +403,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// offset either to a snap point or to within the scrolling bounds. The /// offset either to a snap point or to within the scrolling bounds. The
/// physics simulation used is determined by the scroll behavior. /// physics simulation used is determined by the scroll behavior.
Future settleScrollOffset() { Future settleScrollOffset() {
return _startToEndAnimation(Offset.zero); return _startToEndAnimation(0.0);
} }
/// Calls the onScrollStart callback. /// Calls the onScrollStart callback.
...@@ -408,18 +439,13 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -408,18 +439,13 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
} }
void _handleDragUpdate(double delta) { void _handleDragUpdate(double delta) {
// We negate the delta here because a positive scroll offset moves the scrollBy(pixelOffsetToScrollOffset(delta));
// the content up (or to the left) rather than down (or the right).
scrollBy(pixelToScrollOffset(-delta));
}
double _toScrollVelocity(double velocity) {
return pixelToScrollOffset(velocity.clamp(_kMinFlingVelocity, _kMaxFlingVelocity) / _kMillisecondsPerSecond);
} }
Future _handleDragEnd(Offset pixelScrollVelocity) { Future _handleDragEnd(Offset velocity) {
final Offset scrollVelocity = new Offset(_toScrollVelocity(pixelScrollVelocity.dx), _toScrollVelocity(pixelScrollVelocity.dy)); double scrollVelocity = pixelDeltaToScrollOffset(velocity) / Duration.MILLISECONDS_PER_SECOND;
return fling(scrollVelocity).then((_) { // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then((_) {
dispatchOnScrollEnd(); dispatchOnScrollEnd();
}); });
} }
...@@ -441,15 +467,17 @@ class ScrollNotification extends Notification { ...@@ -441,15 +467,17 @@ class ScrollNotification extends Notification {
class ScrollableViewport extends Scrollable { class ScrollableViewport extends Scrollable {
ScrollableViewport({ ScrollableViewport({
Key key, Key key,
this.child,
double initialScrollOffset, double initialScrollOffset,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScrollStart, ScrollListener onScrollStart,
ScrollListener onScroll, ScrollListener onScroll,
ScrollListener onScrollEnd ScrollListener onScrollEnd,
this.child
}) : super( }) : super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
initialScrollOffset: initialScrollOffset, initialScrollOffset: initialScrollOffset,
onScrollStart: onScrollStart, onScrollStart: onScrollStart,
onScroll: onScroll, onScroll: onScroll,
...@@ -488,18 +516,12 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> { ...@@ -488,18 +516,12 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
)); ));
} }
Offset get _scrollOffsetVector {
if (config.scrollDirection == Axis.horizontal)
return new Offset(scrollOffset, 0.0);
return new Offset(0.0, scrollOffset);
}
Widget buildContent(BuildContext context) { Widget buildContent(BuildContext context) {
return new SizeObserver( return new SizeObserver(
onSizeChanged: _handleViewportSizeChanged, onSizeChanged: _handleViewportSizeChanged,
child: new Viewport( child: new Viewport(
scrollOffset: _scrollOffsetVector,
scrollDirection: config.scrollDirection, scrollDirection: config.scrollDirection,
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
child: new SizeObserver( child: new SizeObserver(
onSizeChanged: _handleChildSizeChanged, onSizeChanged: _handleChildSizeChanged,
child: config.child child: config.child
......
...@@ -24,7 +24,7 @@ void main() { ...@@ -24,7 +24,7 @@ void main() {
), ),
child: size); child: size);
RenderViewport viewport = new RenderViewport(child: red, scrollOffset: new Offset(0.0, -10.0)); RenderViewport viewport = new RenderViewport(child: red, paintOffset: new Offset(0.0, 10.0));
layout(viewport); layout(viewport);
HitTestResult result; HitTestResult result;
......
...@@ -48,14 +48,11 @@ void set scrollOffset(double value) { ...@@ -48,14 +48,11 @@ void set scrollOffset(double value) {
} }
Future fling(double velocity) { Future fling(double velocity) {
Offset velocityOffset = scrollDirection == Axis.vertical return scrollableState.fling(velocity);
? new Offset(0.0, velocity)
: new Offset(velocity, 0.0);
return scrollableState.fling(velocityOffset);
} }
void main() { void main() {
test('ScrollableList snap scrolling, fling(-0.8)', () { test('ScrollableList snap scrolling, fling(0.8)', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
tester.pumpWidget(buildFrame()); tester.pumpWidget(buildFrame());
...@@ -65,7 +62,7 @@ void main() { ...@@ -65,7 +62,7 @@ void main() {
Duration dt = const Duration(seconds: 2); Duration dt = const Duration(seconds: 2);
fling(-0.8); fling(0.8);
tester.pump(); // Start the scheduler at 0.0 tester.pump(); // Start the scheduler at 0.0
tester.pump(dt); tester.pump(dt);
expect(scrollOffset, closeTo(200.0, 1.0)); expect(scrollOffset, closeTo(200.0, 1.0));
...@@ -74,7 +71,7 @@ void main() { ...@@ -74,7 +71,7 @@ void main() {
tester.pump(); tester.pump();
expect(scrollOffset, 0.0); expect(scrollOffset, 0.0);
fling(-2.0); fling(2.0);
tester.pump(); tester.pump();
tester.pump(dt); tester.pump(dt);
expect(scrollOffset, closeTo(400.0, 1.0)); expect(scrollOffset, closeTo(400.0, 1.0));
...@@ -83,7 +80,7 @@ void main() { ...@@ -83,7 +80,7 @@ void main() {
tester.pump(); tester.pump();
expect(scrollOffset, 400.0); expect(scrollOffset, 400.0);
fling(0.8); fling(-0.8);
tester.pump(); tester.pump();
tester.pump(dt); tester.pump(dt);
expect(scrollOffset, closeTo(0.0, 1.0)); expect(scrollOffset, closeTo(0.0, 1.0));
...@@ -92,7 +89,7 @@ void main() { ...@@ -92,7 +89,7 @@ void main() {
tester.pump(); tester.pump();
expect(scrollOffset, 800.0); expect(scrollOffset, 800.0);
fling(2.0); fling(-2.0);
tester.pump(); tester.pump();
tester.pump(dt); tester.pump(dt);
expect(scrollOffset, closeTo(200.0, 1.0)); expect(scrollOffset, closeTo(200.0, 1.0));
...@@ -102,7 +99,7 @@ void main() { ...@@ -102,7 +99,7 @@ void main() {
expect(scrollOffset, 800.0); expect(scrollOffset, 800.0);
bool completed = false; bool completed = false;
fling(2.0).then((_) { fling(-2.0).then((_) {
completed = true; completed = true;
expect(scrollOffset, closeTo(200.0, 1.0)); expect(scrollOffset, closeTo(200.0, 1.0));
}); });
......
...@@ -298,6 +298,7 @@ class AnalyzeCommand extends FlutterCommand { ...@@ -298,6 +298,7 @@ class AnalyzeCommand extends FlutterCommand {
new RegExp(r'\[lint\] Prefer using lowerCamelCase for constant names.'), // sometimes we have no choice (e.g. when matching other platforms) new RegExp(r'\[lint\] Prefer using lowerCamelCase for constant names.'), // sometimes we have no choice (e.g. when matching other platforms)
new RegExp(r'\[lint\] Avoid defining a one-member abstract class when a simple function will do.'), // too many false-positives; code review should catch real instances new RegExp(r'\[lint\] Avoid defining a one-member abstract class when a simple function will do.'), // too many false-positives; code review should catch real instances
new RegExp(r'\[info\] TODO.+'), new RegExp(r'\[info\] TODO.+'),
new RegExp('\\[warning\\] Missing concrete implementation of \'RenderObject\\.applyPaintTransform\''), // https://github.com/dart-lang/sdk/issues/25232
new RegExp(r'[0-9]+ (error|warning|hint|lint).+found\.'), new RegExp(r'[0-9]+ (error|warning|hint|lint).+found\.'),
new RegExp(r'^$'), new RegExp(r'^$'),
]; ];
......
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