opt image gesture

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-02-07 10:18:17 +08:00
parent 29e7e0e556
commit 946a5a1e47
4 changed files with 93 additions and 152 deletions

View File

@@ -0,0 +1,51 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show TransformationController;
class ImageHorizontalDragGestureRecognizer
extends HorizontalDragGestureRecognizer {
ImageHorizontalDragGestureRecognizer({
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
required this.width,
required this.transformationController,
});
Offset? _initialPosition;
final double width;
final TransformationController transformationController;
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
_initialPosition = event.position;
}
bool _isBoundaryAllowed() {
if (_initialPosition == null) {
return true;
}
final scale = transformationController.value.row0[0];
if (scale <= 1.0) {
return true;
}
final double xOffset = transformationController.value.row0[3];
final double boundaryEnd = width * scale;
final int xPos = (boundaryEnd + xOffset).round();
return (boundaryEnd.round() == xPos &&
lastPosition.global.dx > _initialPosition!.dx) ||
(width.round() == xPos &&
lastPosition.global.dx < _initialPosition!.dx);
}
@override
bool hasSufficientGlobalDistanceToAccept(
PointerDeviceKind pointerDeviceKind,
double? deviceTouchSlop,
) {
return globalDistanceMoved.abs() >
computeHitSlop(pointerDeviceKind, gestureSettings) &&
_isBoundaryAllowed();
}
}

View File

