Skip to content

Commit 421929e

Browse files
Use Ozzie's new panning friction simulation (Resolves #6) (#17)
1 parent 60af1b5 commit 421929e

File tree

5 files changed

+69
-375
lines changed

5 files changed

+69
-375
lines changed

.run/Page List - Old Velocity (debug).run.xml

Lines changed: 0 additions & 6 deletions
This file was deleted.

example/lib/main_list_old_velocity.dart

Lines changed: 0 additions & 98 deletions
This file was deleted.

lib/page_list_viewport.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ export 'src/page_list_viewport.dart';
44
export 'src/page_list_performance_optimizer.dart';
55
export 'src/logging.dart';
66

7-
export 'src/deprecated_gestures.dart';
7+
export 'src/page_list_viewport_gestures.dart';

lib/src/page_list_viewport.dart

Lines changed: 0 additions & 253 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,259 +1042,6 @@ class ViewportPageParentData extends ContainerBoxParentData<RenderBox> with Cont
10421042
}
10431043
}
10441044

1045-
/// Controls a [PageListViewportController] with scale gestures to pan and zoom the
1046-
/// associated [PageListViewport].
1047-
class PageListViewportGestures extends StatefulWidget {
1048-
const PageListViewportGestures({
1049-
Key? key,
1050-
required this.controller,
1051-
this.onTapUp,
1052-
this.onLongPressStart,
1053-
this.onLongPressMoveUpdate,
1054-
this.onLongPressEnd,
1055-
this.onDoubleTapDown,
1056-
this.onDoubleTap,
1057-
this.onDoubleTapCancel,
1058-
this.panAndZoomPointerDevices = const {
1059-
PointerDeviceKind.mouse,
1060-
PointerDeviceKind.trackpad,
1061-
PointerDeviceKind.touch,
1062-
},
1063-
this.clock = const Clock(),
1064-
required this.child,
1065-
}) : super(key: key);
1066-
1067-
final PageListViewportController controller;
1068-
1069-
// All of these methods were added because our client needs to
1070-
// respond to them, and we internally respond to other gestures.
1071-
// Flutter won't let gestures pass from parent to child, so we're
1072-
// forced to expose all of these callbacks so that our client can
1073-
// hook into them.
1074-
final void Function(TapUpDetails)? onTapUp;
1075-
final void Function(LongPressStartDetails)? onLongPressStart;
1076-
final void Function(LongPressMoveUpdateDetails)? onLongPressMoveUpdate;
1077-
final void Function(LongPressEndDetails)? onLongPressEnd;
1078-
final void Function(TapDownDetails)? onDoubleTapDown;
1079-
final void Function()? onDoubleTap;
1080-
final void Function()? onDoubleTapCancel;
1081-
1082-
final Set<PointerDeviceKind> panAndZoomPointerDevices;
1083-
1084-
/// Reports the time, so that the gesture system can track how much
1085-
/// time has passed.
1086-
///
1087-
/// [clock] is configurable so that a fake version can be injected
1088-
/// in tests.
1089-
final Clock clock;
1090-
1091-
final Widget child;
1092-
1093-
@override
1094-
State<PageListViewportGestures> createState() => _PageListViewportGesturesState();
1095-
}
1096-
1097-
class _PageListViewportGesturesState extends State<PageListViewportGestures> with TickerProviderStateMixin {
1098-
bool _isPanningEnabled = true;
1099-
bool _isPanning = false;
1100-
1101-
late PanAndScaleVelocityTracker _panAndScaleVelocityTracker;
1102-
double? _startContentScale;
1103-
Offset? _startOffset;
1104-
int? _endTimeInMillis;
1105-
late Ticker _ticker;
1106-
1107-
@override
1108-
void initState() {
1109-
super.initState();
1110-
_panAndScaleVelocityTracker = PanAndScaleVelocityTracker(clock: widget.clock);
1111-
_ticker = createTicker(_onFrictionTick);
1112-
}
1113-
1114-
@override
1115-
void dispose() {
1116-
_ticker.dispose();
1117-
super.dispose();
1118-
}
1119-
1120-
void _onPointerDown(PointerDownEvent event) {
1121-
if (event.kind == PointerDeviceKind.stylus) {
1122-
_isPanningEnabled = false;
1123-
}
1124-
1125-
// Stop any on-going friction simulation.
1126-
_stopMomentum();
1127-
}
1128-
1129-
void _onPointerUp(PointerUpEvent event) {
1130-
_isPanningEnabled = true;
1131-
}
1132-
1133-
void _onPointerCancel(PointerCancelEvent event) {
1134-
_isPanningEnabled = true;
1135-
}
1136-
1137-
void _onScaleStart(ScaleStartDetails details) {
1138-
PageListViewportLogs.pagesListGestures.finer("onScaleStart()");
1139-
if (!_isPanningEnabled) {
1140-
// The user is interacting with a stylus. We don't want to pan
1141-
// or scale with a stylus.
1142-
return;
1143-
}
1144-
1145-
_isPanning = true;
1146-
1147-
final timeSinceLastGesture = _endTimeInMillis != null ? _timeSinceEndOfLastGesture : null;
1148-
_startContentScale = widget.controller.scale;
1149-
_startOffset = widget.controller.origin;
1150-
1151-
_panAndScaleVelocityTracker.onScaleStart(details);
1152-
1153-
if ((timeSinceLastGesture == null || timeSinceLastGesture > const Duration(milliseconds: 30))) {
1154-
// We've started a new gesture after a reasonable period of time since the
1155-
// last gesture. Stop any momentum from the last gesture.
1156-
_stopMomentum();
1157-
}
1158-
}
1159-
1160-
void _onScaleUpdate(ScaleUpdateDetails details) {
1161-
PageListViewportLogs.pagesList
1162-
.finer("onScaleUpdate() - new focal point ${details.focalPoint}, focal delta: ${details.focalPointDelta}");
1163-
if (!_isPanning) {
1164-
// The user is interacting with a stylus. We don't want to pan
1165-
// or scale with a stylus.
1166-
return;
1167-
}
1168-
1169-
if (!_isPanningEnabled) {
1170-
PageListViewportLogs.pagesListGestures.finer("Started panning when the stylus was down. Resetting transform to:");
1171-
PageListViewportLogs.pagesListGestures.finer(" - origin: ${widget.controller.origin}");
1172-
PageListViewportLogs.pagesListGestures.finer(" - scale: ${widget.controller.scale}");
1173-
1174-
_isPanning = false;
1175-
1176-
// When this condition is triggered, _startOffset and _startContentScale
1177-
// should be non-null. But sometimes they are null. I don't know why. When that
1178-
// happens, return.
1179-
if (_startOffset == null || _startContentScale == null) {
1180-
return;
1181-
}
1182-
1183-
widget.controller
1184-
..setScale(_startContentScale!, details.focalPoint)
1185-
..translate(_startOffset! - widget.controller.origin);
1186-
return;
1187-
}
1188-
1189-
_panAndScaleVelocityTracker.onScaleUpdate(details);
1190-
1191-
widget.controller //
1192-
..setScale(details.scale * _startContentScale!, details.localFocalPoint)
1193-
..translate(details.focalPointDelta);
1194-
PageListViewportLogs.pagesListGestures
1195-
.finer("New origin: ${widget.controller.origin}, scale: ${widget.controller.scale}");
1196-
}
1197-
1198-
void _onScaleEnd(ScaleEndDetails details) {
1199-
PageListViewportLogs.pagesListGestures.finer("onScaleEnd()");
1200-
if (!_isPanning) {
1201-
return;
1202-
}
1203-
1204-
_panAndScaleVelocityTracker.onScaleEnd(details);
1205-
1206-
if (details.pointerCount == 0) {
1207-
_startMomentum();
1208-
_isPanning = false;
1209-
}
1210-
}
1211-
1212-
Duration get _timeSinceEndOfLastGesture => Duration(milliseconds: widget.clock.millis - _endTimeInMillis!);
1213-
1214-
void _startMomentum() {
1215-
PageListViewportLogs.pagesListGestures.fine("Starting momentum...");
1216-
final velocity = _panAndScaleVelocityTracker.velocity;
1217-
PageListViewportLogs.pagesListGestures.fine("Starting momentum with velocity: $velocity");
1218-
1219-
_velocity = velocity;
1220-
1221-
if (!_ticker.isTicking) {
1222-
_lastTime = Duration.zero;
1223-
_ticker.start();
1224-
}
1225-
}
1226-
1227-
void _stopMomentum() {
1228-
if (_ticker.isTicking) {
1229-
_ticker.stop();
1230-
}
1231-
}
1232-
1233-
Duration _lastTime = Duration.zero;
1234-
Offset _velocity = Offset.zero;
1235-
void _onFrictionTick(Duration elapsedTime) {
1236-
if (elapsedTime == Duration.zero) {
1237-
return;
1238-
}
1239-
1240-
final dt = elapsedTime - _lastTime;
1241-
_lastTime = elapsedTime;
1242-
final secondsFraction = dt.inMilliseconds / 1000;
1243-
final dp = _velocity * secondsFraction;
1244-
final originBeforeDelta = widget.controller.origin;
1245-
final newOrigin = widget.controller.origin + dp;
1246-
final translate = newOrigin - originBeforeDelta;
1247-
1248-
final direction = Offset.fromDirection(_velocity.direction);
1249-
1250-
// We want:
1251-
// - a decay of 3% when moving very fast (less drag when flinging fast)
1252-
// - a decay of 6% when moving slowly (more drag when coming to rest)
1253-
final velocityIntensity = math.pow((_velocity.distance / kMaxFlingVelocity).clamp(0.0, 1.0), 1 / 4.0).toDouble();
1254-
final drag = lerpDouble(0.06, 0.03, velocityIntensity)!;
1255-
_velocity = _velocity - (direction * _velocity.distance * drag);
1256-
1257-
PageListViewportLogs.pagesListGestures
1258-
.finest("Friction tick. Time: ${elapsedTime.inMilliseconds}ms. Velocity: $_velocity. Movement: $translate");
1259-
1260-
widget.controller.translate(translate);
1261-
1262-
PageListViewportLogs.pagesListGestures.finest("New origin: $newOrigin");
1263-
1264-
// If the viewport hit a wall, or if the simulations are done, stop
1265-
// ticking.
1266-
if (translate.distance < 0.5) {
1267-
_ticker.stop();
1268-
}
1269-
}
1270-
1271-
@override
1272-
Widget build(BuildContext context) {
1273-
return Listener(
1274-
// Listen for finger-down in a Listener so that we have zero
1275-
// latency when stopping a friction simulation. Also, track when
1276-
// a stylus is used, so we can prevent panning.
1277-
onPointerDown: _onPointerDown,
1278-
onPointerUp: _onPointerUp,
1279-
onPointerCancel: _onPointerCancel,
1280-
child: GestureDetector(
1281-
onTapUp: widget.onTapUp,
1282-
onLongPressStart: widget.onLongPressStart,
1283-
onLongPressMoveUpdate: widget.onLongPressMoveUpdate,
1284-
onLongPressEnd: widget.onLongPressEnd,
1285-
onDoubleTapDown: widget.onDoubleTapDown,
1286-
onDoubleTap: widget.onDoubleTap,
1287-
onDoubleTapCancel: widget.onDoubleTapCancel,
1288-
onScaleStart: _onScaleStart,
1289-
onScaleUpdate: _onScaleUpdate,
1290-
onScaleEnd: _onScaleEnd,
1291-
supportedDevices: widget.panAndZoomPointerDevices,
1292-
child: widget.child,
1293-
),
1294-
);
1295-
}
1296-
}
1297-
12981045
/// Tracks scale gesture events and calculates a velocity based on those
12991046
/// events.
13001047
///

0 commit comments

Comments
 (0)