feat: accept edge swipes across the whole margin beside the text column
This commit is contained in:
parent
56f3d959fa
commit
6f9c0e587a
3 changed files with 70 additions and 9 deletions
|
|
@ -97,6 +97,11 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
dark: Theme.of(context).brightness == Brightness.dark,
|
||||
).background;
|
||||
|
||||
// The 40em column plus its padding: swipes are accepted
|
||||
// anywhere in the margin beside it.
|
||||
final contentWidth =
|
||||
42 * 17.0 * deps.settings.fontScale.value;
|
||||
|
||||
return SwipeNavigator(
|
||||
canGoBack: browser.canGoBack,
|
||||
canGoForward: browser.canGoForward,
|
||||
|
|
@ -105,6 +110,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
backTitle: browser.backPage.value?.title,
|
||||
forwardTitle: browser.forwardPage.value?.title,
|
||||
background: peekBackground,
|
||||
contentWidth: page == null ? null : contentWidth,
|
||||
onBack: browser.back,
|
||||
onForward: browser.forward,
|
||||
child: Stack(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class SwipeNavigator extends StatefulWidget {
|
|||
this.forwardView,
|
||||
this.backTitle,
|
||||
this.forwardTitle,
|
||||
this.contentWidth,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -35,6 +36,11 @@ class SwipeNavigator extends StatefulWidget {
|
|||
final String? backTitle;
|
||||
final String? forwardTitle;
|
||||
|
||||
/// Width of the content column; swipes are accepted anywhere in the empty
|
||||
/// margin beside it, plus a base zone at each screen edge. Null keeps only
|
||||
/// the base edge zone.
|
||||
final double? contentWidth;
|
||||
|
||||
final Color background;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onForward;
|
||||
|
|
@ -73,10 +79,16 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
|
||||
bool get _settledAnimating => _settling != null && _settle.isAnimating;
|
||||
|
||||
/// How close to a screen edge a drag must begin to navigate; horizontal
|
||||
/// drags elsewhere belong to text selection.
|
||||
/// Base zone at each screen edge that always swipes; horizontal drags
|
||||
/// elsewhere belong to text selection.
|
||||
static const _edgeZone = 40.0;
|
||||
|
||||
/// The swipe zone: the whole margin beside the content column, with the
|
||||
/// base zone as the floor.
|
||||
double _stripWidth(double width) => widget.contentWidth == null
|
||||
? _edgeZone
|
||||
: math.max(_edgeZone, (width - widget.contentWidth!) / 2);
|
||||
|
||||
void _onDragStart(DragStartDetails details) {
|
||||
if (_settledAnimating) return;
|
||||
setState(() => _dragging = true);
|
||||
|
|
@ -172,23 +184,23 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
offset: Offset(dx, 0),
|
||||
child: widget.child,
|
||||
),
|
||||
// Only horizontal drags that begin at a screen edge navigate;
|
||||
// the strips are translucent, so taps, vertical scrolling and
|
||||
// text selection still pass through everywhere.
|
||||
_edgeStrip(fromLeft: true),
|
||||
_edgeStrip(fromLeft: false),
|
||||
// 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)),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _edgeStrip({required bool fromLeft}) => Positioned(
|
||||
Widget _edgeStrip({required bool fromLeft, required double width}) => Positioned(
|
||||
left: fromLeft ? 0 : null,
|
||||
right: fromLeft ? null : 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: _edgeZone,
|
||||
width: width,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onHorizontalDragStart: _onDragStart,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ SwipeNavigator _navigator({
|
|||
required VoidCallback onForward,
|
||||
bool canGoBack = true,
|
||||
bool canGoForward = true,
|
||||
double? contentWidth,
|
||||
}) =>
|
||||
SwipeNavigator(
|
||||
contentWidth: contentWidth,
|
||||
canGoBack: canGoBack,
|
||||
canGoForward: canGoForward,
|
||||
backTitle: 'Previous page',
|
||||
|
|
@ -101,6 +103,47 @@ void main() {
|
|||
expect(find.text('Previous page'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('swipes anywhere in the margin beside the column',
|
||||
(tester) async {
|
||||
var back = 0;
|
||||
await tester.pumpWidget(_wrap(_navigator(
|
||||
onBack: () => back++,
|
||||
onForward: () {},
|
||||
// A 40em column leaves 200px margins on an 800px surface.
|
||||
contentWidth: 400,
|
||||
)));
|
||||
|
||||
final center = tester.getCenter(find.byType(SwipeNavigator));
|
||||
// Deep inside the left margin, far past the base edge zone.
|
||||
final gesture = await tester.startGesture(Offset(180, center.dy));
|
||||
await gesture.moveBy(const Offset(300, 0));
|
||||
await tester.pump();
|
||||
expect(find.text('Previous page'), findsOneWidget);
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(back, 1);
|
||||
});
|
||||
|
||||
testWidgets('drags over the content column still belong to selection',
|
||||
(tester) async {
|
||||
var back = 0;
|
||||
await tester.pumpWidget(_wrap(_navigator(
|
||||
onBack: () => back++,
|
||||
onForward: () {},
|
||||
contentWidth: 400,
|
||||
)));
|
||||
|
||||
final center = tester.getCenter(find.byType(SwipeNavigator));
|
||||
// Inside the column: no swipe, nothing peeks.
|
||||
final gesture = await tester.startGesture(Offset(400, center.dy));
|
||||
await gesture.moveBy(const Offset(300, 0));
|
||||
await tester.pump();
|
||||
expect(find.text('Previous page'), findsNothing);
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(back, 0);
|
||||
});
|
||||
|
||||
testWidgets('directions without pages rubber-band and never commit',
|
||||
(tester) async {
|
||||
var back = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue