Modularize ReorderableListView auto scrolling logic (#96563)
* Modularize ReorderableListView auto scrolling logic * comment * fix test * addressing comment
This commit is contained in:
parent
c13d4417a8
commit
3bc57c0090
@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:math';
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
@ -16,7 +16,6 @@ import 'media_query.dart';
|
|||||||
import 'overlay.dart';
|
import 'overlay.dart';
|
||||||
import 'scroll_controller.dart';
|
import 'scroll_controller.dart';
|
||||||
import 'scroll_physics.dart';
|
import 'scroll_physics.dart';
|
||||||
import 'scroll_position.dart';
|
|
||||||
import 'scroll_view.dart';
|
import 'scroll_view.dart';
|
||||||
import 'scrollable.dart';
|
import 'scrollable.dart';
|
||||||
import 'sliver.dart';
|
import 'sliver.dart';
|
||||||
@ -523,7 +522,6 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
Offset? _finalDropPosition;
|
Offset? _finalDropPosition;
|
||||||
MultiDragGestureRecognizer? _recognizer;
|
MultiDragGestureRecognizer? _recognizer;
|
||||||
int? _recognizerPointer;
|
int? _recognizerPointer;
|
||||||
bool _autoScrolling = false;
|
|
||||||
// To implement the gap for the dragged item, we replace the dragged item
|
// To implement the gap for the dragged item, we replace the dragged item
|
||||||
// with a zero sized box, and then translate all of the later items down
|
// with a zero sized box, and then translate all of the later items down
|
||||||
// by the size of the dragged item. This allows us to keep the order of the
|
// by the size of the dragged item. This allows us to keep the order of the
|
||||||
@ -534,6 +532,8 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
// so the gap calculation can compensate for it.
|
// so the gap calculation can compensate for it.
|
||||||
bool _dragStartTransitionComplete = false;
|
bool _dragStartTransitionComplete = false;
|
||||||
|
|
||||||
|
_EdgeDraggingAutoScroller? _autoScroller;
|
||||||
|
|
||||||
late ScrollableState _scrollable;
|
late ScrollableState _scrollable;
|
||||||
Axis get _scrollDirection => axisDirectionToAxis(_scrollable.axisDirection);
|
Axis get _scrollDirection => axisDirectionToAxis(_scrollable.axisDirection);
|
||||||
bool get _reverse =>
|
bool get _reverse =>
|
||||||
@ -544,6 +544,13 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
_scrollable = Scrollable.of(context)!;
|
_scrollable = Scrollable.of(context)!;
|
||||||
|
if (_autoScroller?.scrollable != _scrollable) {
|
||||||
|
_autoScroller?.stopAutoScroll();
|
||||||
|
_autoScroller = _EdgeDraggingAutoScroller(
|
||||||
|
_scrollable,
|
||||||
|
onScrollViewScrolled: _handleScrollableAutoScrolled
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -557,6 +564,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_dragInfo?.dispose();
|
_dragInfo?.dispose();
|
||||||
|
_autoScroller?.stopAutoScroll();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -669,7 +677,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
setState(() {
|
setState(() {
|
||||||
_overlayEntry?.markNeedsBuild();
|
_overlayEntry?.markNeedsBuild();
|
||||||
_dragUpdateItems();
|
_dragUpdateItems();
|
||||||
_autoScrollIfNecessary();
|
_autoScroller?.startAutoScrollIfNecessary(_dragTargetRect);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -716,6 +724,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
}
|
}
|
||||||
_dragInfo?.dispose();
|
_dragInfo?.dispose();
|
||||||
_dragInfo = null;
|
_dragInfo = null;
|
||||||
|
_autoScroller?.stopAutoScroll();
|
||||||
_resetItemGap();
|
_resetItemGap();
|
||||||
_recognizer?.dispose();
|
_recognizer?.dispose();
|
||||||
_recognizer = null;
|
_recognizer = null;
|
||||||
@ -732,6 +741,14 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleScrollableAutoScrolled() {
|
||||||
|
if (_dragInfo == null)
|
||||||
|
return;
|
||||||
|
_dragUpdateItems();
|
||||||
|
// Continue scrolling if the drag is still in progress.
|
||||||
|
_autoScroller?.startAutoScrollIfNecessary(_dragTargetRect);
|
||||||
|
}
|
||||||
|
|
||||||
void _dragUpdateItems() {
|
void _dragUpdateItems() {
|
||||||
assert(_dragInfo != null);
|
assert(_dragInfo != null);
|
||||||
final double gapExtent = _dragInfo!.itemExtent;
|
final double gapExtent = _dragInfo!.itemExtent;
|
||||||
@ -810,55 +827,9 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _autoScrollIfNecessary() async {
|
Rect get _dragTargetRect {
|
||||||
if (!_autoScrolling && _dragInfo != null && _dragInfo!.scrollable != null) {
|
final Offset origin = _dragInfo!.dragPosition - _dragInfo!.dragOffset;
|
||||||
final ScrollPosition position = _dragInfo!.scrollable!.position;
|
return Rect.fromLTWH(origin.dx, origin.dy, _dragInfo!.itemSize.width, _dragInfo!.itemSize.height);
|
||||||
double? newOffset;
|
|
||||||
const Duration duration = Duration(milliseconds: 14);
|
|
||||||
const double step = 1.0;
|
|
||||||
const double overDragMax = 20.0;
|
|
||||||
const double overDragCoef = 10;
|
|
||||||
|
|
||||||
final RenderBox scrollRenderBox = _dragInfo!.scrollable!.context.findRenderObject()! as RenderBox;
|
|
||||||
final Offset scrollOrigin = scrollRenderBox.localToGlobal(Offset.zero);
|
|
||||||
final double scrollStart = _offsetExtent(scrollOrigin, _scrollDirection);
|
|
||||||
final double scrollEnd = scrollStart + _sizeExtent(scrollRenderBox.size, _scrollDirection);
|
|
||||||
|
|
||||||
final double proxyStart = _offsetExtent(_dragInfo!.dragPosition - _dragInfo!.dragOffset, _scrollDirection);
|
|
||||||
final double proxyEnd = proxyStart + _dragInfo!.itemExtent;
|
|
||||||
|
|
||||||
if (_reverse) {
|
|
||||||
if (proxyEnd > scrollEnd && position.pixels > position.minScrollExtent) {
|
|
||||||
final double overDrag = max(proxyEnd - scrollEnd, overDragMax);
|
|
||||||
newOffset = max(position.minScrollExtent, position.pixels - step * overDrag / overDragCoef);
|
|
||||||
} else if (proxyStart < scrollStart && position.pixels < position.maxScrollExtent) {
|
|
||||||
final double overDrag = max(scrollStart - proxyStart, overDragMax);
|
|
||||||
newOffset = min(position.maxScrollExtent, position.pixels + step * overDrag / overDragCoef);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (proxyStart < scrollStart && position.pixels > position.minScrollExtent) {
|
|
||||||
final double overDrag = max(scrollStart - proxyStart, overDragMax);
|
|
||||||
newOffset = max(position.minScrollExtent, position.pixels - step * overDrag / overDragCoef);
|
|
||||||
} else if (proxyEnd > scrollEnd && position.pixels < position.maxScrollExtent) {
|
|
||||||
final double overDrag = max(proxyEnd - scrollEnd, overDragMax);
|
|
||||||
newOffset = min(position.maxScrollExtent, position.pixels + step * overDrag / overDragCoef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newOffset != null && (newOffset - position.pixels).abs() >= 1.0) {
|
|
||||||
_autoScrolling = true;
|
|
||||||
await position.animateTo(
|
|
||||||
newOffset,
|
|
||||||
duration: duration,
|
|
||||||
curve: Curves.linear,
|
|
||||||
);
|
|
||||||
_autoScrolling = false;
|
|
||||||
if (_dragInfo != null) {
|
|
||||||
_dragUpdateItems();
|
|
||||||
_autoScrollIfNecessary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Offset _itemOffsetAt(int index) {
|
Offset _itemOffsetAt(int index) {
|
||||||
@ -911,6 +882,152 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An auto scroller that scrolls the [scrollable] if a drag gesture drag close
|
||||||
|
/// to its edge.
|
||||||
|
///
|
||||||
|
/// The scroll velocity is controlled by the [velocityScalar]:
|
||||||
|
///
|
||||||
|
/// velocity = <distance of overscroll> * [velocityScalar].
|
||||||
|
class _EdgeDraggingAutoScroller {
|
||||||
|
/// Creates a auto scroller that scrolls the [scrollable].
|
||||||
|
_EdgeDraggingAutoScroller(this.scrollable, {this.onScrollViewScrolled, this.velocityScalar = _kDefaultAutoScrollVelocityScalar});
|
||||||
|
|
||||||
|
// An eyeball value
|
||||||
|
static const double _kDefaultAutoScrollVelocityScalar = 7;
|
||||||
|
|
||||||
|
/// The [Scrollable] this auto scroller is scrolling.
|
||||||
|
final ScrollableState scrollable;
|
||||||
|
|
||||||
|
/// Called when a scroll view is scrolled.
|
||||||
|
///
|
||||||
|
/// The scroll view may be scrolled multiple times in a roll until the drag
|
||||||
|
/// target no longer triggers the auto scroll. This callback will be called
|
||||||
|
/// in between each scroll.
|
||||||
|
final VoidCallback? onScrollViewScrolled;
|
||||||
|
|
||||||
|
/// The velocity scalar per pixel over scroll.
|
||||||
|
///
|
||||||
|
/// How the velocity scale with the over scroll distance. The auto scroll
|
||||||
|
/// velocity = <distance of overscroll> * velocityScalar.
|
||||||
|
final double velocityScalar;
|
||||||
|
|
||||||
|
late Rect _dragTargetRelatedToScrollOrigin;
|
||||||
|
|
||||||
|
/// Whether the auto scroll is in progress.
|
||||||
|
bool get scrolling => _scrolling;
|
||||||
|
bool _scrolling = false;
|
||||||
|
|
||||||
|
double _offsetExtent(Offset offset, Axis scrollDirection) {
|
||||||
|
switch (scrollDirection) {
|
||||||
|
case Axis.horizontal:
|
||||||
|
return offset.dx;
|
||||||
|
case Axis.vertical:
|
||||||
|
return offset.dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double _sizeExtent(Size size, Axis scrollDirection) {
|
||||||
|
switch (scrollDirection) {
|
||||||
|
case Axis.horizontal:
|
||||||
|
return size.width;
|
||||||
|
case Axis.vertical:
|
||||||
|
return size.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AxisDirection get _axisDirection => scrollable.axisDirection;
|
||||||
|
Axis get _scrollDirection => axisDirectionToAxis(_axisDirection);
|
||||||
|
|
||||||
|
/// Starts the auto scroll if the [dragTarget] is close to the edge.
|
||||||
|
///
|
||||||
|
/// The scroll starts to scroll the [scrollable] if the target rect is close
|
||||||
|
/// to the edge of the [scrollable]; otherwise, it remains stationary.
|
||||||
|
///
|
||||||
|
/// If the scrollable is already scrolling, calling this method updates the
|
||||||
|
/// previous dragTarget to the new value and continue scrolling if necessary.
|
||||||
|
void startAutoScrollIfNecessary(Rect dragTarget) {
|
||||||
|
final Offset deltaToOrigin = _getDeltaToScrollOrigin(scrollable);
|
||||||
|
_dragTargetRelatedToScrollOrigin = dragTarget.translate(deltaToOrigin.dx, deltaToOrigin.dy);
|
||||||
|
if (_scrolling) {
|
||||||
|
// The change will be picked up in the next scroll.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_scrolling)
|
||||||
|
_scroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop any ongoing auto scrolling.
|
||||||
|
void stopAutoScroll() {
|
||||||
|
_scrolling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _scroll() async {
|
||||||
|
final RenderBox scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox;
|
||||||
|
final Rect globalRect = MatrixUtils.transformRect(
|
||||||
|
scrollRenderBox.getTransformTo(null),
|
||||||
|
Rect.fromLTWH(0, 0, scrollRenderBox.size.width, scrollRenderBox.size.height)
|
||||||
|
);
|
||||||
|
_scrolling = true;
|
||||||
|
double? newOffset;
|
||||||
|
const double overDragMax = 20.0;
|
||||||
|
|
||||||
|
final Offset deltaToOrigin = _getDeltaToScrollOrigin(scrollable);
|
||||||
|
final Offset viewportOrigin = globalRect.topLeft.translate(deltaToOrigin.dx, deltaToOrigin.dy);
|
||||||
|
final double viewportStart = _offsetExtent(viewportOrigin, _scrollDirection);
|
||||||
|
final double viewportEnd = viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
|
||||||
|
|
||||||
|
final double proxyStart = _offsetExtent(_dragTargetRelatedToScrollOrigin.topLeft, _scrollDirection);
|
||||||
|
final double proxyEnd = _offsetExtent(_dragTargetRelatedToScrollOrigin.bottomRight, _scrollDirection);
|
||||||
|
late double overDrag;
|
||||||
|
if (_axisDirection == AxisDirection.up || _axisDirection == AxisDirection.left) {
|
||||||
|
if (proxyEnd > viewportEnd && scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||||
|
overDrag = math.max(proxyEnd - viewportEnd, overDragMax);
|
||||||
|
newOffset = math.max(scrollable.position.minScrollExtent, scrollable.position.pixels - overDrag);
|
||||||
|
} else if (proxyStart < viewportStart && scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||||
|
overDrag = math.max(viewportStart - proxyStart, overDragMax);
|
||||||
|
newOffset = math.min(scrollable.position.maxScrollExtent, scrollable.position.pixels + overDrag);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (proxyStart < viewportStart && scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||||
|
overDrag = math.max(viewportStart - proxyStart, overDragMax);
|
||||||
|
newOffset = math.max(scrollable.position.minScrollExtent, scrollable.position.pixels - overDrag);
|
||||||
|
} else if (proxyEnd > viewportEnd && scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||||
|
overDrag = math.max(proxyEnd - viewportEnd, overDragMax);
|
||||||
|
newOffset = math.min(scrollable.position.maxScrollExtent, scrollable.position.pixels + overDrag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newOffset == null || (newOffset - scrollable.position.pixels).abs() < 1.0) {
|
||||||
|
// Drag should not trigger scroll.
|
||||||
|
_scrolling = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Duration duration = Duration(milliseconds: (1000 / velocityScalar).round());
|
||||||
|
await scrollable.position.animateTo(
|
||||||
|
newOffset,
|
||||||
|
duration: duration,
|
||||||
|
curve: Curves.linear,
|
||||||
|
);
|
||||||
|
if (onScrollViewScrolled != null)
|
||||||
|
onScrollViewScrolled!();
|
||||||
|
if (_scrolling)
|
||||||
|
await _scroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) {
|
||||||
|
switch (scrollableState.axisDirection) {
|
||||||
|
case AxisDirection.down:
|
||||||
|
return Offset(0, scrollableState.position.pixels);
|
||||||
|
case AxisDirection.up:
|
||||||
|
return Offset(0, -scrollableState.position.pixels);
|
||||||
|
case AxisDirection.left:
|
||||||
|
return Offset(-scrollableState.position.pixels, 0);
|
||||||
|
case AxisDirection.right:
|
||||||
|
return Offset(scrollableState.position.pixels, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ReorderableItem extends StatefulWidget {
|
class _ReorderableItem extends StatefulWidget {
|
||||||
const _ReorderableItem({
|
const _ReorderableItem({
|
||||||
required Key key,
|
required Key key,
|
||||||
@ -1116,9 +1233,9 @@ class ReorderableDragStartListener extends StatelessWidget {
|
|||||||
void _startDragging(BuildContext context, PointerDownEvent event) {
|
void _startDragging(BuildContext context, PointerDownEvent event) {
|
||||||
final SliverReorderableListState? list = SliverReorderableList.maybeOf(context);
|
final SliverReorderableListState? list = SliverReorderableList.maybeOf(context);
|
||||||
list?.startItemDragReorder(
|
list?.startItemDragReorder(
|
||||||
index: index,
|
index: index,
|
||||||
event: event,
|
event: event,
|
||||||
recognizer: createRecognizer(),
|
recognizer: createRecognizer(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user