Merge pull request #575 from liulifox233/master

[iOS] Enable full screen swipe back gesture
This commit is contained in:
ynyx631
2025-11-01 12:06:16 +08:00
committed by GitHub
13 changed files with 137 additions and 55 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:math';
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:venera/foundation/app.dart';
const double _kBackGestureWidth = 20.0;
@@ -19,6 +20,7 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
super.allowSnapshotting = true,
super.barrierDismissible = false,
this.enableIOSGesture = true,
this.iosFullScreenPopGesture = true,
this.preventRebuild = true,
}) {
assert(opaque);
@@ -48,6 +50,9 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
@override
final bool enableIOSGesture;
@override
final bool iosFullScreenPopGesture;
@override
final bool preventRebuild;
}
@@ -74,6 +79,8 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
bool get enableIOSGesture;
bool get iosFullScreenPopGesture;
bool get preventRebuild;
Widget? _child;
@@ -121,20 +128,22 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
builder = PredictiveBackPageTransitionsBuilder();
} else {
builder = SlidePageTransitionBuilder();
}
}
return builder.buildTransitions(
return builder.buildTransitions(
this,
context,
animation,
secondaryAnimation,
enableIOSGesture && App.isIOS
? IOSBackGestureDetector(
gestureWidth: _kBackGestureWidth,
enabledCallback: () => _isPopGestureEnabled<T>(this),
onStartPopGesture: () => _startPopGesture(this),
child: child)
: child);
enableIOSGesture && App.isIOS
? IOSBackGestureDetector(
gestureWidth: _kBackGestureWidth,
enabledCallback: () => _isPopGestureEnabled<T>(this),
onStartPopGesture: () => _startPopGesture(this),
fullScreen: iosFullScreenPopGesture,
child: child,
)
: child);
}
IOSBackGestureController _startPopGesture(PageRoute<T> route) {
@@ -206,6 +215,7 @@ class IOSBackGestureDetector extends StatefulWidget {
required this.child,
required this.gestureWidth,
required this.onStartPopGesture,
this.fullScreen = false,
super.key});
final double gestureWidth;
@@ -216,6 +226,8 @@ class IOSBackGestureDetector extends StatefulWidget {
final Widget child;
final bool fullScreen;
@override
State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();
}
@@ -247,26 +259,40 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
? MediaQuery.of(context).padding.left
: MediaQuery.of(context).padding.right;
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
final Widget gestureListener = widget.fullScreen
? Positioned.fill(
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.translucent,
),
)
: Positioned(
width: dragAreaWidth,
top: 0.0,
bottom: 0.0,
left: Directionality.of(context) == TextDirection.ltr ? 0.0 : null,
right: Directionality.of(context) == TextDirection.rtl ? 0.0 : null,
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.translucent,
),
);
return Stack(
fit: StackFit.passthrough,
children: <Widget>[
widget.child,
Positioned(
width: dragAreaWidth,
top: 0.0,
bottom: 0.0,
left: 0,
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.translucent,
),
),
gestureListener,
],
);
}
void _handlePointerDown(PointerDownEvent event) {
if (widget.enabledCallback()) _recognizer.addPointer(event);
if (!widget.enabledCallback()) return;
if (widget.fullScreen && _isPointerOverHorizontalScrollable(event)) {
return;
}
_recognizer.addPointer(event);
}
void _handleDragCancel() {
@@ -304,6 +330,28 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
_backGestureController!.dragUpdate(
_convertToLogical(details.primaryDelta! / context.size!.width));
}
bool _isPointerOverHorizontalScrollable(PointerDownEvent event) {
final HitTestResult result = HitTestResult();
WidgetsBinding.instance.hitTest(result, event.position);
for (final entry in result.path) {
final target = entry.target;
if (target is RenderViewport) {
if (_isAxisHorizontal(target.axisDirection)) {
return true;
}
} else if (target is RenderSliver) {
if (_isAxisHorizontal(target.constraints.axisDirection)) {
return true;
}
}
}
return false;
}
bool _isAxisHorizontal(AxisDirection direction) {
return direction == AxisDirection.left || direction == AxisDirection.right;
}
}
class SlidePageTransitionBuilder extends PageTransitionsBuilder {
@@ -314,30 +362,31 @@ class SlidePageTransitionBuilder extends PageTransitionsBuilder {
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
final Animation<double> primaryAnimation = App.isIOS
? animation
: CurvedAnimation(parent: animation, curve: Curves.ease);
final Animation<double> secondaryCurve = App.isIOS
? secondaryAnimation
: CurvedAnimation(parent: secondaryAnimation, curve: Curves.ease);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(primaryAnimation),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.ease,
)),
child: SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(-0.4, 0),
).animate(CurvedAnimation(
parent: secondaryAnimation,
curve: Curves.ease,
)),
child: PhysicalModel(
color: Colors.transparent,
borderRadius: BorderRadius.zero,
clipBehavior: Clip.hardEdge,
elevation: 6,
child: Material(child: child,),
),
)
begin: Offset.zero,
end: const Offset(-0.4, 0),
).animate(secondaryCurve),
child: PhysicalModel(
color: Colors.transparent,
borderRadius: BorderRadius.zero,
clipBehavior: Clip.hardEdge,
elevation: 6,
child: Material(child: child),
),
),
);
}
}

View File

@@ -542,6 +542,7 @@ class PageJumpTarget {
sourceKey: sourceKey,
options: List.from(attributes?["options"] ?? []),
),
iosFullScreenGesture: false,
);
} else if (page == "category") {
var key = ComicSource.find(sourceKey)!.categoryData!.key;

View File

@@ -14,14 +14,20 @@ extension Navigation on BuildContext {
return Navigator.of(this).canPop();
}
Future<T?> to<T>(Widget Function() builder) {
return Navigator.of(this)
.push<T>(AppPageRoute(builder: (context) => builder()));
Future<T?> to<T>(Widget Function() builder,
{bool enableIOSGesture = true, bool iosFullScreenGesture = true}) {
return Navigator.of(this).push<T>(AppPageRoute(
builder: (context) => builder(),
enableIOSGesture: enableIOSGesture,
iosFullScreenPopGesture: iosFullScreenGesture));
}
Future<void> toReplacement<T>(Widget Function() builder) {
return Navigator.of(this)
.pushReplacement(AppPageRoute(builder: (context) => builder()));
Future<void> toReplacement<T>(Widget Function() builder,
{bool enableIOSGesture = true, bool iosFullScreenGesture = true}) {
return Navigator.of(this).pushReplacement(AppPageRoute(
builder: (context) => builder(),
enableIOSGesture: enableIOSGesture,
iosFullScreenPopGesture: iosFullScreenGesture));
}
double get width => MediaQuery.of(this).size.width;

View File

@@ -154,6 +154,8 @@ class LocalComic with HistoryMixin implements Comic {
author: subtitle,
tags: tags,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
}

View File

@@ -170,6 +170,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
text: widget.keyword,
sourceKey: widget.source.key,
),
iosFullScreenGesture: false,
);
},
child: Column(

View File

@@ -116,6 +116,8 @@ abstract mixin class _ComicPageActions {
author: comic.findAuthor() ?? '',
tags: comic.plainTags,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
)
.then((_) {
onReadEnd();

View File

@@ -236,7 +236,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
author: localComic.subTitle ?? '',
tags: localComic.tags,
);
});
}, enableIOSGesture: false, iosFullScreenGesture: false);
App.mainNavigatorKey!.currentContext!.pop();
});
isFirst = false;

View File

@@ -521,7 +521,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
App.rootContext.to(() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
));
),
enableIOSGesture: false,
iosFullScreenGesture: false);
},
),
]),
@@ -622,6 +624,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
id: c.id,
sourceKey: c.sourceKey,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
},
),
@@ -647,6 +651,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
id: c.id,
sourceKey: c.sourceKey,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
}
},

View File

@@ -874,7 +874,10 @@ class _ImageFavoritesState extends State<ImageFavorites> {
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
context.to(() => const ImageFavoritesPage());
context.to(
() => const ImageFavoritesPage(),
iosFullScreenGesture: false,
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -993,7 +996,10 @@ class _ImageFavoritesState extends State<ImageFavorites> {
maxCount: maxCount,
enableTranslation: displayType != 2,
onTap: (text) {
context.to(() => ImageFavoritesPage(initialKeyword: text));
context.to(
() => ImageFavoritesPage(initialKeyword: text),
iosFullScreenGesture: false,
);
},
);
}).toList(),

View File

@@ -37,6 +37,8 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> {
initialEp: ep,
initialPage: page,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
}

View File

@@ -244,6 +244,8 @@ class _ImageFavoritesPhotoViewState extends State<ImageFavoritesPhotoView> {
initialEp: ep,
initialPage: page,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
},
),

View File

@@ -49,7 +49,10 @@ class _SearchPageState extends State<SearchPage> {
void search([String? text]) {
if (aggregatedSearch) {
context
.to(() => AggregatedSearchPage(keyword: text ?? controller.text))
.to(
() => AggregatedSearchPage(keyword: text ?? controller.text),
iosFullScreenGesture: false,
)
.then((_) => update());
} else {
context
@@ -59,6 +62,7 @@ class _SearchPageState extends State<SearchPage> {
sourceKey: searchTarget,
options: options,
),
iosFullScreenGesture: false,
)
.then((_) => update());
}

View File

@@ -252,9 +252,10 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
if (!App.isIOS) {
return;
}
if (event.position.dx < 20) {
gestureRecognizer.addPointer(event);
if (currentPage == -1) {
return;
}
gestureRecognizer.addPointer(event);
}
Widget buildLeft() {