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,
|
dark: Theme.of(context).brightness == Brightness.dark,
|
||||||
).background;
|
).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(
|
return SwipeNavigator(
|
||||||
canGoBack: browser.canGoBack,
|
canGoBack: browser.canGoBack,
|
||||||
canGoForward: browser.canGoForward,
|
canGoForward: browser.canGoForward,
|
||||||
|
|
@ -105,6 +110,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
||||||
backTitle: browser.backPage.value?.title,
|
backTitle: browser.backPage.value?.title,
|
||||||
forwardTitle: browser.forwardPage.value?.title,
|
forwardTitle: browser.forwardPage.value?.title,
|
||||||
background: peekBackground,
|
background: peekBackground,
|
||||||
|
contentWidth: page == null ? null : contentWidth,
|
||||||
onBack: browser.back,
|
onBack: browser.back,
|
||||||
onForward: browser.forward,
|
onForward: browser.forward,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class SwipeNavigator extends StatefulWidget {
|
||||||
this.forwardView,
|
this.forwardView,
|
||||||
this.backTitle,
|
this.backTitle,
|
||||||
this.forwardTitle,
|
this.forwardTitle,
|
||||||
|
this.contentWidth,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -35,6 +36,11 @@ class SwipeNavigator extends StatefulWidget {
|
||||||
final String? backTitle;
|
final String? backTitle;
|
||||||
final String? forwardTitle;
|
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 Color background;
|
||||||
final VoidCallback onBack;
|
final VoidCallback onBack;
|
||||||
final VoidCallback onForward;
|
final VoidCallback onForward;
|
||||||
|
|
@ -73,10 +79,16 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
||||||
|
|
||||||
bool get _settledAnimating => _settling != null && _settle.isAnimating;
|
bool get _settledAnimating => _settling != null && _settle.isAnimating;
|
||||||
|
|
||||||
/// How close to a screen edge a drag must begin to navigate; horizontal
|
/// Base zone at each screen edge that always swipes; horizontal drags
|
||||||
/// drags elsewhere belong to text selection.
|
/// elsewhere belong to text selection.
|
||||||
static const _edgeZone = 40.0;
|
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) {
|
void _onDragStart(DragStartDetails details) {
|
||||||
if (_settledAnimating) return;
|
if (_settledAnimating) return;
|
||||||
setState(() => _dragging = true);
|
setState(() => _dragging = true);
|
||||||
|
|
@ -172,23 +184,23 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
||||||
offset: Offset(dx, 0),
|
offset: Offset(dx, 0),
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
// Only horizontal drags that begin at a screen edge navigate;
|
// Horizontal drags in the strips navigate; elsewhere they select
|
||||||
// the strips are translucent, so taps, vertical scrolling and
|
// text. The strips are translucent, so taps, vertical scrolling
|
||||||
// text selection still pass through everywhere.
|
// and mid-page selection still pass through everywhere.
|
||||||
_edgeStrip(fromLeft: true),
|
_edgeStrip(fromLeft: true, width: _stripWidth(_width)),
|
||||||
_edgeStrip(fromLeft: false),
|
_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,
|
left: fromLeft ? 0 : null,
|
||||||
right: fromLeft ? null : 0,
|
right: fromLeft ? null : 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: _edgeZone,
|
width: width,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onHorizontalDragStart: _onDragStart,
|
onHorizontalDragStart: _onDragStart,
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,10 @@ SwipeNavigator _navigator({
|
||||||
required VoidCallback onForward,
|
required VoidCallback onForward,
|
||||||
bool canGoBack = true,
|
bool canGoBack = true,
|
||||||
bool canGoForward = true,
|
bool canGoForward = true,
|
||||||
|
double? contentWidth,
|
||||||
}) =>
|
}) =>
|
||||||
SwipeNavigator(
|
SwipeNavigator(
|
||||||
|
contentWidth: contentWidth,
|
||||||
canGoBack: canGoBack,
|
canGoBack: canGoBack,
|
||||||
canGoForward: canGoForward,
|
canGoForward: canGoForward,
|
||||||
backTitle: 'Previous page',
|
backTitle: 'Previous page',
|
||||||
|
|
@ -101,6 +103,47 @@ void main() {
|
||||||
expect(find.text('Previous page'), findsNothing);
|
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',
|
testWidgets('directions without pages rubber-band and never commit',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
var back = 0;
|
var back = 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue