opt: image viewer (#1837)

* opt: image

* opt: MatrixTransition

* update


---------

Co-authored-by: dom <githubaccount56556@proton.me>
This commit is contained in:
My-Responsitories
2026-02-15 17:13:55 +08:00
committed by GitHub
parent 9c7c6f9e4e
commit 85292a3df2
3 changed files with 101 additions and 88 deletions

View File

@@ -16,7 +16,6 @@
*/ */
import 'dart:io' show File, Platform; 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/flutter/page/page_view.dart';
import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.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/image_viewer/viewer.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/models/common/image_preview_type.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/extension/string_ext.dart';
import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart';
@@ -70,7 +70,7 @@ class _GalleryViewerState extends State<GalleryViewer>
late Size _containerSize; late Size _containerSize;
late final int _quality; late final int _quality;
late final RxInt _currIndex; late final RxInt _currIndex;
late final List<GlobalKey> _keys; GlobalKey? _key;
Player? _player; Player? _player;
Player get _effectivePlayer => _player ??= Player(); Player get _effectivePlayer => _player ??= Player();
@@ -85,7 +85,6 @@ class _GalleryViewerState extends State<GalleryViewer>
_horizontalDragGestureRecognizer; _horizontalDragGestureRecognizer;
late final LongPressGestureRecognizer _longPressGestureRecognizer; late final LongPressGestureRecognizer _longPressGestureRecognizer;
final Rx<Matrix4> _matrix = Rx(Matrix4.identity());
late final AnimationController _animateController; late final AnimationController _animateController;
late final Animation<Decoration> _opacityAnimation; late final Animation<Decoration> _opacityAnimation;
double dx = 0, dy = 0; double dx = 0, dy = 0;
@@ -106,8 +105,13 @@ class _GalleryViewerState extends State<GalleryViewer>
super.initState(); super.initState();
_quality = Pref.previewQ; _quality = Pref.previewQ;
_currIndex = widget.initIndex.obs; _currIndex = widget.initIndex.obs;
_playIfNeeded(widget.initIndex); final item = widget.sources[widget.initIndex];
_keys = List.generate(widget.sources.length, (_) => GlobalKey()); _playIfNeeded(item);
if (!item.isLongPic) {
_key = GlobalKey();
WidgetsBinding.instance.addPostFrameCallback((_) => _key = null);
}
_pageController = PageController(initialPage: widget.initIndex); _pageController = PageController(initialPage: widget.initIndex);
@@ -134,27 +138,23 @@ class _GalleryViewerState extends State<GalleryViewer>
end: const BoxDecoration(color: Colors.transparent), end: const BoxDecoration(color: Colors.transparent),
), ),
); );
_animateController.addListener(_updateTransformation);
} }
void _updateTransformation() { Matrix4 _onTransform(double val) {
final val = _animateController.value; final scale = val.lerp(1.0, 0.25);
final scale = ui.lerpDouble(1.0, 0.25, val)!;
// Matrix4.identity() // Matrix4.identity()
// ..translateByDouble(size.width / 2, size.height / 2, 0, 1) // ..translateByDouble(size.width / 2, size.height / 2, 0, 1)
// ..translateByDouble(size.width * val * dx, size.height * val * dy, 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); // ..translateByDouble(-size.width / 2, -size.height / 2, 0, 1);
final tmp = (1.0 - scale) / 2.0; final tmp = (1.0 - scale) / 2.0;
_matrix.value = Matrix4.diagonal3Values(scale, scale, scale) return Matrix4.diagonal3Values(scale, scale, scale)..setTranslationRaw(
..setTranslationRaw( _containerSize.width * (val * dx + tmp),
_containerSize.width * (val * dx + tmp), _containerSize.height * (val * dy + tmp),
_containerSize.height * (val * dy + tmp), 0,
0, );
);
} }
void _updateMoveAnimation() { void _updateMoveAnimation() {
@@ -217,13 +217,10 @@ class _GalleryViewerState extends State<GalleryViewer>
_player = null; _player = null;
_videoController = null; _videoController = null;
_pageController.dispose(); _pageController.dispose();
_animateController _animateController.dispose();
..removeListener(_updateTransformation)
..dispose();
_tapGestureRecognizer.dispose(); _tapGestureRecognizer.dispose();
_longPressGestureRecognizer.dispose(); _longPressGestureRecognizer.dispose();
_currIndex.close(); _currIndex.close();
_matrix.close();
if (widget.quality != _quality) { if (widget.quality != _quality) {
for (final item in widget.sources) { for (final item in widget.sources) {
if (item.sourceType == SourceType.networkImage) { if (item.sourceType == SourceType.networkImage) {
@@ -252,21 +249,20 @@ class _GalleryViewerState extends State<GalleryViewer>
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
_containerSize = constraints.biggest; _containerSize = constraints.biggest;
return Obx( return MatrixTransition(
() => Transform( alignment: .topLeft,
transform: _matrix.value, animation: _animateController,
child: onTransform: _onTransform,
PageView<ImageHorizontalDragGestureRecognizer>.builder( child: PageView<ImageHorizontalDragGestureRecognizer>.builder(
controller: _pageController, controller: _pageController,
onPageChanged: _onPageChanged, onPageChanged: _onPageChanged,
physics: const CustomTabBarViewScrollPhysics( physics: const CustomTabBarViewScrollPhysics(
parent: AlwaysScrollableScrollPhysics(), parent: AlwaysScrollableScrollPhysics(),
), ),
itemCount: widget.sources.length, itemCount: widget.sources.length,
itemBuilder: _itemBuilder, itemBuilder: _itemBuilder,
horizontalDragGestureRecognizer: () => horizontalDragGestureRecognizer: () =>
_horizontalDragGestureRecognizer, _horizontalDragGestureRecognizer,
),
), ),
); );
}, },
@@ -308,8 +304,7 @@ class _GalleryViewerState extends State<GalleryViewer>
), ),
); );
void _playIfNeeded(int index) { void _playIfNeeded(SourceModel item) {
final item = widget.sources[index];
if (item.sourceType == .livePhoto) { if (item.sourceType == .livePhoto) {
_effectivePlayer.open(Media(item.liveUrl!)); _effectivePlayer.open(Media(item.liveUrl!));
} }
@@ -317,7 +312,7 @@ class _GalleryViewerState extends State<GalleryViewer>
void _onPageChanged(int index) { void _onPageChanged(int index) {
_player?.pause(); _player?.pause();
_playIfNeeded(index); _playIfNeeded(widget.sources[index]);
_currIndex.value = index; _currIndex.value = index;
} }
@@ -340,11 +335,11 @@ class _GalleryViewerState extends State<GalleryViewer>
Widget _itemBuilder(BuildContext context, int index) { Widget _itemBuilder(BuildContext context, int index) {
final item = widget.sources[index]; final item = widget.sources[index];
Widget child; final Widget child;
switch (item.sourceType) { switch (item.sourceType) {
case SourceType.fileImage: case SourceType.fileImage:
child = Image.file( child = Image.file(
key: _keys[index], key: _key,
File(item.url), File(item.url),
filterQuality: .low, filterQuality: .low,
minScale: widget.minScale, minScale: widget.minScale,
@@ -360,7 +355,7 @@ class _GalleryViewerState extends State<GalleryViewer>
case SourceType.networkImage: case SourceType.networkImage:
final isLongPic = item.isLongPic; final isLongPic = item.isLongPic;
child = Image( child = Image(
key: _keys[index], key: _key,
image: CachedNetworkImageProvider(_getActualUrl(item.url)), image: CachedNetworkImageProvider(_getActualUrl(item.url)),
minScale: widget.minScale, minScale: widget.minScale,
maxScale: widget.maxScale, maxScale: widget.maxScale,
@@ -414,7 +409,7 @@ class _GalleryViewerState extends State<GalleryViewer>
} }
case SourceType.livePhoto: case SourceType.livePhoto:
child = Obx( child = Obx(
key: _keys[index], key: _key,
() => _currIndex.value == index () => _currIndex.value == index
? Viewer( ? Viewer(
minScale: widget.minScale, minScale: widget.minScale,

View File

@@ -99,13 +99,18 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
vsync: this, vsync: this,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
)..addListener(_listener); )..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() { void _listener() {
final storage = _animatable.evaluate(_effectiveAnimationController); final t = Curves.easeOut.transform(_effectiveAnimationController.value);
_scale = storage[0]; _scale = t.lerp(_scaleFrom, _scaleTo);
_position = Offset(storage[12], storage[13]); _position = Offset.lerp(_positionFrom, _positionTo, t)!;
setState(() {}); setState(() {});
} }
@@ -178,24 +183,29 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
Offset _clampPosition(Offset offset, double scale) { Offset _clampPosition(Offset offset, double scale) {
final containerSize = widget.containerSize; final containerSize = widget.containerSize;
final containerWidth = containerSize.width;
final containerHeight = containerSize.height;
final imageWidth = _imageSize.width * scale; final imageWidth = _imageSize.width * scale;
final imageHeight = _imageSize.height * scale; final imageHeight = _imageSize.height * scale;
final dx = (1 - scale) * containerWidth / 2; final center = containerSize * (1 - scale) / 2;
final dxOffset = (imageWidth - containerWidth) / 2;
final dy = (1 - scale) * containerHeight / 2; final dxOffset = (imageWidth - containerSize.width) / 2;
final dyOffset = (imageHeight - containerHeight) / 2; final dyOffset = (imageHeight - containerSize.height) / 2;
return Offset( return Offset(
imageWidth > containerWidth imageWidth > containerSize.width
? clampDouble(offset.dx, dx - dxOffset, dx + dxOffset) ? clampDouble(
: dx, offset.dx,
imageHeight > containerHeight center.width - dxOffset,
? clampDouble(offset.dy, dy - dyOffset, dy + dyOffset) center.width + dxOffset,
: dy, )
: center.width,
imageHeight > containerSize.height
? clampDouble(
offset.dy,
center.height - dyOffset,
center.height + dyOffset,
)
: center.height,
); );
} }
@@ -219,9 +229,9 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
} }
void _handleDoubleTap() { void _handleDoubleTap() {
final begin = Matrix4.identity() if (_effectiveAnimationController.isAnimating) return;
..translateByDouble(_position.dx, _position.dy, 0.0, 1.0) _scaleFrom = _scale;
..scaleByDouble(_scale, _scale, _scale, 1.0); _positionFrom = _position;
double endScale; double endScale;
if (_scale == widget.minScale) { if (_scale == widget.minScale) {
@@ -233,16 +243,13 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
endScale = widget.minScale; endScale = widget.minScale;
} }
final position = _clampPosition( final position = _clampPosition(
(_downPos! * (_scale - endScale) + _position * endScale) / _scale, Offset.lerp(_downPos!, _position, endScale / _scale)!,
endScale, endScale,
); );
final end = Matrix4.identity()
..translateByDouble(position.dx, position.dy, 0.0, 1.0)
..scaleByDouble(endScale, endScale, endScale, 1.0);
_tween _scaleTo = endScale;
..begin = begin _positionTo = position;
..end = end;
_effectiveAnimationController _effectiveAnimationController
..duration = const Duration(milliseconds: 300) ..duration = const Duration(milliseconds: 300)
..forward(from: 0); ..forward(from: 0);
@@ -254,10 +261,12 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
final imageHeight = _scale * _imageSize.height; final imageHeight = _scale * _imageSize.height;
final containerHeight = widget.containerSize.height; final containerHeight = widget.containerSize.height;
if (_scalePos != null && if (_scalePos != null &&
(_round(_position.dy) == (_position.dy.equals(
_round((imageHeight - _scale * containerHeight) / 2) && (imageHeight - _scale * containerHeight) / 2,
1e-6,
) &&
details.focalPoint.dy > _scalePos!.dy) || details.focalPoint.dy > _scalePos!.dy) ||
(_round(_position.dy) == _round(containerHeight - imageHeight) && (_position.dy.equals(containerHeight - imageHeight, 1e-6) &&
details.focalPoint.dy < _scalePos!.dy)) { details.focalPoint.dy < _scalePos!.dy)) {
_gestureType = .drag; _gestureType = .drag;
widget.onDragStart?.call(details); widget.onDragStart?.call(details);
@@ -328,13 +337,11 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
Offset(frictionSimulationX.finalX, frictionSimulationY.finalX), Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
_scale, _scale,
); );
_tween
..begin = (Matrix4.identity() _scaleFrom = _scaleTo = _scale;
..translateByDouble(_position.dx, _position.dy, 0.0, 1.0) _positionFrom = _position;
..scaleByDouble(_scale, _scale, _scale, 1.0)) _positionTo = position;
..end = (Matrix4.identity()
..translateByDouble(position.dx, position.dy, 0.0, 1.0)
..scaleByDouble(_scale, _scale, _scale, 1.0));
_effectiveAnimationController _effectiveAnimationController
..duration = Duration(milliseconds: (tFinal * 1000).round()) ..duration = Duration(milliseconds: (tFinal * 1000).round())
..forward(from: 0); ..forward(from: 0);
@@ -373,9 +380,6 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { 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( return Listener(
behavior: .opaque, behavior: .opaque,
onPointerDown: _onPointerDown, onPointerDown: _onPointerDown,
@@ -383,7 +387,7 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
onPointerSignal: _onPointerSignal, onPointerSignal: _onPointerSignal,
child: ClipRRect( child: ClipRRect(
child: Transform( child: Transform(
transform: matrix, transform: _matrix,
child: widget.child, child: widget.child,
), ),
), ),
@@ -419,9 +423,9 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
final dx = (1 - _scale) * containerWidth / 2; final dx = (1 - _scale) * containerWidth / 2;
final dxOffset = (imageWidth - containerWidth) / 2; final dxOffset = (imageWidth - containerWidth) / 2;
if (initialPosition.dx < lastPosition.global.dx) { if (initialPosition.dx < lastPosition.global.dx) {
return _round(_position.dx) == _round(dx + dxOffset); return _position.dx.equals(dx + dxOffset);
} else { } else {
return _round(_position.dx) == _round(dx - dxOffset); return _position.dx.equals(dx - dxOffset);
} }
} }
@@ -447,8 +451,6 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
} }
} }
double _round(double value) => value.toPrecision(6);
enum _GestureType { pan, scale, drag } enum _GestureType { pan, scale, drag }
double _getFinalTime( double _getFinalTime(

View File

@@ -21,4 +21,20 @@ extension DoubleExt on double {
final mod = pow(10, fractionDigits).toDouble(); final mod = pow(10, fractionDigits).toDouble();
return (this * mod).roundToDouble() / mod; 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;
}
} }