chapter switching gesture

This commit is contained in:
nyne
2024-10-30 20:15:50 +08:00
parent 7ce84d095e
commit 3e1bb5ef5c
3 changed files with 148 additions and 55 deletions

View File

@@ -12,14 +12,16 @@ class _ReaderGestureDetector extends StatefulWidget {
class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
late TapGestureRecognizer _tapGestureRecognizer; late TapGestureRecognizer _tapGestureRecognizer;
static const _kDoubleTapMinTime = Duration(milliseconds: 200); static const _kDoubleTapMaxTime = Duration(milliseconds: 200);
static const _kLongPressMinTime = Duration(milliseconds: 200); static const _kLongPressMinTime = Duration(milliseconds: 250);
static const _kDoubleTapMaxDistanceSquared = 20.0 * 20.0; static const _kDoubleTapMaxDistanceSquared = 20.0 * 20.0;
static const _kTapToTurnPagePercent = 0.3; static const _kTapToTurnPagePercent = 0.3;
_DragListener? dragListener;
@override @override
void initState() { void initState() {
_tapGestureRecognizer = TapGestureRecognizer() _tapGestureRecognizer = TapGestureRecognizer()
@@ -28,6 +30,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
onSecondaryTapUp(details.globalPosition); onSecondaryTapUp(details.globalPosition);
}; };
super.initState(); super.initState();
context.readerScaffold._gestureDetectorState = this;
} }
@override @override
@@ -38,11 +41,20 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
_lastTapPointer = event.pointer; _lastTapPointer = event.pointer;
_lastTapMoveDistance = Offset.zero; _lastTapMoveDistance = Offset.zero;
_tapGestureRecognizer.addPointer(event); _tapGestureRecognizer.addPointer(event);
if(_dragInProgress) {
dragListener?.onEnd?.call();
_dragInProgress = false;
}
Future.delayed(_kLongPressMinTime, () { Future.delayed(_kLongPressMinTime, () {
if (_lastTapPointer == event.pointer && if (_lastTapPointer == event.pointer) {
_lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) { if(_lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) {
onLongPressedDown(event.position); onLongPressedDown(event.position);
_longPressInProgress = true; _longPressInProgress = true;
} else {
_dragInProgress = true;
dragListener?.onStart?.call(event.position);
dragListener?.onMove?.call(_lastTapMoveDistance!);
}
} }
}); });
}, },
@@ -50,11 +62,18 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
if (event.pointer == _lastTapPointer) { if (event.pointer == _lastTapPointer) {
_lastTapMoveDistance = event.delta + _lastTapMoveDistance!; _lastTapMoveDistance = event.delta + _lastTapMoveDistance!;
} }
if(_dragInProgress) {
dragListener?.onMove?.call(event.delta);
}
}, },
onPointerUp: (event) { onPointerUp: (event) {
if (_longPressInProgress) { if (_longPressInProgress) {
onLongPressedUp(event.position); onLongPressedUp(event.position);
} }
if(_dragInProgress) {
dragListener?.onEnd?.call();
_dragInProgress = false;
}
_lastTapPointer = null; _lastTapPointer = null;
_lastTapMoveDistance = null; _lastTapMoveDistance = null;
}, },
@@ -89,6 +108,8 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
bool _longPressInProgress = false; bool _longPressInProgress = false;
bool _dragInProgress = false;
void onTapUp(TapUpDetails event) { void onTapUp(TapUpDetails event) {
if (_longPressInProgress) { if (_longPressInProgress) {
_longPressInProgress = false; _longPressInProgress = false;
@@ -107,7 +128,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
} }
} }
_previousEvent = event; _previousEvent = event;
Future.delayed(_kDoubleTapMinTime, () { Future.delayed(_kDoubleTapMaxTime, () {
if (_previousEvent == event) { if (_previousEvent == event) {
onTap(location); onTap(location);
_previousEvent = null; _previousEvent = null;
@@ -222,3 +243,11 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
context.reader._imageViewController?.handleLongPressDown(location); context.reader._imageViewController?.handleLongPressDown(location);
} }
} }
class _DragListener {
void Function(Offset point)? onStart;
void Function(Offset offset)? onMove;
void Function()? onEnd;
_DragListener({this.onStart, this.onMove, this.onEnd});
}

View File

@@ -2,6 +2,7 @@ library venera_reader;
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';

View File

@@ -22,22 +22,66 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
var lastValue = 0; var lastValue = 0;
double fABValue = 0; var fABValue = ValueNotifier<double>(0);
_ReaderGestureDetectorState? _gestureDetectorState;
void setFloatingButton(int value) { void setFloatingButton(int value) {
lastValue = showFloatingButtonValue; lastValue = showFloatingButtonValue;
if (value == 0) { if (value == 0) {
if (showFloatingButtonValue != 0) { if (showFloatingButtonValue != 0) {
showFloatingButtonValue = 0; showFloatingButtonValue = 0;
fABValue = 0; fABValue.value = 0;
update(); update();
} }
_gestureDetectorState!.dragListener = null;
} }
var readerMode = context.reader.mode;
if (value == 1 && showFloatingButtonValue == 0) { if (value == 1 && showFloatingButtonValue == 0) {
showFloatingButtonValue = 1; showFloatingButtonValue = 1;
_gestureDetectorState!.dragListener = _DragListener(
onMove: (offset) {
if (readerMode == ReaderMode.continuousTopToBottom) {
fABValue.value -= offset.dy;
} else if (readerMode == ReaderMode.continuousLeftToRight) {
fABValue.value -= offset.dx;
} else if (readerMode == ReaderMode.continuousRightToLeft) {
fABValue.value += offset.dx;
}
},
onEnd: () {
if (fABValue.value.abs() > 58 * 3) {
setState(() {
showFloatingButtonValue = 0;
});
context.reader.toNextChapter();
}
fABValue.value = 0;
},
);
update(); update();
} else if (value == -1 && showFloatingButtonValue == 0) { } else if (value == -1 && showFloatingButtonValue == 0) {
showFloatingButtonValue = -1; showFloatingButtonValue = -1;
_gestureDetectorState!.dragListener = _DragListener(
onMove: (offset) {
if (readerMode == ReaderMode.continuousTopToBottom) {
fABValue.value += offset.dy;
} else if (readerMode == ReaderMode.continuousLeftToRight) {
fABValue.value += offset.dx;
} else if (readerMode == ReaderMode.continuousRightToLeft) {
fABValue.value -= offset.dx;
}
},
onEnd: () {
if (fABValue.value.abs() > 58 * 3) {
setState(() {
showFloatingButtonValue = 0;
});
context.reader.toPrevChapter();
}
fABValue.value = 0;
},
);
update(); update();
} }
} }
@@ -50,7 +94,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
sliderFocus.nextFocus(); sliderFocus.nextFocus();
} }
}); });
if(rotation != null) { if (rotation != null) {
SystemChrome.setPreferredOrientations(DeviceOrientation.values); SystemChrome.setPreferredOrientations(DeviceOrientation.values);
} }
super.initState(); super.initState();
@@ -167,8 +211,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton.filledTonal( IconButton.filledTonal(
onPressed: () { onPressed: () {
if(!context.reader.toPrevChapter()) { if (!context.reader.toPrevChapter()) {
context.reader.toPage(1); context.reader.toPage(1);
} else {
if(showFloatingButtonValue != 0) {
setState(() {
showFloatingButtonValue = 0;
});
}
} }
}, },
icon: const Icon(Icons.first_page), icon: const Icon(Icons.first_page),
@@ -178,8 +228,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
), ),
IconButton.filledTonal( IconButton.filledTonal(
onPressed: () { onPressed: () {
if(!context.reader.toNextChapter()) { if (!context.reader.toNextChapter()) {
context.reader.toPage(context.reader.maxPage); context.reader.toPage(context.reader.maxPage);
} else {
if(showFloatingButtonValue != 0) {
setState(() {
showFloatingButtonValue = 0;
});
}
} }
}, },
icon: const Icon(Icons.last_page)), icon: const Icon(Icons.last_page)),
@@ -373,8 +429,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
if (imageKey.startsWith("file://")) { if (imageKey.startsWith("file://")) {
return await File(imageKey.substring(7)).readAsBytes(); return await File(imageKey.substring(7)).readAsBytes();
} else { } else {
return (await CacheManager() return (await CacheManager().findCache(
.findCache("$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
.readAsBytes(); .readAsBytes();
} }
} }
@@ -416,14 +472,6 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
Widget buildEpChangeButton() { Widget buildEpChangeButton() {
if (context.reader.widget.chapters == null) return const SizedBox(); if (context.reader.widget.chapters == null) return const SizedBox();
switch (showFloatingButtonValue) { switch (showFloatingButtonValue) {
case -1:
return FloatingActionButton(
onPressed: () {
setFloatingButton(0);
context.reader.toPrevChapter();
},
child: const Icon(Icons.arrow_back_ios_outlined),
);
case 0: case 0:
return Container( return Container(
width: 58, width: 58,
@@ -441,6 +489,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
color: Theme.of(context).colorScheme.onPrimaryContainer, color: Theme.of(context).colorScheme.onPrimaryContainer,
), ),
); );
case -1:
case 1: case 1:
return Container( return Container(
width: 58, width: 58,
@@ -450,40 +499,54 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Stack( child: ValueListenableBuilder(
children: [ valueListenable: fABValue,
Positioned.fill( builder: (context, value, child) {
child: Material( return Stack(
color: Colors.transparent, children: [
child: InkWell( Positioned.fill(
onTap: () { child: Material(
setFloatingButton(0); color: Colors.transparent,
context.reader.toNextChapter(); child: InkWell(
}, onTap: () {
borderRadius: BorderRadius.circular(16), setFloatingButton(0);
child: Center( if (showFloatingButtonValue == 1) {
child: Icon( context.reader.toNextChapter();
Icons.arrow_forward_ios, } else {
size: 24, context.reader.toPrevChapter();
color: Theme.of(context).colorScheme.onPrimaryContainer, }
)), },
borderRadius: BorderRadius.circular(16),
child: Center(
child: Icon(
showFloatingButtonValue == 1
? Icons.arrow_forward_ios
: Icons.arrow_back_ios_outlined,
size: 24,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
),
), ),
), Positioned(
), bottom: 0,
Positioned( left: 0,
bottom: 0, right: 0,
left: 0, height: value.clamp(0, 58*3) / 3,
right: 0, child: ColoredBox(
height: fABValue, color: Theme.of(context)
child: ColoredBox( .colorScheme
color: Theme.of(context) .surfaceTint
.colorScheme .withOpacity(0.2),
.surfaceTint child: const SizedBox.expand(),
.withOpacity(0.2), ),
child: const SizedBox.expand(), ),
), ],
) );
], },
), ),
); );
} }