From 3e1bb5ef5cd7ecd8c523cd206567e5d0776d3e83 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 30 Oct 2024 20:15:50 +0800 Subject: [PATCH] chapter switching gesture --- lib/pages/reader/gesture.dart | 43 +++++++-- lib/pages/reader/reader.dart | 1 + lib/pages/reader/scaffold.dart | 159 +++++++++++++++++++++++---------- 3 files changed, 148 insertions(+), 55 deletions(-) diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart index 49f1448..9c04e34 100644 --- a/lib/pages/reader/gesture.dart +++ b/lib/pages/reader/gesture.dart @@ -12,14 +12,16 @@ class _ReaderGestureDetector extends StatefulWidget { class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { 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 _kTapToTurnPagePercent = 0.3; + _DragListener? dragListener; + @override void initState() { _tapGestureRecognizer = TapGestureRecognizer() @@ -28,6 +30,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { onSecondaryTapUp(details.globalPosition); }; super.initState(); + context.readerScaffold._gestureDetectorState = this; } @override @@ -38,11 +41,20 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { _lastTapPointer = event.pointer; _lastTapMoveDistance = Offset.zero; _tapGestureRecognizer.addPointer(event); + if(_dragInProgress) { + dragListener?.onEnd?.call(); + _dragInProgress = false; + } Future.delayed(_kLongPressMinTime, () { - if (_lastTapPointer == event.pointer && - _lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) { - onLongPressedDown(event.position); - _longPressInProgress = true; + if (_lastTapPointer == event.pointer) { + if(_lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) { + onLongPressedDown(event.position); + _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) { _lastTapMoveDistance = event.delta + _lastTapMoveDistance!; } + if(_dragInProgress) { + dragListener?.onMove?.call(event.delta); + } }, onPointerUp: (event) { if (_longPressInProgress) { onLongPressedUp(event.position); } + if(_dragInProgress) { + dragListener?.onEnd?.call(); + _dragInProgress = false; + } _lastTapPointer = null; _lastTapMoveDistance = null; }, @@ -89,6 +108,8 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { bool _longPressInProgress = false; + bool _dragInProgress = false; + void onTapUp(TapUpDetails event) { if (_longPressInProgress) { _longPressInProgress = false; @@ -107,7 +128,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { } } _previousEvent = event; - Future.delayed(_kDoubleTapMinTime, () { + Future.delayed(_kDoubleTapMaxTime, () { if (_previousEvent == event) { onTap(location); _previousEvent = null; @@ -222,3 +243,11 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { 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}); +} \ No newline at end of file diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index b90f42f..65b4a47 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -2,6 +2,7 @@ library venera_reader; import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 96d1de8..a4db13a 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -22,22 +22,66 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { var lastValue = 0; - double fABValue = 0; + var fABValue = ValueNotifier(0); + + _ReaderGestureDetectorState? _gestureDetectorState; void setFloatingButton(int value) { lastValue = showFloatingButtonValue; if (value == 0) { if (showFloatingButtonValue != 0) { showFloatingButtonValue = 0; - fABValue = 0; + fABValue.value = 0; update(); } + _gestureDetectorState!.dragListener = null; } + var readerMode = context.reader.mode; if (value == 1 && showFloatingButtonValue == 0) { 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(); } else if (value == -1 && showFloatingButtonValue == 0) { 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(); } } @@ -50,7 +94,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { sliderFocus.nextFocus(); } }); - if(rotation != null) { + if (rotation != null) { SystemChrome.setPreferredOrientations(DeviceOrientation.values); } super.initState(); @@ -167,8 +211,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { const SizedBox(width: 8), IconButton.filledTonal( onPressed: () { - if(!context.reader.toPrevChapter()) { + if (!context.reader.toPrevChapter()) { context.reader.toPage(1); + } else { + if(showFloatingButtonValue != 0) { + setState(() { + showFloatingButtonValue = 0; + }); + } } }, icon: const Icon(Icons.first_page), @@ -178,8 +228,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ), IconButton.filledTonal( onPressed: () { - if(!context.reader.toNextChapter()) { + if (!context.reader.toNextChapter()) { context.reader.toPage(context.reader.maxPage); + } else { + if(showFloatingButtonValue != 0) { + setState(() { + showFloatingButtonValue = 0; + }); + } } }, icon: const Icon(Icons.last_page)), @@ -373,8 +429,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { if (imageKey.startsWith("file://")) { return await File(imageKey.substring(7)).readAsBytes(); } else { - return (await CacheManager() - .findCache("$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! + return (await CacheManager().findCache( + "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! .readAsBytes(); } } @@ -416,14 +472,6 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { Widget buildEpChangeButton() { if (context.reader.widget.chapters == null) return const SizedBox(); switch (showFloatingButtonValue) { - case -1: - return FloatingActionButton( - onPressed: () { - setFloatingButton(0); - context.reader.toPrevChapter(); - }, - child: const Icon(Icons.arrow_back_ios_outlined), - ); case 0: return Container( width: 58, @@ -441,6 +489,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { color: Theme.of(context).colorScheme.onPrimaryContainer, ), ); + case -1: case 1: return Container( width: 58, @@ -450,40 +499,54 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(16), ), - child: Stack( - children: [ - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - setFloatingButton(0); - context.reader.toNextChapter(); - }, - borderRadius: BorderRadius.circular(16), - child: Center( - child: Icon( - Icons.arrow_forward_ios, - size: 24, - color: Theme.of(context).colorScheme.onPrimaryContainer, - )), + child: ValueListenableBuilder( + valueListenable: fABValue, + builder: (context, value, child) { + return Stack( + children: [ + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setFloatingButton(0); + if (showFloatingButtonValue == 1) { + context.reader.toNextChapter(); + } else { + context.reader.toPrevChapter(); + } + }, + 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, - left: 0, - right: 0, - height: fABValue, - child: ColoredBox( - color: Theme.of(context) - .colorScheme - .surfaceTint - .withOpacity(0.2), - child: const SizedBox.expand(), - ), - ) - ], + Positioned( + bottom: 0, + left: 0, + right: 0, + height: value.clamp(0, 58*3) / 3, + child: ColoredBox( + color: Theme.of(context) + .colorScheme + .surfaceTint + .withOpacity(0.2), + child: const SizedBox.expand(), + ), + ), + ], + ); + }, ), ); }