Skip to content

Commit

Permalink
feat: image rotation animation
Browse files Browse the repository at this point in the history
* Image rotation animation;
* Thumbnail rotation animation.

Log: Add rotation animation.
Influence: Animation
  • Loading branch information
rb-union committed Aug 9, 2024
1 parent 6169eb3 commit a0d1657
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 18 deletions.
23 changes: 19 additions & 4 deletions src/qml/ImageDelegate/BaseImageDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
// 图片绘制区域到边框的位置
Expand Down Expand Up @@ -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) {
Expand All @@ -72,7 +80,14 @@ Item {
}

Connections {
// 用于绘制更新后缩放等处理
function onPaintedWidthChanged() {
if (!inited) {
if (targetImage.paintedWidth > 0) {
inited = true;
}
reset();
}
updateOffset();
}

Expand Down
137 changes: 129 additions & 8 deletions src/qml/ImageDelegate/NormalImageDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
}
}
Expand All @@ -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;
}
}

Expand Down
9 changes: 8 additions & 1 deletion src/qml/ImageViewer.qml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ Item {
}

function rotateImage(angle) {
if (targetImageReady) {
if (targetImageReady && !rotateDelay.running) {
rotateDelay.start();
IV.GControl.currentRotation += angle;
}
}
Expand Down Expand Up @@ -148,6 +149,12 @@ Item {
}
onWidthChanged: keepImageDisplayScale()

Timer {
id: rotateDelay

interval: IV.GStatus.animationDefaultDuration + 50
}

// 图像动画:缩放
ImageAnimation {
id: imageAnimation
Expand Down
28 changes: 26 additions & 2 deletions src/qml/ThumbnailDelegate/NormalThumbnailDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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();
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/qml/ThumbnailDelegate/ThumbnailImage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ Item {
var temp = contentImage.source;
contentImage.source = "";
contentImage.source = temp;
contentImage.rotation = 0;
}

clip: true

Image {
id: contentImage

Expand All @@ -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
}

Expand Down
10 changes: 8 additions & 2 deletions src/src/globalcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/src/globalcontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down

0 comments on commit a0d1657

Please sign in to comment.