diff --git a/lib/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart b/lib/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart new file mode 100644 index 000000000..659473ee4 --- /dev/null +++ b/lib/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart @@ -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(); + } +} diff --git a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart index 8662c1f9e..1008efa5b 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart @@ -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? isAnimating; final ValueChanged? onPanStart; final ValueChanged? onPanUpdate; final ValueChanged? onPanEnd; @@ -873,9 +870,6 @@ class _InteractiveViewerState extends State // 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); diff --git a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart index 6d5edc9bc..beba6861c 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart @@ -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 with SingleTickerProviderStateMixin { late TransformationController _controller; - - double? _scale; - late AnimationController _animateController; late Animation _slideAnimation; late Animation _scaleAnimation; @@ -179,36 +161,6 @@ class InteractiveViewerBoundaryState extends State } } - 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 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, ); diff --git a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart index f0ff69414..db18f6ba5 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart @@ -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 late AnimationController _animationController; Animation? _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 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 @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 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), + ), ), ), ),