From 461b1819cba412f811f42755de62d4759ac297c4 Mon Sep 17 00:00:00 2001 From: wjyrich Date: Tue, 3 Mar 2026 16:09:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20enhance=20drag-and-drop=20visual=20feed?= =?UTF-8?q?back=20and=20folder=20icon=20animation=20feat:=20=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E6=8B=96=E6=94=BE=E8=A7=86=E8=A7=89=E5=8F=8D=E9=A6=88?= =?UTF-8?q?=E5=92=8C=E6=96=87=E4=BB=B6=E5=A4=B9=E5=9B=BE=E6=A0=87=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提供拖动应用到文件夹的动画效果,实现应用原地缩小以及位移到相应的文件夹的应用位置, 采用方案为获取鼠标位置大小,然后采用hotspot的坐标偏移,获取应用图标中心点,然后转为icon坐标系的坐标图标应该放置的应用图标中心点,从而解决由于缩放图标会产生的位置偏移问题。 另外去除 girdview 是由于计算位置正确,但是存在着显示问题却会偏移像素级位置,所以去除。 采用dragAndfolderBackground 为 文件夹的背景, 也是对应的drag时候的图标的背景。 PMS: BUG-288931 BUG-315679 --- qml/FolderGridViewPopup.qml | 13 ++- qml/FullscreenFrame.qml | 36 ++++++- qml/IconItemDelegate.qml | 186 ++++++++++++++++++++++++++------- qml/windowed/WindowedFrame.qml | 12 ++- 4 files changed, 202 insertions(+), 45 deletions(-) diff --git a/qml/FolderGridViewPopup.qml b/qml/FolderGridViewPopup.qml index d2528707..a3e8b89f 100644 --- a/qml/FolderGridViewPopup.qml +++ b/qml/FolderGridViewPopup.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -541,9 +541,15 @@ Popup { } component DelegateDropArea: DropArea { + property bool isDragHover: false + onEntered: function(drag) { root.onDragEnter(this) - folderDragApplyTimer.dragId = drag.getDataAsString("text/x-dde-launcher-dnd-desktopId") + let dragId = drag.getDataAsString("text/x-dde-launcher-dnd-desktopId") + if (dragId !== model.desktopId) { + isDragHover = true + } + folderDragApplyTimer.dragId = dragId folderDragApplyTimer.restart() } onPositionChanged: function(drag) { @@ -558,6 +564,7 @@ Popup { } } onExited: { + isDragHover = false root.onDragExit(this) folderDragApplyTimer.stop() folderDragApplyTimer.dragId = "" @@ -566,6 +573,7 @@ Popup { root.onDragExit(this) } onDropped: function(drop) { + isDragHover = false let dragId = drop.getDataAsString("text/x-dde-launcher-dnd-desktopId") if (dragId === "") { return @@ -619,6 +627,7 @@ Popup { id: innerItem anchors.fill: parent dndEnabled: true + isDragHover: false displayFont: isWindowedMode ? DTK.fontManager.t9 : DTK.fontManager.t6 Drag.mimeData: Helper.generateDragMimeData(model.desktopId) visible: dndItem.currentlyDraggedId !== model.desktopId diff --git a/qml/FullscreenFrame.qml b/qml/FullscreenFrame.qml index e2c9a2b2..df6c267e 100644 --- a/qml/FullscreenFrame.qml +++ b/qml/FullscreenFrame.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -33,7 +33,18 @@ InputEventItem { // ----------- Drag and Drop related functions START ----------- Label { property string currentlyDraggedId - + property string currentlyDraggedIconName + + property bool mergeAnimPending: false + //被拖拽图标 + property string mergeAnimTargetIcon: "" + //放下的图标或文件夹 + property string mergeAnimTargetIcon2: "" + // 鼠标松手位置(窗口坐标) + property real mergeAnimStartX: 0 + property real mergeAnimStartY: 0 + + property real mergeSize: 0 signal dragEnded() id: dndItem @@ -45,6 +56,7 @@ InputEventItem { text = "Dragging " + currentlyDraggedId } else { currentlyDraggedId = "" + currentlyDraggedIconName = "" dragEnded() } } @@ -547,6 +559,8 @@ InputEventItem { delegate: DropArea { Keys.forwardTo: [iconItemDelegate] + property bool isDragHover: false + visible: !folderGridViewPopup.visible || folderGridViewPopup.currentFolderId !== Number(model.desktopId.replace("internal/folders/", "")) width: gridViewContainer.cellWidth height: gridViewContainer.cellHeight @@ -554,14 +568,20 @@ InputEventItem { if (folderGridViewPopup.opened) { folderGridViewPopup.close() } - dndDropEnterTimer.dragId = drag.getDataAsString("text/x-dde-launcher-dnd-desktopId") + let dragId = drag.getDataAsString("text/x-dde-launcher-dnd-desktopId") + if (dragId !== model.desktopId) { + isDragHover = true + } + dndDropEnterTimer.dragId = dragId dndDropEnterTimer.restart() } onExited: { + isDragHover = false dndDropEnterTimer.stop() dndDropEnterTimer.dragId = "" } onDropped: function (drop) { + isDragHover = false dndDropEnterTimer.stop() dndDropEnterTimer.dragId = "" let dragId = drop.getDataAsString("text/x-dde-launcher-dnd-desktopId") @@ -572,6 +592,15 @@ InputEventItem { } else if (drop.x > (width - sideOpPadding)) { op = 1 } + if (op === 0) { + dndItem.mergeAnimTargetIcon = dndItem.currentlyDraggedIconName + dndItem.mergeAnimTargetIcon2 = !folderIcons ? iconItemDelegate.iconSource : "" + let cursorScene = mapToItem(null, drop.x, drop.y) + let hs = dndItem.Drag.hotSpot + dndItem.mergeAnimStartX = cursorScene.x - hs.x + dndItem.mergeSize / 2 + dndItem.mergeAnimStartY = cursorScene.y - hs.y + dndItem.mergeSize / 2 + dndItem.mergeAnimPending = true + } dropOnItem(dragId, model.desktopId, op) proxyModel.sort(0) } @@ -606,6 +635,7 @@ InputEventItem { } enabled: !folderGridViewPopup.visible dndEnabled: !folderGridViewPopup.opened + isDragHover: parent.isDragHover Drag.mimeData: Helper.generateDragMimeData(model.desktopId) visible: dndItem.currentlyDraggedId !== model.desktopId iconSource: (iconName && iconName !== "") ? iconName : "application-x-desktop" diff --git a/qml/IconItemDelegate.qml b/qml/IconItemDelegate.qml index 1f7c1958..74bad542 100644 --- a/qml/IconItemDelegate.qml +++ b/qml/IconItemDelegate.qml @@ -28,9 +28,11 @@ Control { property string iconSource property bool dndEnabled: false + property bool isDragHover: false readonly property bool isWindowedMode: LauncherController.currentFrame === "WindowedFrame" property alias displayFont: iconItemLabel.font property real iconScaleFactor: 1.0 + property bool iconIntroAnimRunning: false Accessible.name: iconItemLabel.text @@ -53,6 +55,7 @@ Control { } contentItem: Button { + hoverEnabled: !root.iconIntroAnimRunning focusPolicy: Qt.NoFocus ColorSelector.pressed: false ColorSelector.family: D.Palette.CrystalColor @@ -67,10 +70,42 @@ Control { } Item { + id: iconContainer width: parent.width / 2 height: width anchors.horizontalCenter: parent.horizontalCenter + Rectangle { + id: dragAndfolderBackground + visible: root.icons !== undefined || (root.isDragHover && !isWindowedMode) + opacity: root.icons !== undefined || (root.isDragHover && !isWindowedMode) ? 1 : 0 + scale: (root.isDragHover && !isWindowedMode) ? 1.2 : 1 + color: "#26FFFFFF" + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutQuad } + } + Behavior on scale { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + anchors.fill: parent + radius: 12 + + NumberAnimation on scale { + id: ininAni + running: false + from: 1.2 + to: 1 + duration: 200 + easing.type: Easing.OutCubic + } + + Component.onCompleted: { + if (root.icons !== undefined && dndItem.mergeAnimTargetIcon && dndItem.mergeAnimTargetIcon2) { + ininAni.start() + } + } + } + Loader { id: iconLoader anchors.fill: parent @@ -91,8 +126,10 @@ Control { // Item will be hidden by checking the dndItem.currentlyDraggedId property. We assign the value // to that property here dndItem.currentlyDraggedId = target.Drag.mimeData["text/x-dde-launcher-dnd-desktopId"] + dndItem.currentlyDraggedIconName = root.iconSource dndItem.Drag.hotSpot = target.Drag.hotSpot dndItem.Drag.mimeData = target.Drag.mimeData + dndItem.mergeSize = Math.min(iconLoader.width, iconLoader.height) iconLoader.grabToImage(function(result) { dndItem.Drag.imageSource = result.url; @@ -118,50 +155,120 @@ Control { Component { id: folderComponent - Rectangle { + Item { + id: iconItem anchors.fill: parent - color: "#26FFFFFF" - radius: 12 - - GridLayout { - id: folderGrid - anchors.fill: parent - rows: 2 - columns: 2 - anchors.margins: 8 - columnSpacing: 8 - rowSpacing: 8 - - Repeater { - model: icons - - DciIcon { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - - // 添加最大高度限制,确保图标高度一致 - Layout.maximumHeight: Math.max(0, parent.height / 2 - folderGrid.rowSpacing / 2) - - name: modelData - sourceSize: Qt.size(root.maxIconSizeInFolder, root.maxIconSizeInFolder) - scale: (parent.width / 2 / root.maxIconSizeInFolder) * root.iconScaleFactor - palette: DTK.makeIconPalette(root.palette) - theme: ApplicationHelper.DarkType + property real maxIconCount: 2 + property real spacing: 8 + property real itemWidth: (width - ((maxIconCount + 1) * spacing)) / 2 + property real itemHeight: (height - ((maxIconCount + 1) * spacing)) / maxIconCount + + function getItemX(index) { + let col = index % maxIconCount + let ItemX = (col + 1) * spacing + col * itemWidth + + return ItemX + } + + function getItemY(index) { + let row = Math.floor(index / maxIconCount) + let ItemY = (row + 1) * spacing + row * itemHeight + return ItemY + } + Repeater { + model: icons + + DciIcon { + id: folderIcon + x: iconItem.getItemX(index) + y: iconItem.getItemY(index) + + width: iconItem.itemWidth + height: iconItem.itemHeight + + name: modelData + sourceSize: Qt.size(root.maxIconSizeInFolder, root.maxIconSizeInFolder) + scale: (itemWidth / root.maxIconSizeInFolder) * root.iconScaleFactor + + property real introScale: 1.0 + + palette: DTK.makeIconPalette(root.palette) + theme: ApplicationHelper.DarkType + + // 位移动画属性 + property real iconCenterX: 0 + property real iconCenterY: 0 + ParallelAnimation { + id: iconIntroAnim + onStarted: root.iconIntroAnimRunning = true + + NumberAnimation { + target: folderIcon + property: "scale" + from: folderIcon.introScale + to: (itemWidth / root.maxIconSizeInFolder) * root.iconScaleFactor + duration: 400 + easing.type: Easing.OutExpo + } + NumberAnimation { + target: folderIcon + property: "x" + from: folderIcon.iconCenterX; to: iconItem.getItemX(index) + duration: 400 + easing.type: Easing.OutExpo + } + NumberAnimation { + target: folderIcon + property: "y" + from: folderIcon.iconCenterY; to: iconItem.getItemY(index) + duration: 400 + easing.type: Easing.OutExpo + } + + onFinished: { + root.iconIntroAnimRunning = false + dndItem.mergeAnimPending = false + dndItem.mergeAnimTargetIcon = "" + dndItem.mergeAnimTargetIcon2 = "" + } + } + + Component.onCompleted: { + if (dndItem.mergeAnimPending + && modelData === dndItem.mergeAnimTargetIcon) { + folderIcon.visible = false + Qt.callLater(function() { + let localPos = iconItem.mapFromItem(null, + dndItem.mergeAnimStartX, dndItem.mergeAnimStartY) + folderIcon.iconCenterX = localPos.x - folderIcon.width / 2 + folderIcon.iconCenterY = localPos.y - folderIcon.height / 2 + folderIcon.introScale = (iconContainer.width / root.maxIconSizeInFolder) * root.iconScaleFactor + folderIcon.visible = true + iconIntroAnim.start() + }) + } else if (dndItem.mergeAnimPending + && modelData === dndItem.mergeAnimTargetIcon2) { + Qt.callLater(function() { + folderIcon.iconCenterX = iconContainer.width / 2 - folderIcon.width / 2 + folderIcon.iconCenterY = iconContainer.height / 2 - folderIcon.height / 2 + folderIcon.introScale = (iconContainer.width / root.maxIconSizeInFolder) * root.iconScaleFactor + iconIntroAnim.start() + }) + } } } + } - Repeater { - model: 4 - icons.length + Repeater { + model: 4 - icons.length - Item { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignLeft - width: parent.width / 2 - height: parent.height / 2 - } + width: parent.width / 2 + height: parent.height / 2 } } } @@ -175,7 +282,7 @@ Control { anchors.fill: parent name: iconSource sourceSize: Qt.size(root.maxIconSize, root.maxIconSize) - scale: (parent.width / root.maxIconSize) * root.iconScaleFactor + scale: (iconContainer.width / root.maxIconSize) * root.iconScaleFactor palette: DTK.makeIconPalette(root.palette) theme: ApplicationHelper.DarkType fillMode: Image.PreserveAspectFit @@ -193,6 +300,7 @@ Control { property bool singleRow: font.pixelSize > (isWindowedMode ? Helper.windowed.doubleRowMaxFontSize : Helper.fullscreen.doubleRowMaxFontSize) property bool isNewlyInstalled: model.lastLaunchedTime === 0 && model.installedTime !== 0 id: iconItemLabel + visible: !root.isDragHover text: isNewlyInstalled ? ("  " + root.text) : root.text textFormat: isNewlyInstalled ? Text.StyledText : Text.PlainText width: parent.width diff --git a/qml/windowed/WindowedFrame.qml b/qml/windowed/WindowedFrame.qml index ff6d1b4b..1d2759a6 100644 --- a/qml/windowed/WindowedFrame.qml +++ b/qml/windowed/WindowedFrame.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -44,6 +44,15 @@ InputEventItem { // ----------- Drag and Drop related functions START ----------- Label { property string currentlyDraggedId + property string currentlyDraggedIconName + + property bool mergeAnimPending: false + property string mergeAnimTargetIcon: "" + property string mergeAnimTargetIcon2: "" + property real mergeAnimStartX: 0 + property real mergeAnimStartY: 0 + + property real mergeSize: 0 id: dndItem visible: DebugHelper.qtDebugEnabled @@ -54,6 +63,7 @@ InputEventItem { text = "Dragging " + currentlyDraggedId } else { currentlyDraggedId = "" + currentlyDraggedIconName = "" } } }