Improve unit conversions in Scrollable
This commit is contained in:
@@ -922,12 +922,12 @@ class _TabBarViewState extends PageableListState<TabBarView> implements TabBarSe
|
||||
controller.value = scrollOffset / 2.0;
|
||||
}
|
||||
|
||||
Future fling(Offset scrollVelocity) {
|
||||
Future fling(double scrollVelocity) {
|
||||
if (_selection == null || _selection.valueIsChanging)
|
||||
return new Future.value();
|
||||
|
||||
if (scrollVelocity.dx.abs() > _kMinFlingVelocity) {
|
||||
final int selectionDelta = scrollVelocity.dx > 0 ? -1 : 1;
|
||||
if (scrollVelocity.abs() > _kMinFlingVelocity) {
|
||||
final int selectionDelta = scrollVelocity.sign.truncate();
|
||||
final int targetIndex = (_selection.index + selectionDelta).clamp(0, _tabCount - 1);
|
||||
_selection.value = _selection.values[targetIndex];
|
||||
return new Future.value();
|
||||
|
||||
@@ -9,6 +9,11 @@ import 'package:vector_math/vector_math_64.dart';
|
||||
import 'box.dart';
|
||||
import 'object.dart';
|
||||
|
||||
enum ViewportAnchor {
|
||||
start,
|
||||
end,
|
||||
}
|
||||
|
||||
abstract class HasScrollDirection {
|
||||
Axis get scrollDirection;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
@@ -65,12 +64,25 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
|
||||
int get itemCount => config.children?.length ?? 0;
|
||||
int _previousItemCount;
|
||||
|
||||
double pixelToScrollOffset(double value) {
|
||||
double get _pixelsPerScrollUnit {
|
||||
final RenderBox box = context.findRenderObject();
|
||||
if (box == null || !box.hasSize)
|
||||
return 0.0;
|
||||
final double pixelScrollExtent = config.scrollDirection == Axis.vertical ? box.size.height : box.size.width;
|
||||
return pixelScrollExtent == 0.0 ? 0.0 : value / pixelScrollExtent;
|
||||
switch (config.scrollDirection) {
|
||||
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() {
|
||||
@@ -143,7 +155,7 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
|
||||
|
||||
ScrollBehavior createScrollBehavior() => scrollBehavior;
|
||||
|
||||
bool get snapScrollOffsetChanges => config.itemsSnapAlignment == ItemsSnapAlignment.item;
|
||||
bool get shouldSnapScrollOffset => config.itemsSnapAlignment == ItemsSnapAlignment.item;
|
||||
|
||||
double snapScrollOffset(double newScrollOffset) {
|
||||
final double previousItemOffset = newScrollOffset.floorToDouble();
|
||||
@@ -152,20 +164,19 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
|
||||
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
|
||||
}
|
||||
|
||||
Future _flingToAdjacentItem(Offset velocity) {
|
||||
final double scrollVelocity = scrollDirectionVelocity(velocity);
|
||||
Future _flingToAdjacentItem(double scrollVelocity) {
|
||||
final double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign)
|
||||
.clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5));
|
||||
return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve)
|
||||
.then(_notifyPageChanged);
|
||||
}
|
||||
|
||||
Future fling(Offset velocity) {
|
||||
Future fling(double scrollVelocity) {
|
||||
switch(config.itemsSnapAlignment) {
|
||||
case ItemsSnapAlignment.adjacentItem:
|
||||
return _flingToAdjacentItem(velocity);
|
||||
return _flingToAdjacentItem(scrollVelocity);
|
||||
default:
|
||||
return super.fling(velocity).then(_notifyPageChanged);
|
||||
return super.fling(scrollVelocity).then(_notifyPageChanged);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,6 @@ import 'notification_listener.dart';
|
||||
import 'page_storage.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.
|
||||
final Tolerance kPixelScrollTolerance = new Tolerance(
|
||||
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
|
||||
@@ -45,14 +40,16 @@ abstract class Scrollable extends StatefulComponent {
|
||||
Key key,
|
||||
this.initialScrollOffset,
|
||||
this.scrollDirection: Axis.vertical,
|
||||
this.scrollAnchor: ViewportAnchor.start,
|
||||
this.onScrollStart,
|
||||
this.onScroll,
|
||||
this.onScrollEnd,
|
||||
this.snapOffsetCallback,
|
||||
this.snapAlignmentOffset: 0.0
|
||||
}) : super(key: key) {
|
||||
assert(scrollDirection == Axis.vertical ||
|
||||
scrollDirection == Axis.horizontal);
|
||||
assert(scrollDirection == Axis.vertical || scrollDirection == Axis.horizontal);
|
||||
assert(scrollAnchor == ViewportAnchor.start || scrollAnchor == ViewportAnchor.end);
|
||||
assert(snapAlignmentOffset != null);
|
||||
}
|
||||
|
||||
/// The scroll offset this widget should use when first created.
|
||||
@@ -61,6 +58,8 @@ abstract class Scrollable extends StatefulComponent {
|
||||
/// The axis along which this widget should scroll.
|
||||
final Axis scrollDirection;
|
||||
|
||||
final ViewportAnchor scrollAnchor;
|
||||
|
||||
/// Called whenever this widget starts to scroll.
|
||||
final ScrollListener onScrollStart;
|
||||
|
||||
@@ -172,13 +171,46 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
|
||||
/// Scrollable gesture handlers convert their incoming values with this method.
|
||||
/// Subclasses that define scrollOffset in units other than pixels must
|
||||
/// 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 scrollDirectionVelocity(Offset scrollVelocity) {
|
||||
return config.scrollDirection == Axis.horizontal
|
||||
? -scrollVelocity.dx
|
||||
: -scrollVelocity.dy;
|
||||
double scrollOffsetToPixelOffset(double scrollOffset) {
|
||||
switch (config.scrollAnchor) {
|
||||
case ViewportAnchor.start:
|
||||
return -scrollOffset;
|
||||
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;
|
||||
@@ -244,19 +276,19 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
|
||||
return _controller.animateTo(newScrollOffset, duration: duration, curve: curve);
|
||||
}
|
||||
|
||||
bool _scrollOffsetIsInBounds(double offset) {
|
||||
bool _scrollOffsetIsInBounds(double scrollOffset) {
|
||||
if (scrollBehavior is! ExtentScrollBehavior)
|
||||
return false;
|
||||
ExtentScrollBehavior behavior = scrollBehavior;
|
||||
return offset >= behavior.minScrollOffset && offset < behavior.maxScrollOffset;
|
||||
return scrollOffset >= behavior.minScrollOffset && scrollOffset < behavior.maxScrollOffset;
|
||||
}
|
||||
|
||||
Simulation _createFlingSimulation(double velocity) {
|
||||
final Simulation simulation = scrollBehavior.createFlingScrollSimulation(scrollOffset, velocity);
|
||||
Simulation _createFlingSimulation(double scrollVelocity) {
|
||||
final Simulation simulation = scrollBehavior.createFlingScrollSimulation(scrollOffset, scrollVelocity);
|
||||
if (simulation != null) {
|
||||
final double endVelocity = pixelToScrollOffset(kPixelScrollTolerance.velocity);
|
||||
final double endDistance = pixelToScrollOffset(kPixelScrollTolerance.distance);
|
||||
simulation.tolerance = new Tolerance(velocity: endVelocity.abs(), distance: endDistance);
|
||||
final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity) * scrollVelocity.sign;
|
||||
final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs();
|
||||
simulation.tolerance = new Tolerance(velocity: endVelocity, distance: endDistance);
|
||||
}
|
||||
return simulation;
|
||||
}
|
||||
@@ -267,41 +299,40 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
if (!snapScrollOffsetChanges || velocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
|
||||
Simulation _createSnapSimulation(double scrollVelocity) {
|
||||
if (!shouldSnapScrollOffset || scrollVelocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
|
||||
return null;
|
||||
|
||||
Simulation simulation = _createFlingSimulation(velocity);
|
||||
Simulation simulation = _createFlingSimulation(scrollVelocity);
|
||||
if (simulation == null)
|
||||
return null;
|
||||
|
||||
double endScrollOffset = simulation.x(double.INFINITY);
|
||||
final double endScrollOffset = simulation.x(double.INFINITY);
|
||||
if (endScrollOffset.isNaN)
|
||||
return null;
|
||||
|
||||
double snappedScrollOffset = snapScrollOffset(endScrollOffset + config.snapAlignmentOffset);
|
||||
double alignedScrollOffset = snappedScrollOffset - config.snapAlignmentOffset;
|
||||
final double snappedScrollOffset = snapScrollOffset(endScrollOffset + config.snapAlignmentOffset);
|
||||
final double alignedScrollOffset = snappedScrollOffset - config.snapAlignmentOffset;
|
||||
if (!_scrollOffsetIsInBounds(alignedScrollOffset))
|
||||
return null;
|
||||
|
||||
double snapVelocity = velocity.abs() * (alignedScrollOffset - scrollOffset).sign;
|
||||
double endVelocity = pixelToScrollOffset(kPixelScrollTolerance.velocity * velocity.sign);
|
||||
final double snapVelocity = scrollVelocity.abs() * (alignedScrollOffset - scrollOffset).sign;
|
||||
final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * scrollVelocity.sign;
|
||||
Simulation toSnapSimulation =
|
||||
scrollBehavior.createSnapScrollSimulation(scrollOffset, alignedScrollOffset, snapVelocity, endVelocity);
|
||||
if (toSnapSimulation == null)
|
||||
return null;
|
||||
|
||||
double offsetMin = math.min(scrollOffset, alignedScrollOffset);
|
||||
double offsetMax = math.max(scrollOffset, alignedScrollOffset);
|
||||
return new ClampedSimulation(toSnapSimulation, xMin: offsetMin, xMax: offsetMax);
|
||||
final double scrollOffsetMin = math.min(scrollOffset, alignedScrollOffset);
|
||||
final double scrollOffsetMax = math.max(scrollOffset, alignedScrollOffset);
|
||||
return new ClampedSimulation(toSnapSimulation, xMin: scrollOffsetMin, xMax: scrollOffsetMax);
|
||||
}
|
||||
|
||||
Future _startToEndAnimation(Offset scrollVelocity) {
|
||||
double velocity = scrollDirectionVelocity(scrollVelocity);
|
||||
Future _startToEndAnimation(double scrollVelocity) {
|
||||
_controller.stop();
|
||||
Simulation simulation = _createSnapSimulation(velocity) ?? _createFlingSimulation(velocity);
|
||||
Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
|
||||
if (simulation == null)
|
||||
return new Future.value();
|
||||
return _controller.animateWith(simulation);
|
||||
@@ -358,8 +389,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
|
||||
/// Calling this function starts a physics-based animation of the scroll
|
||||
/// offset with the given value as the initial velocity. The physics
|
||||
/// simulation used is determined by the scroll behavior.
|
||||
Future fling(Offset scrollVelocity) {
|
||||
if (scrollVelocity != Offset.zero)
|
||||
Future fling(double scrollVelocity) {
|
||||
if (scrollVelocity != 0.0)
|
||||
return _startToEndAnimation(scrollVelocity);
|
||||
if (!_controller.isAnimating)
|
||||
return settleScrollOffset();
|
||||
@@ -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
|
||||
/// physics simulation used is determined by the scroll behavior.
|
||||
Future settleScrollOffset() {
|
||||
return _startToEndAnimation(Offset.zero);
|
||||
return _startToEndAnimation(0.0);
|
||||
}
|
||||
|
||||
/// Calls the onScrollStart callback.
|
||||
@@ -408,19 +439,14 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
|
||||
}
|
||||
|
||||
void _handleDragUpdate(double delta) {
|
||||
// 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).
|
||||
scrollBy(pixelToScrollOffset(-delta));
|
||||
scrollBy(pixelOffsetToScrollOffset(delta));
|
||||
}
|
||||
|
||||
double _toScrollVelocity(double velocity) {
|
||||
return pixelToScrollOffset(velocity.clamp(_kMinFlingVelocity, _kMaxFlingVelocity) / _kMillisecondsPerSecond);
|
||||
}
|
||||
|
||||
Future _handleDragEnd(Offset pixelScrollVelocity) {
|
||||
final Offset scrollVelocity = new Offset(_toScrollVelocity(pixelScrollVelocity.dx), _toScrollVelocity(pixelScrollVelocity.dy));
|
||||
return fling(scrollVelocity).then((_) {
|
||||
dispatchOnScrollEnd();
|
||||
Future _handleDragEnd(Offset velocity) {
|
||||
double scrollVelocity = pixelDeltaToScrollOffset(velocity) / Duration.MILLISECONDS_PER_SECOND;
|
||||
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
|
||||
return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then((_) {
|
||||
dispatchOnScrollEnd();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +48,11 @@ void set scrollOffset(double value) {
|
||||
}
|
||||
|
||||
Future fling(double velocity) {
|
||||
Offset velocityOffset = scrollDirection == Axis.vertical
|
||||
? new Offset(0.0, velocity)
|
||||
: new Offset(velocity, 0.0);
|
||||
return scrollableState.fling(velocityOffset);
|
||||
return scrollableState.fling(velocity);
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('ScrollableList snap scrolling, fling(-0.8)', () {
|
||||
test('ScrollableList snap scrolling, fling(0.8)', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
tester.pumpWidget(buildFrame());
|
||||
|
||||
@@ -65,7 +62,7 @@ void main() {
|
||||
|
||||
Duration dt = const Duration(seconds: 2);
|
||||
|
||||
fling(-0.8);
|
||||
fling(0.8);
|
||||
tester.pump(); // Start the scheduler at 0.0
|
||||
tester.pump(dt);
|
||||
expect(scrollOffset, closeTo(200.0, 1.0));
|
||||
@@ -74,7 +71,7 @@ void main() {
|
||||
tester.pump();
|
||||
expect(scrollOffset, 0.0);
|
||||
|
||||
fling(-2.0);
|
||||
fling(2.0);
|
||||
tester.pump();
|
||||
tester.pump(dt);
|
||||
expect(scrollOffset, closeTo(400.0, 1.0));
|
||||
@@ -83,7 +80,7 @@ void main() {
|
||||
tester.pump();
|
||||
expect(scrollOffset, 400.0);
|
||||
|
||||
fling(0.8);
|
||||
fling(-0.8);
|
||||
tester.pump();
|
||||
tester.pump(dt);
|
||||
expect(scrollOffset, closeTo(0.0, 1.0));
|
||||
@@ -92,7 +89,7 @@ void main() {
|
||||
tester.pump();
|
||||
expect(scrollOffset, 800.0);
|
||||
|
||||
fling(2.0);
|
||||
fling(-2.0);
|
||||
tester.pump();
|
||||
tester.pump(dt);
|
||||
expect(scrollOffset, closeTo(200.0, 1.0));
|
||||
@@ -102,7 +99,7 @@ void main() {
|
||||
expect(scrollOffset, 800.0);
|
||||
|
||||
bool completed = false;
|
||||
fling(2.0).then((_) {
|
||||
fling(-2.0).then((_) {
|
||||
completed = true;
|
||||
expect(scrollOffset, closeTo(200.0, 1.0));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user