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; Offset? _initialPosition;
final double width; double width;
final TransformationController transformationController; final TransformationController transformationController;
@override @override

View File

@@ -1,5 +1,8 @@
import 'dart:ui' as ui;
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart' import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart'
as custom; as custom;
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// https://github.com/qq326646683/interactiveviewer_gallery /// https://github.com/qq326646683/interactiveviewer_gallery
@@ -53,15 +56,16 @@ class InteractiveViewerBoundary extends StatefulWidget {
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary> class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late TransformationController _controller; late final TransformationController _controller;
late AnimationController _animateController; late final AnimationController _animateController;
late Animation<Offset> _slideAnimation; late final Animation<Decoration> _opacityAnimation;
late Animation<double> _scaleAnimation; double dx = 0, dy = 0;
late Animation<Decoration> _opacityAnimation;
Offset _offset = Offset.zero; Offset _offset = Offset.zero;
bool _dragging = false; bool _dragging = false;
late Size _size;
bool get _isActive => _dragging || _animateController.isAnimating; bool get _isActive => _dragging || _animateController.isAnimating;
@override @override
@@ -74,35 +78,42 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
vsync: this, vsync: this,
); );
_updateMoveAnimation();
_scaleAnimation = _animateController.drive(
Tween<double>(begin: 1.0, end: 0.25),
);
_opacityAnimation = _animateController.drive( _opacityAnimation = _animateController.drive(
DecorationTween( DecorationTween(
begin: const BoxDecoration(color: Colors.black), begin: const BoxDecoration(color: Colors.black),
end: const BoxDecoration(color: Colors.transparent), end: const BoxDecoration(color: Colors.transparent),
), ),
); );
_animateController.addListener(_updateTransformation);
} }
@override void _updateTransformation() {
void dispose() { final val = _animateController.value;
_animateController.dispose(); final scale = ui.lerpDouble(1.0, 0.25, val)!;
super.dispose();
// 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() { void _updateMoveAnimation() {
final double endX = _offset.dx.sign * (_offset.dx.abs() / _offset.dy.abs()); dy = _offset.dy.sign;
final double endY = _offset.dy.sign; if (dy == 0) {
_slideAnimation = _animateController.drive( dx = 0;
Tween<Offset>( } else {
begin: Offset.zero, dx = _offset.dx / _offset.dy.abs();
end: Offset(endX, endY), }
),
);
} }
void _handleDragStart(ScaleStartDetails details) { void _handleDragStart(ScaleStartDetails details) {
@@ -114,7 +125,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
_offset = Offset.zero; _offset = Offset.zero;
_animateController.value = 0.0; _animateController.value = 0.0;
} }
setState(_updateMoveAnimation); _updateMoveAnimation();
} }
void _handleDragUpdate(ScaleUpdateDetails details) { void _handleDragUpdate(ScaleUpdateDetails details) {
@@ -123,11 +134,10 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
} }
_offset += details.focalPointDelta; _offset += details.focalPointDelta;
_updateMoveAnimation();
setState(_updateMoveAnimation);
if (!_animateController.isAnimating) { 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( @override
decoration: _opacityAnimation, void dispose() {
child: SlideTransition( _animateController
position: _slideAnimation, ..removeListener(_updateTransformation)
child: ScaleTransition( ..dispose();
scale: _scaleAnimation, super.dispose();
child: widget.child, }
),
),
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return custom.InteractiveViewer( return LayoutSizeWidget(
maxScale: widget.maxScale, onPerformLayout: (size) => _size = size,
minScale: widget.minScale, child: DecoratedBoxTransition(
transformationController: _controller, decoration: _opacityAnimation,
onPanStart: _handleDragStart, child: custom.InteractiveViewer(
onPanUpdate: _handleDragUpdate, maxScale: widget.maxScale,
onPanEnd: _handleDragEnd, minScale: widget.minScale,
onInteractionEnd: widget.onInteractionEnd, transformationController: _controller,
isAnimating: () => _animateController.value != 0, onPanStart: _handleDragStart,
child: content, 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 /// The controller to animate the transformation value of the
/// [InteractiveViewer] when it should reset. /// [InteractiveViewer] when it should reset.
late AnimationController _animationController; 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 Offset _doubleTapLocalPosition;
late double _width;
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;
@@ -113,7 +123,16 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
} }
void listener() { 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 @override
@@ -125,6 +144,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
..removeListener(listener) ..removeListener(listener)
..dispose(); ..dispose();
_transformationController.dispose(); _transformationController.dispose();
_horizontalDragGestureRecognizer.dispose();
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) {
@@ -159,13 +179,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
if (_transformationController.value != Matrix4.identity()) { if (_transformationController.value != Matrix4.identity()) {
// animate the reset for the transformation of the interactive viewer // animate the reset for the transformation of the interactive viewer
_animation = _animationController.drive( _tween
Matrix4Tween( ..begin = _transformationController.value.clone()
begin: _transformationController.value, ..end = Matrix4.identity();
end: Matrix4.identity(),
).chain(CurveTween(curve: Curves.easeOut)),
);
_animationController.forward(from: 0); _animationController.forward(from: 0);
} }
} }
@@ -181,13 +197,12 @@ 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: width, boundaryWidth: _width,
maxScale: widget.maxScale, maxScale: widget.maxScale,
minScale: widget.minScale, minScale: widget.minScale,
onDismissed: Get.back, onDismissed: Get.back,
@@ -231,11 +246,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
: _itemBuilder(index, item), : _itemBuilder(index, item),
); );
}, },
horizontalDragGestureRecognizer: horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer,
ImageHorizontalDragGestureRecognizer(
width: width,
transformationController: _transformationController,
),
), ),
), ),
Positioned( Positioned(
@@ -315,8 +326,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
} }
void onDoubleTap() { void onDoubleTap() {
Matrix4 matrix = _transformationController.value.clone(); final matrix = _transformationController.value.clone();
double currentScale = matrix.storage[0]; final currentScale = matrix.storage[0];
double targetScale = widget.minScale; double targetScale = widget.minScale;
@@ -324,38 +335,26 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
targetScale = widget.maxScale * 0.4; targetScale = widget.maxScale * 0.4;
} }
double offSetX = targetScale == 1.0 final double dx, dy;
? 0.0 if (targetScale == 1.0) {
: -_doubleTapLocalPosition.dx * (targetScale - 1); dx = dy = 0;
double offSetY = targetScale == 1.0 } else {
? 0.0 final tmp = 1 - targetScale;
: -_doubleTapLocalPosition.dy * (targetScale - 1); dx = _doubleTapLocalPosition.dx * tmp;
dy = _doubleTapLocalPosition.dy * tmp;
}
matrix = Matrix4.fromList([ matrix
targetScale, ..[0] = targetScale
matrix.row1.x, ..[5] = targetScale
matrix.row2.x, ..[10] = targetScale
matrix.row3.x, ..[12] = dx
matrix.row0.y, ..[13] = dy;
targetScale,
matrix.row2.y, _tween
matrix.row3.y, ..begin = _transformationController.value.clone()
matrix.row0.z, ..end = matrix;
matrix.row1.z,
targetScale,
matrix.row3.z,
offSetX,
offSetY,
matrix.row2.w,
matrix.row3.w,
]);
_animation = _animationController.drive(
Matrix4Tween(
begin: _transformationController.value,
end: matrix,
).chain(CurveTween(curve: Curves.easeOut)),
);
_animationController _animationController
.forward(from: 0) .forward(from: 0)
.whenComplete(() => _onScaleChanged(targetScale)); .whenComplete(() => _onScaleChanged(targetScale));

View File

@@ -15,17 +15,20 @@ class OnlyLayoutWidget extends SingleChildRenderObjectWidget {
@override @override
RenderObject createRenderObject(BuildContext context) => RenderObject createRenderObject(BuildContext context) =>
Layout(onPerformLayout: onPerformLayout); NoRenderLayoutBox(onPerformLayout: onPerformLayout);
@override @override
void updateRenderObject(BuildContext context, Layout renderObject) { void updateRenderObject(
BuildContext context,
NoRenderLayoutBox renderObject,
) {
super.updateRenderObject(context, renderObject); super.updateRenderObject(context, renderObject);
renderObject.onPerformLayout = onPerformLayout; renderObject.onPerformLayout = onPerformLayout;
} }
} }
class Layout extends RenderProxyBox { class NoRenderLayoutBox extends RenderProxyBox {
Layout({required this.onPerformLayout}); NoRenderLayoutBox({required this.onPerformLayout});
LayoutCallback onPerformLayout; LayoutCallback onPerformLayout;
@@ -40,3 +43,42 @@ class Layout extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) {} 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);
}
}
}

View File

@@ -48,9 +48,7 @@ class AppBarAni extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SlideTransition( return SlideTransition(
position: isTop position: controller.drive(isTop ? _topPos : _bottomPos),
? controller.drive(_topPos)
: controller.drive(_bottomPos),
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: isTop ? _topDecoration : _bottomDecoration, gradient: isTop ? _topDecoration : _bottomDecoration,

View File

@@ -575,24 +575,17 @@ abstract final class PageUtils {
List<SourceModel> imgList, List<SourceModel> imgList,
int index, int index,
) { ) {
final animController = AnimationController(
vsync: state,
duration: Duration.zero,
reverseDuration: Duration.zero,
);
state.showBottomSheet( state.showBottomSheet(
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
(context) => InteractiveviewerGallery( (context) => InteractiveviewerGallery(
sources: imgList, sources: imgList,
initIndex: index, initIndex: index,
quality: GlobalData().imgQuality, quality: GlobalData().imgQuality,
onClose: animController.dispose,
), ),
enableDrag: false, enableDrag: false,
elevation: 0.0, elevation: 0.0,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
transitionAnimationController: animController, sheetAnimationStyle: AnimationStyle.noAnimation,
sheetAnimationStyle: const AnimationStyle(duration: Duration.zero),
); );
} }