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: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<GalleryViewer>
late Size _containerSize;
late final int _quality;
late final RxInt _currIndex;
late final List<GlobalKey> _keys;
GlobalKey? _key;
Player? _player;
Player get _effectivePlayer => _player ??= Player();
@@ -85,7 +85,6 @@ class _GalleryViewerState extends State<GalleryViewer>
_horizontalDragGestureRecognizer;
late final LongPressGestureRecognizer _longPressGestureRecognizer;
final Rx<Matrix4> _matrix = Rx(Matrix4.identity());
late final AnimationController _animateController;
late final Animation<Decoration> _opacityAnimation;
double dx = 0, dy = 0;
@@ -106,8 +105,13 @@ class _GalleryViewerState extends State<GalleryViewer>
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<GalleryViewer>
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<GalleryViewer>
_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<GalleryViewer>
LayoutBuilder(
builder: (context, constraints) {
_containerSize = constraints.biggest;
return Obx(
() => Transform(
transform: _matrix.value,
child:
PageView<ImageHorizontalDragGestureRecognizer>.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<ImageHorizontalDragGestureRecognizer>.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<GalleryViewer>
),
);
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<GalleryViewer>
void _onPageChanged(int index) {
_player?.pause();
_playIfNeeded(index);
_playIfNeeded(widget.sources[index]);
_currIndex.value = index;
}
@@ -340,11 +335,11 @@ class _GalleryViewerState extends State<GalleryViewer>
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<GalleryViewer>
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<GalleryViewer>
}
case SourceType.livePhoto:
child = Obx(
key: _keys[index],
key: _key,
() => _currIndex.value == index
? Viewer(
minScale: widget.minScale,

View File

@@ -99,13 +99,18 @@ class _ViewerState extends State<Viewer> 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<Viewer> 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<Viewer> 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<Viewer> 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<Viewer> 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<Viewer> 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<Viewer> 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<Viewer> with SingleTickerProviderStateMixin {
onPointerSignal: _onPointerSignal,
child: ClipRRect(
child: Transform(
transform: matrix,
transform: _matrix,
child: widget.child,
),
),
@@ -419,9 +423,9 @@ class _ViewerState extends State<Viewer> 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<Viewer> with SingleTickerProviderStateMixin {
}
}
double _round(double value) => value.toPrecision(6);
enum _GestureType { pan, scale, drag }
double _getFinalTime(