From 6e7eaeab0f88f856cf872e8aadd26072113902d4 Mon Sep 17 00:00:00 2001 From: Mengci Cai Date: Mon, 2 Mar 2026 09:56:23 +0800 Subject: [PATCH] feat: add bubble removal animation 1. Implement asynchronous removal logic in BubbleModel to support QML animations 2. Add bubbleAboutToRemove signal and removeAnimationDuration property 3. Delay actual data removal until animation completes using QTimer::singleShot 4. Introduce slide-out and fade-out transitions in Bubble.qml and main.qml 5. Set ListView cacheBuffer to 0 to ensure proper transition rendering 6. Add removeDisplaced transition for smooth repositioning of remaining items Log: Notification bubbles will now slide out smoothly when closed instead of disappearing instantly. Influence: 1. Close a notification bubble and observe the slide-out and fade animation 2. Verify the bubble is only removed from the model after the animation finishes 3. Test removing multiple bubbles rapidly to check for visual artifacts 4. Verify that remaining bubbles slide up smoothly when a bubble is removed 5. Check model consistency after multiple remove operations 6. Confirm that hovering over a bubble does not interrupt the removal animation PMS: BUG-284659 --- panels/notification/bubble/bubblemodel.cpp | 23 ++++++++++++- panels/notification/bubble/bubblemodel.h | 7 ++++ panels/notification/bubble/package/Bubble.qml | 34 +++++++++++++++++++ panels/notification/bubble/package/main.qml | 32 +++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/panels/notification/bubble/bubblemodel.cpp b/panels/notification/bubble/bubblemodel.cpp index 1bcc6ee82..5f8cc223f 100644 --- a/panels/notification/bubble/bubblemodel.cpp +++ b/panels/notification/bubble/bubblemodel.cpp @@ -137,7 +137,17 @@ BubbleItem *BubbleModel::removeById(qint64 id) for (const auto &item : m_bubbles) { if (item->id() == id) { m_delayBubbles.removeAll(id); - remove(m_bubbles.indexOf(item)); + // Emit signal before removing to trigger QML animation + Q_EMIT bubbleAboutToRemove(id); + // Delay the actual removal to allow animation to complete + QTimer::singleShot(m_removeAnimationDuration, this, [this, id]() { + for (const auto &item : m_bubbles) { + if (item->id() == id) { + remove(m_bubbles.indexOf(item)); + break; + } + } + }); return item; } } @@ -145,6 +155,17 @@ BubbleItem *BubbleModel::removeById(qint64 id) return nullptr; } +int BubbleModel::removeAnimationDuration() const +{ + return m_removeAnimationDuration; +} + +void BubbleModel::setRemoveAnimationDuration(int duration) +{ + m_removeAnimationDuration = duration + 100; + Q_EMIT removeAnimationDurationChanged(); +} + BubbleItem *BubbleModel::bubbleItem(int bubbleIndex) const { if (bubbleIndex < 0 || bubbleIndex >= items().count()) diff --git a/panels/notification/bubble/bubblemodel.h b/panels/notification/bubble/bubblemodel.h index 5977775ff..bf81528de 100644 --- a/panels/notification/bubble/bubblemodel.h +++ b/panels/notification/bubble/bubblemodel.h @@ -18,6 +18,7 @@ class BubbleModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(qint64 delayRemovedBubble READ delayRemovedBubble WRITE setDelayRemovedBubble NOTIFY delayRemovedBubbleChanged FINAL) + Q_PROPERTY(int removeAnimationDuration READ removeAnimationDuration WRITE setRemoveAnimationDuration NOTIFY removeAnimationDurationChanged FINAL) public: enum { AppName = Qt::UserRole + 1, @@ -64,10 +65,15 @@ class BubbleModel : public QAbstractListModel qint64 delayRemovedBubble() const; void setDelayRemovedBubble(qint64 newDelayRemovedBubble); + int removeAnimationDuration() const; + void setRemoveAnimationDuration(int duration); + void clearInvalidBubbles(); signals: void delayRemovedBubbleChanged(); + void bubbleAboutToRemove(qint64 id); + void removeAnimationDurationChanged(); private: void updateBubbleCount(int count); @@ -85,6 +91,7 @@ class BubbleModel : public QAbstractListModel QList m_delayBubbles; qint64 m_delayRemovedBubble{NotifyEntity::InvalidId}; const int DelayRemovBubbleTime{1000}; + int m_removeAnimationDuration{700}; }; } diff --git a/panels/notification/bubble/package/Bubble.qml b/panels/notification/bubble/package/Bubble.qml index 4a78cbedc..50b506631 100644 --- a/panels/notification/bubble/package/Bubble.qml +++ b/panels/notification/bubble/package/Bubble.qml @@ -12,6 +12,17 @@ Control { id: control height: loader.height property var bubble + property bool isRemoving: false + + Connections { + target: Applet.bubbles + function onBubbleAboutToRemove(id) { + if (id === bubble.id) { + control.isRemoving = true + } + } + } + onHoveredChanged: function () { if (control.hovered) { Applet.bubbles.delayRemovedBubble = bubble.id @@ -19,6 +30,29 @@ Control { Applet.bubbles.delayRemovedBubble = NotifyEntity.InvalidId } } + + states: [ + State { + name: "removing" + when: control.isRemoving + PropertyChanges { + target: control + x: control.width + opacity: 0 + } + } + ] + + transitions: [ + Transition { + to: "removing" + NumberAnimation { + properties: "x,opacity" + duration: Applet.bubbles.removeAnimationDuration + easing.type: Easing.InExpo + } + } + ] Loader { id: loader diff --git a/panels/notification/bubble/package/main.qml b/panels/notification/bubble/package/main.qml index 80ccac395..bea9256b7 100644 --- a/panels/notification/bubble/package/main.qml +++ b/panels/notification/bubble/package/main.qml @@ -12,6 +12,12 @@ import org.deepin.dtk 1.0 Window { id: root + + readonly property int removeAnimationDuration: 600 + + Component.onCompleted: { + Applet.bubbles.removeAnimationDuration = removeAnimationDuration + } function windowMargin(position) { let dockApplet = DS.applet("org.deepin.ds.dock") @@ -105,6 +111,7 @@ Window { model: Applet.bubbles interactive: false verticalLayoutDirection: ListView.BottomToTop + cacheBuffer: 0 add: Transition { id: addTrans // Before starting the new animation, forcibly complete the previous notification bubble's animation @@ -128,6 +135,31 @@ Window { easing.type: Easing.OutExpo } } + remove: Transition { + NumberAnimation { + target: removeTrans.ViewTransition.item + property: "x" + from: 0 + to: removeTrans.ViewTransition.item.width + duration: root.removeAnimationDuration + easing.type: Easing.InExpo + } + NumberAnimation { + target: removeTrans.ViewTransition.item + property: "opacity" + from: 1.0 + to: 0.0 + duration: root.removeAnimationDuration + easing.type: Easing.InExpo + } + } + removeDisplaced: Transition { + NumberAnimation { + properties: "y" + duration: 400 + easing.type: Easing.OutCubic + } + } delegate: Bubble { width: 360 bubble: model