opt: matrix anim (#1829)

This commit is contained in:
My-Responsitories
2026-02-08 15:27:03 +08:00
committed by GitHub
parent 4ac855d393
commit 8234b7ac92
6 changed files with 156 additions and 111 deletions

View File

@@ -13,7 +13,7 @@ class ImageHorizontalDragGestureRecognizer
Offset? _initialPosition;
final double width;
double width;
final TransformationController transformationController;
@override

View File

@@ -1,5 +1,8 @@
import 'dart:ui' as ui;
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart'
as custom;
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
import 'package:flutter/material.dart';
/// https://github.com/qq326646683/interactiveviewer_gallery
@@ -53,15 +56,16 @@ class InteractiveViewerBoundary extends StatefulWidget {
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
with SingleTickerProviderStateMixin {
late TransformationController _controller;
late AnimationController _animateController;
late Animation<Offset> _slideAnimation;
late Animation<double> _scaleAnimation;
late Animation<Decoration> _opacityAnimation;
late final TransformationController _controller;
late final AnimationController _animateController;
late final Animation<Decoration> _opacityAnimation;
double dx = 0, dy = 0;
Offset _offset = Offset.zero;
bool _dragging = false;
late Size _size;
bool get _isActive => _dragging || _animateController.isAnimating;
@override
@@ -74,35 +78,42 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
vsync: this,
);
_updateMoveAnimation();
_scaleAnimation = _animateController.drive(
Tween<double>(begin: 1.0, end: 0.25),
);
_opacityAnimation = _animateController.drive(
DecorationTween(
begin: const BoxDecoration(color: Colors.black),
end: const BoxDecoration(color: Colors.transparent),
),
);
_animateController.addListener(_updateTransformation);
}
@override
void dispose() {
_animateController.dispose();
super.dispose();
void _updateTransformation() {
final val = _animateController.value;
final scale = ui.lerpDouble(1.0, 0.25, val)!;
// 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)
// ..translateByDouble(-size.width / 2, -size.height / 2, 0, 1);
final tmp = (1.0 - scale) / 2.0;
_controller.value = Matrix4.diagonal3Values(scale, scale, scale)
..setTranslationRaw(
_size.width * (val * dx + tmp),
_size.height * (val * dy + tmp),
0,
);
}
void _updateMoveAnimation() {
final double endX = _offset.dx.sign * (_offset.dx.abs() / _offset.dy.abs());
final double endY = _offset.dy.sign;
_slideAnimation = _animateController.drive(
Tween<Offset>(
begin: Offset.zero,
end: Offset(endX, endY),
),
);
dy = _offset.dy.sign;
if (dy == 0) {
dx = 0;
} else {
dx = _offset.dx / _offset.dy.abs();
}
}
void _handleDragStart(ScaleStartDetails details) {
@@ -114,7 +125,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
_offset = Offset.zero;
_animateController.value = 0.0;
}
setState(_updateMoveAnimation);
_updateMoveAnimation();
}
void _handleDragUpdate(ScaleUpdateDetails details) {
@@ -123,11 +134,10 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
}
_offset += details.focalPointDelta;
setState(_updateMoveAnimation);
_updateMoveAnimation();
if (!_animateController.isAnimating) {
_animateController.value = _offset.dy.abs() / context.size!.height;
_animateController.value = _offset.dy.abs() / _size.height;
}
}
@@ -153,29 +163,32 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
}
}
Widget get content => DecoratedBoxTransition(
decoration: _opacityAnimation,
child: SlideTransition(
position: _slideAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: widget.child,
),
),
);
@override
void dispose() {
_animateController
..removeListener(_updateTransformation)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return custom.InteractiveViewer(
maxScale: widget.maxScale,
minScale: widget.minScale,
transformationController: _controller,
onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate,
onPanEnd: _handleDragEnd,
onInteractionEnd: widget.onInteractionEnd,
isAnimating: () => _animateController.value != 0,
child: content,
return LayoutSizeWidget(
onPerformLayout: (size) => _size = size,
child: DecoratedBoxTransition(
decoration: _opacityAnimation,
child: custom.InteractiveViewer(
maxScale: widget.maxScale,
minScale: widget.minScale,
transformationController: _controller,
onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate,
onPanEnd: _handleDragEnd,
onInteractionEnd: widget.onInteractionEnd,
isAnimating: () => _animateController.value != 0,
child: widget.child,
),
),
);
}
}

View File

@@ -83,10 +83,20 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
/// The controller to animate the transformation value of the
/// [InteractiveViewer] when it should reset.
late AnimationController _animationController;
Animation<Matrix4>? _animation;
late final _tween = Matrix4Tween();
late final _animatable = _tween.chain(CurveTween(curve: Curves.easeOut));
late final _horizontalDragGestureRecognizer =
ImageHorizontalDragGestureRecognizer(
width: 0,
transformationController: _transformationController,
);
late Offset _doubleTapLocalPosition;
late double _width;
late final RxInt currentIndex = widget.initIndex.obs;
late final int _quality = Pref.previewQ;
@@ -113,7 +123,16 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
}
void listener() {
_transformationController.value = _animation?.value ?? Matrix4.identity();
_transformationController.value = _animatable.evaluate(
_animationController,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_width = MediaQuery.widthOf(context);
_horizontalDragGestureRecognizer.width = _width;
}
@override
@@ -125,6 +144,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
..removeListener(listener)
..dispose();
_transformationController.dispose();
_horizontalDragGestureRecognizer.dispose();
if (widget.quality != _quality) {
for (final item in widget.sources) {
if (item.sourceType == SourceType.networkImage) {
@@ -159,13 +179,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
if (_transformationController.value != Matrix4.identity()) {
// animate the reset for the transformation of the interactive viewer
_animation = _animationController.drive(
Matrix4Tween(
begin: _transformationController.value,
end: Matrix4.identity(),
).chain(CurveTween(curve: Curves.easeOut)),
);
_tween
..begin = _transformationController.value.clone()
..end = Matrix4.identity();
_animationController.forward(from: 0);
}
}
@@ -181,13 +197,12 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
@override
Widget build(BuildContext context) {
final width = MediaQuery.widthOf(context);
return Stack(
clipBehavior: Clip.none,
children: [
InteractiveViewerBoundary(
controller: _transformationController,
boundaryWidth: width,
boundaryWidth: _width,
maxScale: widget.maxScale,
minScale: widget.minScale,
onDismissed: Get.back,
@@ -231,11 +246,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
: _itemBuilder(index, item),
);
},
horizontalDragGestureRecognizer:
ImageHorizontalDragGestureRecognizer(
width: width,
transformationController: _transformationController,
),
horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer,
),
),
Positioned(
@@ -315,8 +326,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
}
void onDoubleTap() {
Matrix4 matrix = _transformationController.value.clone();
double currentScale = matrix.storage[0];
final matrix = _transformationController.value.clone();
final currentScale = matrix.storage[0];
double targetScale = widget.minScale;
@@ -324,38 +335,26 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
targetScale = widget.maxScale * 0.4;
}
double offSetX = targetScale == 1.0
? 0.0
: -_doubleTapLocalPosition.dx * (targetScale - 1);
double offSetY = targetScale == 1.0
? 0.0
: -_doubleTapLocalPosition.dy * (targetScale - 1);
final double dx, dy;
if (targetScale == 1.0) {
dx = dy = 0;
} else {
final tmp = 1 - targetScale;
dx = _doubleTapLocalPosition.dx * tmp;
dy = _doubleTapLocalPosition.dy * tmp;
}
matrix = Matrix4.fromList([
targetScale,
matrix.row1.x,
matrix.row2.x,
matrix.row3.x,
matrix.row0.y,
targetScale,
matrix.row2.y,
matrix.row3.y,
matrix.row0.z,
matrix.row1.z,
targetScale,
matrix.row3.z,
offSetX,
offSetY,
matrix.row2.w,
matrix.row3.w,
]);
matrix
..[0] = targetScale
..[5] = targetScale
..[10] = targetScale
..[12] = dx
..[13] = dy;
_tween
..begin = _transformationController.value.clone()
..end = matrix;
_animation = _animationController.drive(
Matrix4Tween(
begin: _transformationController.value,
end: matrix,
).chain(CurveTween(curve: Curves.easeOut)),
);
_animationController
.forward(from: 0)
.whenComplete(() => _onScaleChanged(targetScale));

View File

@@ -15,17 +15,20 @@ class OnlyLayoutWidget extends SingleChildRenderObjectWidget {
@override
RenderObject createRenderObject(BuildContext context) =>
Layout(onPerformLayout: onPerformLayout);
NoRenderLayoutBox(onPerformLayout: onPerformLayout);
@override
void updateRenderObject(BuildContext context, Layout renderObject) {
void updateRenderObject(
BuildContext context,
NoRenderLayoutBox renderObject,
) {
super.updateRenderObject(context, renderObject);
renderObject.onPerformLayout = onPerformLayout;
}
}
class Layout extends RenderProxyBox {
Layout({required this.onPerformLayout});
class NoRenderLayoutBox extends RenderProxyBox {
NoRenderLayoutBox({required this.onPerformLayout});
LayoutCallback onPerformLayout;
@@ -40,3 +43,42 @@ class Layout extends RenderProxyBox {
@override
void paint(PaintingContext context, Offset offset) {}
}
class LayoutSizeWidget extends SingleChildRenderObjectWidget {
const LayoutSizeWidget({
super.key,
super.child,
required this.onPerformLayout,
});
final LayoutCallback onPerformLayout;
@override
RenderObject createRenderObject(BuildContext context) =>
RenderLayoutBox(onPerformLayout: onPerformLayout);
@override
void updateRenderObject(
BuildContext context,
RenderLayoutBox renderObject,
) {
super.updateRenderObject(context, renderObject);
renderObject.onPerformLayout = onPerformLayout;
}
}
class RenderLayoutBox extends RenderProxyBox {
RenderLayoutBox({required this.onPerformLayout});
LayoutCallback onPerformLayout;
Size? _size;
@override
void performLayout() {
super.performLayout();
if (_size != size) {
onPerformLayout(_size = size);
}
}
}