mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-01 00:28:18 +08:00
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,7 +71,6 @@ class InteractiveViewer extends StatefulWidget {
|
|||||||
this.onPanStart,
|
this.onPanStart,
|
||||||
this.onPanUpdate,
|
this.onPanUpdate,
|
||||||
this.onPanEnd,
|
this.onPanEnd,
|
||||||
this.onReset,
|
|
||||||
this.isAnimating,
|
this.isAnimating,
|
||||||
required Widget this.child,
|
required Widget this.child,
|
||||||
}) : assert(minScale > 0),
|
}) : assert(minScale > 0),
|
||||||
@@ -121,7 +120,6 @@ class InteractiveViewer extends StatefulWidget {
|
|||||||
this.onPanStart,
|
this.onPanStart,
|
||||||
this.onPanUpdate,
|
this.onPanUpdate,
|
||||||
this.onPanEnd,
|
this.onPanEnd,
|
||||||
this.onReset,
|
|
||||||
this.isAnimating,
|
this.isAnimating,
|
||||||
required InteractiveViewerWidgetBuilder this.builder,
|
required InteractiveViewerWidgetBuilder this.builder,
|
||||||
}) : assert(minScale > 0),
|
}) : assert(minScale > 0),
|
||||||
@@ -143,8 +141,7 @@ class InteractiveViewer extends StatefulWidget {
|
|||||||
constrained = false,
|
constrained = false,
|
||||||
child = null;
|
child = null;
|
||||||
|
|
||||||
final Function? isAnimating;
|
final ValueGetter<bool>? isAnimating;
|
||||||
final VoidCallback? onReset;
|
|
||||||
final ValueChanged<ScaleStartDetails>? onPanStart;
|
final ValueChanged<ScaleStartDetails>? onPanStart;
|
||||||
final ValueChanged<ScaleUpdateDetails>? onPanUpdate;
|
final ValueChanged<ScaleUpdateDetails>? onPanUpdate;
|
||||||
final ValueChanged<ScaleEndDetails>? onPanEnd;
|
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
|
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
|
||||||
// are handled with GestureDetector's scale gesture.
|
// are handled with GestureDetector's scale gesture.
|
||||||
void _onScaleEnd(ScaleEndDetails details) {
|
void _onScaleEnd(ScaleEndDetails details) {
|
||||||
if (_transformer.value.storage[0] == 1.0) {
|
|
||||||
widget.onReset?.call();
|
|
||||||
}
|
|
||||||
if (widget.isAnimating?.call() == true ||
|
if (widget.isAnimating?.call() == true ||
|
||||||
(details.pointerCount < 2 && _transformer.value.storage[0] == 1.0)) {
|
(details.pointerCount < 2 && _transformer.value.storage[0] == 1.0)) {
|
||||||
widget.onPanEnd?.call(details);
|
widget.onPanEnd?.call(details);
|
||||||
|
|||||||
@@ -19,18 +19,13 @@ class InteractiveViewerBoundary extends StatefulWidget {
|
|||||||
required this.child,
|
required this.child,
|
||||||
required this.boundaryWidth,
|
required this.boundaryWidth,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.onScaleChanged,
|
|
||||||
this.onLeftBoundaryHit,
|
|
||||||
this.onRightBoundaryHit,
|
|
||||||
this.onNoBoundaryHit,
|
|
||||||
required this.maxScale,
|
required this.maxScale,
|
||||||
required this.minScale,
|
required this.minScale,
|
||||||
this.onDismissed,
|
this.onDismissed,
|
||||||
this.onReset,
|
|
||||||
this.dismissThreshold = 0.2,
|
this.dismissThreshold = 0.2,
|
||||||
|
this.onInteractionEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback? onReset;
|
|
||||||
final double dismissThreshold;
|
final double dismissThreshold;
|
||||||
final VoidCallback? onDismissed;
|
final VoidCallback? onDismissed;
|
||||||
|
|
||||||
@@ -45,22 +40,12 @@ class InteractiveViewerBoundary extends StatefulWidget {
|
|||||||
/// The [TransformationController] for the [InteractiveViewer].
|
/// The [TransformationController] for the [InteractiveViewer].
|
||||||
final TransformationController controller;
|
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 maxScale;
|
||||||
|
|
||||||
final double minScale;
|
final double minScale;
|
||||||
|
|
||||||
|
final GestureScaleEndCallback? onInteractionEnd;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InteractiveViewerBoundaryState createState() =>
|
InteractiveViewerBoundaryState createState() =>
|
||||||
InteractiveViewerBoundaryState();
|
InteractiveViewerBoundaryState();
|
||||||
@@ -69,9 +54,6 @@ class InteractiveViewerBoundary extends StatefulWidget {
|
|||||||
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late TransformationController _controller;
|
late TransformationController _controller;
|
||||||
|
|
||||||
double? _scale;
|
|
||||||
|
|
||||||
late AnimationController _animateController;
|
late AnimationController _animateController;
|
||||||
late Animation<Offset> _slideAnimation;
|
late Animation<Offset> _slideAnimation;
|
||||||
late Animation<double> _scaleAnimation;
|
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(
|
Widget get content => DecoratedBoxTransition(
|
||||||
decoration: _opacityAnimation,
|
decoration: _opacityAnimation,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
@@ -226,11 +178,10 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
|||||||
maxScale: widget.maxScale,
|
maxScale: widget.maxScale,
|
||||||
minScale: widget.minScale,
|
minScale: widget.minScale,
|
||||||
transformationController: _controller,
|
transformationController: _controller,
|
||||||
onInteractionEnd: (_) => _updateBoundaryDetection(),
|
|
||||||
onPanStart: _handleDragStart,
|
onPanStart: _handleDragStart,
|
||||||
onPanUpdate: _handleDragUpdate,
|
onPanUpdate: _handleDragUpdate,
|
||||||
onPanEnd: _handleDragEnd,
|
onPanEnd: _handleDragEnd,
|
||||||
onReset: widget.onReset,
|
onInteractionEnd: widget.onInteractionEnd,
|
||||||
isAnimating: () => _animateController.value != 0,
|
isAnimating: () => _animateController.value != 0,
|
||||||
child: content,
|
child: content,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:io';
|
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/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart';
|
||||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||||
import 'package:PiliPlus/utils/extension/string_ext.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:PiliPlus/utils/utils.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:easy_debounce/easy_throttle.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:flutter/services.dart' show HapticFeedback;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
@@ -33,7 +35,6 @@ typedef IndexedFocusedWidgetBuilder =
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
int index,
|
int index,
|
||||||
bool isFocus,
|
bool isFocus,
|
||||||
bool enablePageView,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef IndexedTagStringBuilder = String Function(int index);
|
typedef IndexedTagStringBuilder = String Function(int index);
|
||||||
@@ -82,16 +83,14 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
Animation<Matrix4>? _animation;
|
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 Offset _doubleTapLocalPosition;
|
||||||
|
|
||||||
late final RxInt currentIndex = widget.initIndex.obs;
|
late final RxInt currentIndex = widget.initIndex.obs;
|
||||||
|
|
||||||
late final int _quality = Pref.previewQ;
|
late final int _quality = Pref.previewQ;
|
||||||
|
|
||||||
|
late final RxBool _hasScaled = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -134,57 +133,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
super.dispose();
|
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) {
|
void _onScaleChanged(double scale) {
|
||||||
final bool initialScale = scale <= widget.minScale;
|
_hasScaled.value = scale > 1.0;
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPlay(String liveUrl) {
|
void _onPlay(String liveUrl) {
|
||||||
@@ -229,32 +179,21 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final width = MediaQuery.widthOf(context);
|
||||||
return Stack(
|
return Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
InteractiveViewerBoundary(
|
InteractiveViewerBoundary(
|
||||||
controller: _transformationController,
|
controller: _transformationController,
|
||||||
boundaryWidth: MediaQuery.widthOf(context),
|
boundaryWidth: width,
|
||||||
onScaleChanged: _onScaleChanged,
|
|
||||||
onLeftBoundaryHit: _onLeftBoundaryHit,
|
|
||||||
onRightBoundaryHit: _onRightBoundaryHit,
|
|
||||||
onNoBoundaryHit: _onNoBoundaryHit,
|
|
||||||
maxScale: widget.maxScale,
|
maxScale: widget.maxScale,
|
||||||
minScale: widget.minScale,
|
minScale: widget.minScale,
|
||||||
onDismissed: Get.back,
|
onDismissed: Get.back,
|
||||||
onReset: () {
|
onInteractionEnd: (_) =>
|
||||||
if (!_enablePageView) {
|
_onScaleChanged(_transformationController.value.row0[0]),
|
||||||
setState(() {
|
|
||||||
_enablePageView = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
onPageChanged: _onPageChanged,
|
onPageChanged: _onPageChanged,
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
physics: _enablePageView
|
|
||||||
? null
|
|
||||||
: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: widget.sources.length,
|
itemCount: widget.sources.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final item = widget.sources[index];
|
final item = widget.sources[index];
|
||||||
@@ -283,38 +222,44 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
context,
|
context,
|
||||||
index,
|
index,
|
||||||
index == currentIndex.value,
|
index == currentIndex.value,
|
||||||
_enablePageView,
|
|
||||||
)
|
)
|
||||||
: _itemBuilder(index, item),
|
: _itemBuilder(index, item),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
horizontalDragGestureRecognizer:
|
||||||
|
ImageHorizontalDragGestureRecognizer(
|
||||||
|
width: width,
|
||||||
|
transformationController: _transformationController,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Container(
|
child: Obx(
|
||||||
padding:
|
() => Container(
|
||||||
MediaQuery.viewPaddingOf(context) +
|
padding:
|
||||||
const EdgeInsets.fromLTRB(12, 8, 20, 8),
|
MediaQuery.viewPaddingOf(context) +
|
||||||
decoration: _enablePageView
|
const EdgeInsets.fromLTRB(12, 8, 20, 8),
|
||||||
? BoxDecoration(
|
decoration: _hasScaled.value
|
||||||
gradient: LinearGradient(
|
? null
|
||||||
begin: Alignment.topCenter,
|
: BoxDecoration(
|
||||||
end: Alignment.bottomCenter,
|
gradient: LinearGradient(
|
||||||
colors: [
|
begin: Alignment.topCenter,
|
||||||
Colors.transparent,
|
end: Alignment.bottomCenter,
|
||||||
Colors.black.withValues(alpha: 0.3),
|
colors: [
|
||||||
],
|
Colors.transparent,
|
||||||
|
Colors.black.withValues(alpha: 0.3),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
alignment: Alignment.center,
|
||||||
: null,
|
child: Obx(
|
||||||
alignment: Alignment.center,
|
() => Text(
|
||||||
child: Obx(
|
"${currentIndex.value + 1}/${widget.sources.length}",
|
||||||
() => Text(
|
style: const TextStyle(color: Colors.white),
|
||||||
"${currentIndex.value + 1}/${widget.sources.length}",
|
),
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user