@@ -71,7 +71,6 @@ class InteractiveViewer extends StatefulWidget {
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onReset,
this.isAnimating,
required Widget this.child,
}) : assert(minScale > 0),
@@ -121,7 +120,6 @@ class InteractiveViewer extends StatefulWidget {
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onReset,
this.isAnimating,
required InteractiveViewerWidgetBuilder this.builder,
}) : assert(minScale > 0),
@@ -143,8 +141,7 @@ class InteractiveViewer extends StatefulWidget {
constrained = false,
child = null;
final Function? isAnimating;
final VoidCallback? onReset;
final ValueGetter<bool>? isAnimating;
final ValueChanged<ScaleStartDetails>? onPanStart;
final ValueChanged<ScaleUpdateDetails>? onPanUpdate;
final ValueChanged<ScaleEndDetails>? onPanEnd;
@@ -873,9 +870,6 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
// are handled with GestureDetector's scale gesture.
void _onScaleEnd(ScaleEndDetails details) {
if (_transformer.value.storage[0] == 1.0) {
widget.onReset?.call();
}
if (widget.isAnimating?.call() == true ||
(details.pointerCount < 2 && _transformer.value.storage[0] == 1.0)) {
widget.onPanEnd?.call(details);

View File

@@ -19,18 +19,13 @@ class InteractiveViewerBoundary extends StatefulWidget {
required this.child,
required this.boundaryWidth,
required this.controller,
this.onScaleChanged,
this.onLeftBoundaryHit,
this.onRightBoundaryHit,
this.onNoBoundaryHit,
required this.maxScale,
required this.minScale,
this.onDismissed,
this.onReset,
this.dismissThreshold = 0.2,
this.onInteractionEnd,
});
final VoidCallback? onReset;
final double dismissThreshold;
final VoidCallback? onDismissed;
@@ -45,22 +40,12 @@ class InteractiveViewerBoundary extends StatefulWidget {
/// The [TransformationController] for the [InteractiveViewer].
final TransformationController controller;
/// Called when the scale changed after an interaction ended.
final ScaleChanged? onScaleChanged;
/// Called when the left boundary has been hit after an interaction ended.
final VoidCallback? onLeftBoundaryHit;
/// Called when the right boundary has been hit after an interaction ended.
final VoidCallback? onRightBoundaryHit;
/// Called when no boundary has been hit after an interaction ended.
final VoidCallback? onNoBoundaryHit;
final double maxScale;
final double minScale;
final GestureScaleEndCallback? onInteractionEnd;
@override
InteractiveViewerBoundaryState createState() =>
InteractiveViewerBoundaryState();
@@ -69,9 +54,6 @@ class InteractiveViewerBoundary extends StatefulWidget {
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
with SingleTickerProviderStateMixin {
late TransformationController _controller;
double? _scale;
late AnimationController _animateController;
late Animation<Offset> _slideAnimation;
late Animation<double> _scaleAnimation;
@@ -179,36 +161,6 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
}
}
void _updateBoundaryDetection() {
final double scale = _controller.value.row0[0];
if (_scale != scale) {
// the scale changed
_scale = scale;
widget.onScaleChanged?.call(scale);
}
if (scale <= 1.01) {
// cant hit any boundaries when the child is not scaled
return;
}
final double xOffset = _controller.value.row0[3];
final double boundaryWidth = widget.boundaryWidth;
final double boundaryEnd = boundaryWidth * scale;
final double xPos = boundaryEnd + xOffset;
if (boundaryEnd.round() == xPos.round()) {
// left boundary hit
widget.onLeftBoundaryHit?.call();
} else if (boundaryWidth.round() == xPos.round()) {
// right boundary hit
widget.onRightBoundaryHit?.call();
} else {
widget.onNoBoundaryHit?.call();
}
}
Widget get content => DecoratedBoxTransition(
decoration: _opacityAnimation,
child: SlideTransition(
@@ -226,11 +178,10 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
maxScale: widget.maxScale,
minScale: widget.minScale,
transformationController: _controller,
onInteractionEnd: (_) => _updateBoundaryDetection(),
onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate,
onPanEnd: _handleDragEnd,
onReset: widget.onReset,
onInteractionEnd: widget.onInteractionEnd,
isAnimating: () => _animateController.value != 0,
child: content,
);

View File

@@ -1,5 +1,7 @@
import 'dart:io';
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/utils/extension/string_ext.dart';
@@ -10,7 +12,7 @@ import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide PageView;
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart';
@@ -33,7 +35,6 @@ typedef IndexedFocusedWidgetBuilder =
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
);
typedef IndexedTagStringBuilder = String Function(int index);
@@ -82,16 +83,14 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
late AnimationController _animationController;
Animation<Matrix4>? _animation;
/// `true` when an source is zoomed in and not at the at a horizontal boundary
/// to disable the [PageView].
bool _enablePageView = true;
late Offset _doubleTapLocalPosition;
late final RxInt currentIndex = widget.initIndex.obs;
late final int _quality = Pref.previewQ;
late final RxBool _hasScaled = false.obs;
@override
void initState() {
super.initState();
@@ -134,57 +133,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
super.dispose();
}
/// When the source gets scaled up, the swipe up / down to dismiss gets
/// disabled.
///
/// When the scale resets, the dismiss and the page view swiping gets enabled.
void _onScaleChanged(double scale) {
final bool initialScale = scale <= widget.minScale;
if (initialScale) {
if (!_enablePageView) {
setState(() {
_enablePageView = true;
});
}
} else {
if (_enablePageView) {
setState(() {
_enablePageView = false;
});
}
}
}
/// When the left boundary has been hit after scaling up the source, the page
/// view swiping gets enabled if it has a page to swipe to.
void _onLeftBoundaryHit() {
if (!_enablePageView && _pageController.page!.floor() > 0) {
setState(() {
_enablePageView = true;
});
}
}
/// When the right boundary has been hit after scaling up the source, the page
/// view swiping gets enabled if it has a page to swipe to.
void _onRightBoundaryHit() {
if (!_enablePageView &&
_pageController.page!.floor() < widget.sources.length - 1) {
setState(() {
_enablePageView = true;
});
}
}
/// When the source has been scaled up and no horizontal boundary has been hit,
/// the page view swiping gets disabled.
void _onNoBoundaryHit() {
if (_enablePageView) {
setState(() {
_enablePageView = false;
});
}
_hasScaled.value = scale > 1.0;
}
void _onPlay(String liveUrl) {
@@ -229,32 +179,21 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
@override
Widget build(BuildContext context) {
final width = MediaQuery.widthOf(context);
return Stack(
clipBehavior: Clip.none,
children: [
InteractiveViewerBoundary(
controller: _transformationController,
boundaryWidth: MediaQuery.widthOf(context),
onScaleChanged: _onScaleChanged,
onLeftBoundaryHit: _onLeftBoundaryHit,
onRightBoundaryHit: _onRightBoundaryHit,
onNoBoundaryHit: _onNoBoundaryHit,
boundaryWidth: width,
maxScale: widget.maxScale,
minScale: widget.minScale,
onDismissed: Get.back,
onReset: () {
if (!_enablePageView) {
setState(() {
_enablePageView = true;
});
}
},
onInteractionEnd: (_) =>
_onScaleChanged(_transformationController.value.row0[0]),
child: PageView.builder(
onPageChanged: _onPageChanged,
controller: _pageController,
physics: _enablePageView
? null
: const NeverScrollableScrollPhysics(),
itemCount: widget.sources.length,
itemBuilder: (BuildContext context, int index) {
final item = widget.sources[index];
@@ -283,38 +222,44 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
context,
index,
index == currentIndex.value,
_enablePageView,
)
: _itemBuilder(index, item),
);
},
horizontalDragGestureRecognizer:
ImageHorizontalDragGestureRecognizer(
width: width,
transformationController: _transformationController,
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding:
MediaQuery.viewPaddingOf(context) +
const EdgeInsets.fromLTRB(12, 8, 20, 8),
decoration: _enablePageView
? BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.3),
],
child: Obx(
() => Container(
padding:
MediaQuery.viewPaddingOf(context) +
const EdgeInsets.fromLTRB(12, 8, 20, 8),
decoration: _hasScaled.value
? null
: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.3),
],
),
),
)
: null,
alignment: Alignment.center,
child: Obx(
() => Text(
"${currentIndex.value + 1}/${widget.sources.length}",
style: const TextStyle(color: Colors.white),
alignment: Alignment.center,
child: Obx(
() => Text(
"${currentIndex.value + 1}/${widget.sources.length}",
style: const TextStyle(color: Colors.white),
),
),
),
),