diff --git a/lib/common/widgets/image_viewer/gallery_viewer.dart b/lib/common/widgets/image_viewer/gallery_viewer.dart index b388c9ddc..bb97f2f24 100644 --- a/lib/common/widgets/image_viewer/gallery_viewer.dart +++ b/lib/common/widgets/image_viewer/gallery_viewer.dart @@ -16,7 +16,6 @@ */ import 'dart:io' show File, Platform; -import 'dart:ui' as ui; import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; @@ -26,6 +25,7 @@ import 'package:PiliPlus/common/widgets/image_viewer/loading_indicator.dart'; import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; +import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -70,7 +70,7 @@ class _GalleryViewerState extends State late Size _containerSize; late final int _quality; late final RxInt _currIndex; - late final List _keys; + GlobalKey? _key; Player? _player; Player get _effectivePlayer => _player ??= Player(); @@ -85,7 +85,6 @@ class _GalleryViewerState extends State _horizontalDragGestureRecognizer; late final LongPressGestureRecognizer _longPressGestureRecognizer; - final Rx _matrix = Rx(Matrix4.identity()); late final AnimationController _animateController; late final Animation _opacityAnimation; double dx = 0, dy = 0; @@ -106,8 +105,13 @@ class _GalleryViewerState extends State super.initState(); _quality = Pref.previewQ; _currIndex = widget.initIndex.obs; - _playIfNeeded(widget.initIndex); - _keys = List.generate(widget.sources.length, (_) => GlobalKey()); + final item = widget.sources[widget.initIndex]; + _playIfNeeded(item); + + if (!item.isLongPic) { + _key = GlobalKey(); + WidgetsBinding.instance.addPostFrameCallback((_) => _key = null); + } _pageController = PageController(initialPage: widget.initIndex); @@ -134,27 +138,23 @@ class _GalleryViewerState extends State end: const BoxDecoration(color: Colors.transparent), ), ); - - _animateController.addListener(_updateTransformation); } - void _updateTransformation() { - final val = _animateController.value; - final scale = ui.lerpDouble(1.0, 0.25, val)!; + Matrix4 _onTransform(double val) { + final scale = val.lerp(1.0, 0.25); // Matrix4.identity() // ..translateByDouble(size.width / 2, size.height / 2, 0, 1) // ..translateByDouble(size.width * val * dx, size.height * val * dy, 0, 1) - // ..scaleByDouble(scale, scale, 1, 1) + // ..scaleByDouble(scale, scale, scale, 1) // ..translateByDouble(-size.width / 2, -size.height / 2, 0, 1); final tmp = (1.0 - scale) / 2.0; - _matrix.value = Matrix4.diagonal3Values(scale, scale, scale) - ..setTranslationRaw( - _containerSize.width * (val * dx + tmp), - _containerSize.height * (val * dy + tmp), - 0, - ); + return Matrix4.diagonal3Values(scale, scale, scale)..setTranslationRaw( + _containerSize.width * (val * dx + tmp), + _containerSize.height * (val * dy + tmp), + 0, + ); } void _updateMoveAnimation() { @@ -217,13 +217,10 @@ class _GalleryViewerState extends State _player = null; _videoController = null; _pageController.dispose(); - _animateController - ..removeListener(_updateTransformation) - ..dispose(); + _animateController.dispose(); _tapGestureRecognizer.dispose(); _longPressGestureRecognizer.dispose(); _currIndex.close(); - _matrix.close(); if (widget.quality != _quality) { for (final item in widget.sources) { if (item.sourceType == SourceType.networkImage) { @@ -252,21 +249,20 @@ class _GalleryViewerState extends State LayoutBuilder( builder: (context, constraints) { _containerSize = constraints.biggest; - return Obx( - () => Transform( - transform: _matrix.value, - child: - PageView.builder( - controller: _pageController, - onPageChanged: _onPageChanged, - physics: const CustomTabBarViewScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - itemCount: widget.sources.length, - itemBuilder: _itemBuilder, - horizontalDragGestureRecognizer: () => - _horizontalDragGestureRecognizer, - ), + return MatrixTransition( + alignment: .topLeft, + animation: _animateController, + onTransform: _onTransform, + child: PageView.builder( + controller: _pageController, + onPageChanged: _onPageChanged, + physics: const CustomTabBarViewScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + itemCount: widget.sources.length, + itemBuilder: _itemBuilder, + horizontalDragGestureRecognizer: () => + _horizontalDragGestureRecognizer, ), ); }, @@ -308,8 +304,7 @@ class _GalleryViewerState extends State ), ); - void _playIfNeeded(int index) { - final item = widget.sources[index]; + void _playIfNeeded(SourceModel item) { if (item.sourceType == .livePhoto) { _effectivePlayer.open(Media(item.liveUrl!)); } @@ -317,7 +312,7 @@ class _GalleryViewerState extends State void _onPageChanged(int index) { _player?.pause(); - _playIfNeeded(index); + _playIfNeeded(widget.sources[index]); _currIndex.value = index; } @@ -340,11 +335,11 @@ class _GalleryViewerState extends State Widget _itemBuilder(BuildContext context, int index) { final item = widget.sources[index]; - Widget child; + final Widget child; switch (item.sourceType) { case SourceType.fileImage: child = Image.file( - key: _keys[index], + key: _key, File(item.url), filterQuality: .low, minScale: widget.minScale, @@ -360,7 +355,7 @@ class _GalleryViewerState extends State case SourceType.networkImage: final isLongPic = item.isLongPic; child = Image( - key: _keys[index], + key: _key, image: CachedNetworkImageProvider(_getActualUrl(item.url)), minScale: widget.minScale, maxScale: widget.maxScale, @@ -414,7 +409,7 @@ class _GalleryViewerState extends State } case SourceType.livePhoto: child = Obx( - key: _keys[index], + key: _key, () => _currIndex.value == index ? Viewer( minScale: widget.minScale, diff --git a/lib/common/widgets/image_viewer/viewer.dart b/lib/common/widgets/image_viewer/viewer.dart index 2915e05cf..da95302b6 100644 --- a/lib/common/widgets/image_viewer/viewer.dart +++ b/lib/common/widgets/image_viewer/viewer.dart @@ -99,13 +99,18 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { vsync: this, duration: const Duration(milliseconds: 300), )..addListener(_listener); - late final _tween = Matrix4Tween(); - late final _animatable = _tween.chain(CurveTween(curve: Curves.easeOut)); + + late double _scaleFrom, _scaleTo; + late Offset _positionFrom, _positionTo; + + Matrix4 get _matrix => + Matrix4.translationValues(_position.dx, _position.dy, 0.0) + ..scaleByDouble(_scale, _scale, _scale, 1.0); void _listener() { - final storage = _animatable.evaluate(_effectiveAnimationController); - _scale = storage[0]; - _position = Offset(storage[12], storage[13]); + final t = Curves.easeOut.transform(_effectiveAnimationController.value); + _scale = t.lerp(_scaleFrom, _scaleTo); + _position = Offset.lerp(_positionFrom, _positionTo, t)!; setState(() {}); } @@ -178,24 +183,29 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { Offset _clampPosition(Offset offset, double scale) { final containerSize = widget.containerSize; - final containerWidth = containerSize.width; - final containerHeight = containerSize.height; final imageWidth = _imageSize.width * scale; final imageHeight = _imageSize.height * scale; - final dx = (1 - scale) * containerWidth / 2; - final dxOffset = (imageWidth - containerWidth) / 2; + final center = containerSize * (1 - scale) / 2; - final dy = (1 - scale) * containerHeight / 2; - final dyOffset = (imageHeight - containerHeight) / 2; + final dxOffset = (imageWidth - containerSize.width) / 2; + final dyOffset = (imageHeight - containerSize.height) / 2; return Offset( - imageWidth > containerWidth - ? clampDouble(offset.dx, dx - dxOffset, dx + dxOffset) - : dx, - imageHeight > containerHeight - ? clampDouble(offset.dy, dy - dyOffset, dy + dyOffset) - : dy, + imageWidth > containerSize.width + ? clampDouble( + offset.dx, + center.width - dxOffset, + center.width + dxOffset, + ) + : center.width, + imageHeight > containerSize.height + ? clampDouble( + offset.dy, + center.height - dyOffset, + center.height + dyOffset, + ) + : center.height, ); } @@ -219,9 +229,9 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { } void _handleDoubleTap() { - final begin = Matrix4.identity() - ..translateByDouble(_position.dx, _position.dy, 0.0, 1.0) - ..scaleByDouble(_scale, _scale, _scale, 1.0); + if (_effectiveAnimationController.isAnimating) return; + _scaleFrom = _scale; + _positionFrom = _position; double endScale; if (_scale == widget.minScale) { @@ -233,16 +243,13 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { endScale = widget.minScale; } final position = _clampPosition( - (_downPos! * (_scale - endScale) + _position * endScale) / _scale, + Offset.lerp(_downPos!, _position, endScale / _scale)!, endScale, ); - final end = Matrix4.identity() - ..translateByDouble(position.dx, position.dy, 0.0, 1.0) - ..scaleByDouble(endScale, endScale, endScale, 1.0); - _tween - ..begin = begin - ..end = end; + _scaleTo = endScale; + _positionTo = position; + _effectiveAnimationController ..duration = const Duration(milliseconds: 300) ..forward(from: 0); @@ -254,10 +261,12 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { final imageHeight = _scale * _imageSize.height; final containerHeight = widget.containerSize.height; if (_scalePos != null && - (_round(_position.dy) == - _round((imageHeight - _scale * containerHeight) / 2) && + (_position.dy.equals( + (imageHeight - _scale * containerHeight) / 2, + 1e-6, + ) && details.focalPoint.dy > _scalePos!.dy) || - (_round(_position.dy) == _round(containerHeight - imageHeight) && + (_position.dy.equals(containerHeight - imageHeight, 1e-6) && details.focalPoint.dy < _scalePos!.dy)) { _gestureType = .drag; widget.onDragStart?.call(details); @@ -328,13 +337,11 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { Offset(frictionSimulationX.finalX, frictionSimulationY.finalX), _scale, ); - _tween - ..begin = (Matrix4.identity() - ..translateByDouble(_position.dx, _position.dy, 0.0, 1.0) - ..scaleByDouble(_scale, _scale, _scale, 1.0)) - ..end = (Matrix4.identity() - ..translateByDouble(position.dx, position.dy, 0.0, 1.0) - ..scaleByDouble(_scale, _scale, _scale, 1.0)); + + _scaleFrom = _scaleTo = _scale; + _positionFrom = _position; + _positionTo = position; + _effectiveAnimationController ..duration = Duration(milliseconds: (tFinal * 1000).round()) ..forward(from: 0); @@ -373,9 +380,6 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { - final matrix = Matrix4.identity() - ..translateByDouble(_position.dx, _position.dy, 0.0, 1.0) - ..scaleByDouble(_scale, _scale, _scale, 1.0); return Listener( behavior: .opaque, onPointerDown: _onPointerDown, @@ -383,7 +387,7 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { onPointerSignal: _onPointerSignal, child: ClipRRect( child: Transform( - transform: matrix, + transform: _matrix, child: widget.child, ), ), @@ -419,9 +423,9 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { final dx = (1 - _scale) * containerWidth / 2; final dxOffset = (imageWidth - containerWidth) / 2; if (initialPosition.dx < lastPosition.global.dx) { - return _round(_position.dx) == _round(dx + dxOffset); + return _position.dx.equals(dx + dxOffset); } else { - return _round(_position.dx) == _round(dx - dxOffset); + return _position.dx.equals(dx - dxOffset); } } @@ -447,8 +451,6 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { } } -double _round(double value) => value.toPrecision(6); - enum _GestureType { pan, scale, drag } double _getFinalTime( diff --git a/lib/utils/extension/num_ext.dart b/lib/utils/extension/num_ext.dart index 5cbf6a6d3..397eb12f7 100644 --- a/lib/utils/extension/num_ext.dart +++ b/lib/utils/extension/num_ext.dart @@ -21,4 +21,20 @@ extension DoubleExt on double { final mod = pow(10, fractionDigits).toDouble(); return (this * mod).roundToDouble() / mod; } + + bool equals(double other, [double epsilon = 1e-10]) => + (this - other).abs() < epsilon; + + double lerp(double a, double b) { + assert( + a.isFinite, + 'Cannot interpolate between finite and non-finite values', + ); + assert( + b.isFinite, + 'Cannot interpolate between finite and non-finite values', + ); + assert(isFinite, 't must be finite when interpolating between values'); + return a * (1.0 - this) + b * this; + } }