fix: optimized swiping and address bar toggle on tap
This commit is contained in:
parent
ac998b394d
commit
0e2027c9e2
5 changed files with 466 additions and 78 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/gestures.dart'
|
||||
show kLongPressTimeout, kTouchSlop, PointerDeviceKind;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Wraps the current page and turns the back/forward swipes into visible page
|
||||
|
|
@ -22,6 +24,7 @@ class SwipeNavigator extends StatefulWidget {
|
|||
this.backTitle,
|
||||
this.forwardTitle,
|
||||
this.contentWidth,
|
||||
this.swipesAnywhere = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -41,6 +44,15 @@ class SwipeNavigator extends StatefulWidget {
|
|||
/// the base edge zone.
|
||||
final double? contentWidth;
|
||||
|
||||
/// Whether the page itself answers horizontal drags, not only the strips.
|
||||
/// A touch host has no margin beside the column to swipe in, so there the
|
||||
/// whole surface navigates: the finger is tracked outside the gesture
|
||||
/// arena, because a selectable page's own recognizer wins every
|
||||
/// horizontal drag over its text there. A swipe crosses the slop sideways
|
||||
/// before the long press that starts a selection completes, so the two
|
||||
/// never compete for the same finger.
|
||||
final bool swipesAnywhere;
|
||||
|
||||
final Color background;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onForward;
|
||||
|
|
@ -71,6 +83,14 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
bool _dragging = false;
|
||||
double _width = 0;
|
||||
|
||||
// The raw touch swipe's finger, tracked outside the gesture arena.
|
||||
int? _trackedPointer;
|
||||
Offset _downPosition = Offset.zero;
|
||||
Duration _downTime = Duration.zero;
|
||||
Offset _lastPosition = Offset.zero;
|
||||
Duration _lastMoveTime = Duration.zero;
|
||||
double _swipeVelocity = 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_settle.dispose();
|
||||
|
|
@ -99,12 +119,15 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
setState(() => _drag += details.delta.dx);
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details) {
|
||||
void _onDragEnd(DragEndDetails details) =>
|
||||
_commitSwipe(details.primaryVelocity ?? 0);
|
||||
|
||||
void _commitSwipe(double velocity) {
|
||||
if (!_dragging) return;
|
||||
final dx = _displayed;
|
||||
final velocity = details.primaryVelocity ?? 0;
|
||||
final goBack = widget.canGoBack && (dx > _width * 0.3 || velocity > 500);
|
||||
final goForward = widget.canGoForward && (dx < -_width * 0.3 || velocity < -500);
|
||||
final goForward =
|
||||
widget.canGoForward && (dx < -_width * 0.3 || velocity < -500);
|
||||
|
||||
_animateTo(
|
||||
goBack
|
||||
|
|
@ -119,6 +142,71 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
);
|
||||
}
|
||||
|
||||
// Raw touch tracking ------------------------------------------------------
|
||||
|
||||
void _onPagePointerDown(PointerDownEvent event) {
|
||||
if (_trackedPointer != null || _dragging || _settledAnimating) return;
|
||||
// A mouse drives a pointer host's strips, not this listener.
|
||||
if (event.kind == PointerDeviceKind.mouse) return;
|
||||
_trackedPointer = event.pointer;
|
||||
_downPosition = event.position;
|
||||
_downTime = event.timeStamp;
|
||||
_lastPosition = event.position;
|
||||
_lastMoveTime = event.timeStamp;
|
||||
_swipeVelocity = 0;
|
||||
}
|
||||
|
||||
void _onPagePointerMove(PointerMoveEvent event) {
|
||||
if (_trackedPointer != event.pointer) return;
|
||||
final total = event.position - _downPosition;
|
||||
|
||||
if (!_dragging) {
|
||||
if (total.distance <= kTouchSlop) {
|
||||
_lastPosition = event.position;
|
||||
_lastMoveTime = event.timeStamp;
|
||||
return;
|
||||
}
|
||||
// The gate between a swipe and a selection: a finger that crosses the
|
||||
// slop sideways, before the long press that starts a selection
|
||||
// completes, is swiping. One that held still that long, or traveled
|
||||
// along the page first, belongs to text selection or to scrolling.
|
||||
final stillInSwipeWindow =
|
||||
event.timeStamp - _downTime < kLongPressTimeout;
|
||||
final sideways = total.dx.abs() > total.dy.abs();
|
||||
if (!stillInSwipeWindow || !sideways) {
|
||||
_trackedPointer = null;
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
_drag = total.dx;
|
||||
});
|
||||
_lastPosition = event.position;
|
||||
_lastMoveTime = event.timeStamp;
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _drag += event.position.dx - _lastPosition.dx);
|
||||
final elapsed = event.timeStamp - _lastMoveTime;
|
||||
if (elapsed > Duration.zero) {
|
||||
_swipeVelocity =
|
||||
(event.position.dx - _lastPosition.dx) * 1e6 / elapsed.inMicroseconds;
|
||||
}
|
||||
_lastPosition = event.position;
|
||||
_lastMoveTime = event.timeStamp;
|
||||
}
|
||||
|
||||
void _finishPagePointer(PointerEvent event, {required bool commit}) {
|
||||
if (_trackedPointer != event.pointer) return;
|
||||
_trackedPointer = null;
|
||||
if (!_dragging) return;
|
||||
if (commit) {
|
||||
_commitSwipe(_swipeVelocity);
|
||||
} else {
|
||||
_animateTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// The page offset shown: full travel when a page waits in that direction,
|
||||
/// a rubber band when none does, capped near the screen width.
|
||||
double get _displayed {
|
||||
|
|
@ -182,13 +270,36 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
if (peek != null) Positioned.fill(child: peek),
|
||||
Transform.translate(
|
||||
offset: Offset(dx, 0),
|
||||
child: widget.child,
|
||||
// Opaque, so a drag over a blank stretch of an error page
|
||||
// navigates too; the widgets inside still take their own taps,
|
||||
// scrolls and long presses first.
|
||||
// The raw listener owns the touch swipe: a selectable page's
|
||||
// own recognizer wins every horizontal drag over its text in
|
||||
// the arena, so the finger is tracked outside it. Opaque, so
|
||||
// a drag over a blank stretch of an error page navigates too;
|
||||
// the widgets inside keep their own taps, scrolls and long
|
||||
// presses.
|
||||
child: widget.swipesAnywhere
|
||||
? Listener(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPointerDown: _onPagePointerDown,
|
||||
onPointerMove: _onPagePointerMove,
|
||||
onPointerUp: (event) => _finishPagePointer(event, commit: true),
|
||||
onPointerCancel: (event) =>
|
||||
_finishPagePointer(event, commit: false),
|
||||
child: widget.child,
|
||||
)
|
||||
: widget.child,
|
||||
),
|
||||
// Horizontal drags in the strips navigate; elsewhere they select
|
||||
// text. The strips are translucent, so taps, vertical scrolling
|
||||
// and mid-page selection still pass through everywhere.
|
||||
_edgeStrip(fromLeft: true, width: _stripWidth(_width)),
|
||||
_edgeStrip(fromLeft: false, width: _stripWidth(_width)),
|
||||
// and mid-page selection still pass through everywhere. A touch
|
||||
// host navigates with the raw listener instead; strips there
|
||||
// would feed the same drag twice.
|
||||
if (!widget.swipesAnywhere) ...[
|
||||
_edgeStrip(fromLeft: true, width: _stripWidth(_width)),
|
||||
_edgeStrip(fromLeft: false, width: _stripWidth(_width)),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue