From a0d16572c9042e217d7ca2b0f862bf74e9ca534a Mon Sep 17 00:00:00 2001 From: renbin Date: Fri, 9 Aug 2024 17:54:44 +0800 Subject: [PATCH] feat: image rotation animation * Image rotation animation; * Thumbnail rotation animation. Log: Add rotation animation. Influence: Animation --- src/qml/ImageDelegate/BaseImageDelegate.qml | 23 ++- src/qml/ImageDelegate/NormalImageDelegate.qml | 137 +++++++++++++++++- src/qml/ImageViewer.qml | 9 +- .../NormalThumbnailDelegate.qml | 28 +++- src/qml/ThumbnailDelegate/ThumbnailImage.qml | 14 +- src/src/globalcontrol.cpp | 10 +- src/src/globalcontrol.h | 1 + 7 files changed, 204 insertions(+), 18 deletions(-) diff --git a/src/qml/ImageDelegate/BaseImageDelegate.qml b/src/qml/ImageDelegate/BaseImageDelegate.qml index 9dc5d7db..56a3799f 100644 --- a/src/qml/ImageDelegate/BaseImageDelegate.qml +++ b/src/qml/ImageDelegate/BaseImageDelegate.qml @@ -21,8 +21,12 @@ Item { return 0; } property int frameIndex: 0 + + // 图片绘制后是否初始化,Qt6后 Image.Ready 时 PaintedWidth 没有更新,以此标志判断是否执行初始化 + property bool inited: false property ImageInputHandler inputHandler: null - property bool isCurrentImage: index === IV.GControl.currentIndex + // PathView下不再提供 index === IV.GControl.currentIndex + property bool isCurrentImage: parent.PathView.isCurrentItem // 坐标偏移,用于动画效果时调整显示位置 property real offset: 0 // 图片绘制区域到边框的位置 @@ -58,11 +62,15 @@ Item { paintedPaddingWidth = (width - realdWidth) / 2; } - // 动画时调整显示距离 + // 动画时调整显示距离 ( 前一张图片 (-1) <-- 当前图片 (0) --> 下一张图片 (1) ) x: animationOffsetWidth * offset - onIsCurrentImageChanged: { - reset(); + // TODO: 优化绑定计算 + // 根据当前图片在 PathView 中的相对距离计算是否还原显示参数 + onOffsetChanged: { + if (offset < -0.99 || 0.99 < offset) { + reset(); + } } onStatusChanged: { if (Image.Ready === status || Image.Error === status) { @@ -72,7 +80,14 @@ Item { } Connections { + // 用于绘制更新后缩放等处理 function onPaintedWidthChanged() { + if (!inited) { + if (targetImage.paintedWidth > 0) { + inited = true; + } + reset(); + } updateOffset(); } diff --git a/src/qml/ImageDelegate/NormalImageDelegate.qml b/src/qml/ImageDelegate/NormalImageDelegate.qml index 07801568..58f30273 100644 --- a/src/qml/ImageDelegate/NormalImageDelegate.qml +++ b/src/qml/ImageDelegate/NormalImageDelegate.qml @@ -9,6 +9,17 @@ import "../Utils" BaseImageDelegate { id: delegate + property bool rotationRunning: false + + function resetSource() { + // 加载完成,触发动画效果 + var temp = image.source; + image.source = ""; + // 重置初始状态 + delegate.inited = false; + image.source = temp; + } + inputHandler: imageInput status: image.status targetImage: image @@ -26,10 +37,121 @@ BaseImageDelegate { source: "image://ImageLoad/" + delegate.source + "#frame_" + delegate.frameIndex width: delegate.width - Behavior on rotation { - NumberAnimation { - duration: 366 - easing.type: Easing.InOutQuad + onStatusChanged: { + if (Image.Ready === image.status && !rotationRunning) { + rotateAnimationLoader.active = false; + } + } + } + + // 旋转动画效果 + Loader { + id: rotateAnimationLoader + + active: false + anchors.fill: parent + + sourceComponent: Item { + id: rotateItem + + property real previousRealWidth: 0 + + function calcAnimation() { + rotationAnimation.to = IV.GControl.currentRotation; + // 初始化缩放比后再允许动画 + imageProxy.scale = image.scale; + imageScaleBehavior.enabled = true; + + // 记录之前的绘制宽度,用于计算缩放比例 + previousRealWidth = image.paintedWidth; + + // 触发动画 + aniamtion.start(); + } + + anchors.fill: parent + + Connections { + function onPaintedWidthChanged() { + // 注意宽高交换,缩放比 = 实际显示的高度(绘制高度 * 缩放比) / 之前绘制的高度 + // 因此缩放的图片也能正常旋转匹配 + imageProxy.scale = (image.paintedHeight * image.scale) / rotateItem.previousRealWidth; + } + + target: image + } + + ShaderEffectSource { + id: imageProxy + + anchors.centerIn: parent + height: image.height + live: false + sourceItem: image + width: image.width + + Behavior on scale { + id: imageScaleBehavior + + enabled: false + + NumberAnimation { + id: scaleAnimation + + duration: IV.GStatus.animationDefaultDuration - delayUpdate.interval + easing.type: Easing.OutExpo + } + } + + Component.onCompleted: { + scheduleUpdate(); + // 计算动画参数并触发动画 + calcAnimation(); + } + } + + Timer { + id: delayUpdate + + interval: 50 + + onTriggered: { + delegate.resetSource(); + } + } + + // 并行动画 + ParallelAnimation { + id: aniamtion + + onRunningChanged: { + if (running) { + image.visible = false; + delayUpdate.start(); + } + if (!running && Image.Ready === image.status) { + image.visible = true; + rotateAnimationLoader.active = false; + } + rotationRunning = running; + } + + RotationAnimation { + id: rotationAnimation + + direction: RotationAnimation.Shortest + duration: IV.GStatus.animationDefaultDuration + easing.type: Easing.OutExpo + target: imageProxy + } + + NumberAnimation { + duration: IV.GStatus.animationDefaultDuration + easing.type: Easing.OutExpo + properties: "x, y" + target: imageProxy + to: 0 + } } } } @@ -43,13 +165,12 @@ BaseImageDelegate { } Connections { - function onCurrentRotationChanged() { + function onChangeRotationCacheBegin() { // Note: 确保缓存中的数据已刷新后更新界面 // 0 为复位,缓存中的数据已转换,无需再次加载 if (0 !== IV.GControl.currentRotation) { - var temp = image.source; - image.source = ""; - image.source = temp; + // 激活旋转动画加载器 + rotateAnimationLoader.active = true; } } diff --git a/src/qml/ImageViewer.qml b/src/qml/ImageViewer.qml index a96792e7..2f930600 100644 --- a/src/qml/ImageViewer.qml +++ b/src/qml/ImageViewer.qml @@ -88,7 +88,8 @@ Item { } function rotateImage(angle) { - if (targetImageReady) { + if (targetImageReady && !rotateDelay.running) { + rotateDelay.start(); IV.GControl.currentRotation += angle; } } @@ -148,6 +149,12 @@ Item { } onWidthChanged: keepImageDisplayScale() + Timer { + id: rotateDelay + + interval: IV.GStatus.animationDefaultDuration + 50 + } + // 图像动画:缩放 ImageAnimation { id: imageAnimation diff --git a/src/qml/ThumbnailDelegate/NormalThumbnailDelegate.qml b/src/qml/ThumbnailDelegate/NormalThumbnailDelegate.qml index df1f7e05..ed009708 100644 --- a/src/qml/ThumbnailDelegate/NormalThumbnailDelegate.qml +++ b/src/qml/ThumbnailDelegate/NormalThumbnailDelegate.qml @@ -47,8 +47,17 @@ BaseThumbnailDelegate { ThumbnailImage { id: img - anchors.fill: parent + anchors.centerIn: parent + height: parent.height source: normalThumbnailDelegate.source + width: parent.width + } + + Rectangle { + id: maskRect + + anchors.fill: img + radius: imgRadius visible: false } @@ -76,13 +85,28 @@ BaseThumbnailDelegate { } } + RotationAnimation { + id: rotateAnimation + + duration: IV.GStatus.animationDefaultDuration + easing.type: Easing.OutExpo + target: img.image + + onRunningChanged: { + if (!running) { + img.reset(); + } + } + } + // 执行旋转操作后,重新读取缓存数据,更新图片状态 Connections { function onCurrentRotationChanged() { // Note: 确保缓存中的数据已刷新后更新界面 // 0 为复位,缓存中的数据已转换,无需再次加载 if (0 !== IV.GControl.currentRotation) { - img.reset(); + rotateAnimation.to = IV.GControl.currentRotation; + rotateAnimation.start(); } } diff --git a/src/qml/ThumbnailDelegate/ThumbnailImage.qml b/src/qml/ThumbnailDelegate/ThumbnailImage.qml index ebe7b273..5d4d98c2 100644 --- a/src/qml/ThumbnailDelegate/ThumbnailImage.qml +++ b/src/qml/ThumbnailDelegate/ThumbnailImage.qml @@ -19,8 +19,11 @@ Item { var temp = contentImage.source; contentImage.source = ""; contentImage.source = temp; + contentImage.rotation = 0; } + clip: true + Image { id: contentImage @@ -29,7 +32,16 @@ Item { cache: false fillMode: Image.PreserveAspectCrop height: thumbnailImage.height - smooth: false + // 用于在旋转过程中不显示白边,图片大小比例 1 -> sqrt(2) -> 1 + scale: { + if (0 === rotation) { + return 1; + } + var normalRotation = (Math.abs(rotation) % 90); + var ratio = (normalRotation < 45) ? (normalRotation / 45) : ((90 - normalRotation) / 45); + return 1 + ((Math.sqrt(2) - 1) * ratio); + } + smooth: true width: thumbnailImage.width } diff --git a/src/src/globalcontrol.cpp b/src/src/globalcontrol.cpp index fd31da92..5f763b62 100644 --- a/src/src/globalcontrol.cpp +++ b/src/src/globalcontrol.cpp @@ -120,11 +120,17 @@ void GlobalControl::setCurrentRotation(int angle) } // 计算相较上一次是否需要交换宽高,angle 为 0 时特殊处理,不调整 - if (angle && !!((angle - imageRotation) % 180)) { + bool needSwap = angle && !!((angle - imageRotation) % 180); + + imageRotation = angle; + + // 开始变更旋转文件缓存和参数设置前触发,用于部分前置操作更新 + Q_EMIT changeRotationCacheBegin(); + + if (needSwap) { currentImage.swapWidthAndHeight(); } - imageRotation = angle; // 保证更新界面旋转前刷新缓存,为0时同样通知,用以复位状态 Q_EMIT requestRotateCacheImage(); Q_EMIT currentRotationChanged(); diff --git a/src/src/globalcontrol.h b/src/src/globalcontrol.h index e7ae4127..e5a12952 100644 --- a/src/src/globalcontrol.h +++ b/src/src/globalcontrol.h @@ -53,6 +53,7 @@ class GlobalControl : public QObject // 图像旋转处理 void setCurrentRotation(int angle); int currentRotation(); + Q_SIGNAL void changeRotationCacheBegin(); Q_SIGNAL void currentRotationChanged(); Q_SIGNAL void requestRotateCacheImage();