Skip to content

fix: improve notification bubble panel hide animation#1490

Open
qxp930712 wants to merge 1 commit intolinuxdeepin:masterfrom
qxp930712:master
Open

fix: improve notification bubble panel hide animation#1490
qxp930712 wants to merge 1 commit intolinuxdeepin:masterfrom
qxp930712:master

Conversation

@qxp930712
Copy link

@qxp930712 qxp930712 commented Mar 10, 2026

Changed the bubble panel visibility logic to include a 400ms delay before hiding when there are no notifications. This prevents the panel from disappearing abruptly when the last bubble is removed. The QML ListView now includes a remove transition with smooth exit animation for bubbles.

Added QTimer include for delayed hide functionality. Modified the ListView height calculation to use maximum of contentHeight and childrenRect.height to ensure proper layout during animations. Implemented a sequential animation for bubble removal with x-axis slide- out effect.

Log: Improved notification bubble animations with smoother hide effects

Influence:

  1. Test notification bubble appearance and disappearance
  2. Verify panel remains visible during bubble removal animations
  3. Check that multiple bubbles animate correctly
  4. Test edge cases with rapid notification additions/removals
  5. Verify panel properly hides after all bubbles are removed
  6. Test animation timing and smoothness

PMS: BUG-284659

Summary by Sourcery

Improve notification bubble panel visibility and removal animations to provide a smoother hide experience when bubbles are cleared.

New Features:

  • Add a ListView remove transition with a sliding exit animation for notification bubbles.

Bug Fixes:

  • Delay hiding the notification bubble panel by 400ms after the last bubble is removed to prevent abrupt disappearance.
  • Adjust ListView height calculation to account for both contentHeight and childrenRect.height so layout remains correct during animations.

Enhancements:

  • Integrate delayed hide logic using QTimer in the bubble panel to coordinate panel visibility with bubble removal animations.

Changed the bubble panel visibility logic to include a 400ms delay
before hiding when there are no notifications. This prevents the panel
from disappearing abruptly when the last bubble is removed. The QML
ListView now includes a remove transition with smooth exit animation
for bubbles.

Added QTimer include for delayed hide functionality. Modified the
ListView height calculation to use maximum of contentHeight and
childrenRect.height to ensure proper layout during animations.
Implemented a sequential animation for bubble removal with x-axis slide-
out effect.

Log: Improved notification bubble animations with smoother hide effects

Influence:
1. Test notification bubble appearance and disappearance
2. Verify panel remains visible during bubble removal animations
3. Check that multiple bubbles animate correctly
4. Test edge cases with rapid notification additions/removals
5. Verify panel properly hides after all bubbles are removed
6. Test animation timing and smoothness

PMS: BUG-284659
@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: qxp930712

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 10, 2026

Reviewer's Guide

Improves the notification bubble panel’s disappearance behavior by delaying panel hide until after a 400ms bubble removal animation, and adjusts ListView sizing to better accommodate animated content.

Sequence diagram for delayed hide of notification bubble panel

sequenceDiagram
    participant NotificationSource
    participant BubbleModel
    participant BubblePanel
    participant QML_BubbleView
    participant QTimer

    NotificationSource->>BubbleModel: removeNotification(id)
    BubbleModel-->>BubblePanel: bubbleCountChanged()

    BubblePanel->>BubblePanel: onBubbleCountChanged()
    BubblePanel->>BubblePanel: isEmpty = m_bubbles.items().isEmpty()
    BubblePanel->>BubblePanel: visible = !isEmpty && enabled()

    alt visible is true
        BubblePanel->>BubblePanel: setVisible(true)
        BubblePanel-->>QML_BubbleView: remain visible
    else visible is false
        BubblePanel->>QTimer: singleShot(400ms, this, lambda)
        QML_BubbleView->>QML_BubbleView: ListView remove Transition
        QML_BubbleView->>QML_BubbleView: SequentialAnimation
        QML_BubbleView->>QML_BubbleView: ListView.delayRemove = true
        QML_BubbleView->>QML_BubbleView: NumberAnimation x -> 360 (400ms)
        QML_BubbleView->>QML_BubbleView: ListView.delayRemove = false
        QTimer-->>BubblePanel: timeout after 400ms
        BubblePanel->>BubblePanel: setVisible(false)
    end
Loading

Updated class diagram for BubblePanel notification visibility logic

classDiagram
    class BubblePanel {
        - m_bubbles
        + onNotificationStateChanged(id, processedType) void
        + onBubbleCountChanged() void
        + addBubble(id) void
        + setVisible(visible) void
        + enabled() bool
    }

    class QTimer {
        + singleShot(msec, receiver, slot) void
    }

    BubblePanel ..> QTimer : uses_singleShot_for_delayed_hide
Loading

File-Level Changes

Change Details Files
Adjust ListView sizing so animated bubble content is fully accounted for during layout.
  • Change ListView height binding to use the maximum of contentHeight and childrenRect.height to avoid clipping during animations
panels/notification/bubble/package/main.qml
Add a smooth slide-out remove transition for notification bubbles in the ListView.
  • Define a remove Transition on the ListView using a SequentialAnimation
  • Use PropertyAction to enable ListView.delayRemove before the animation and disable it after
  • Animate bubble x-property to 360 over 400ms with an InExpo easing curve inside a ParallelAnimation
panels/notification/bubble/package/main.qml
Delay hiding the bubble panel by 400ms when the last bubble is removed to let the exit animation complete.
  • Include QTimer header to support delayed operations
  • Update onBubbleCountChanged to compute desired visibility based on emptiness and enabled state
  • When the panel should become invisible, schedule setVisible(false) via QTimer::singleShot(400, this, ...) instead of hiding immediately
  • Ensure that when there are bubbles and the panel is enabled, visibility is set immediately
panels/notification/bubble/bubblepanel.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The QTimer::singleShot in onBubbleCountChanged() will hide the panel unconditionally after 400ms even if new bubbles arrived in the meantime; consider checking the current bubble count/enabled state in the lambda or cancelling/reusing a single timer to avoid hiding an active panel.
  • In the ListView remove transition, the hardcoded value to: 360 for the x animation couples the animation to a magic number; using to: width or a shared constant would make the slide-out behavior robust to future width/layout changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The QTimer::singleShot in onBubbleCountChanged() will hide the panel unconditionally after 400ms even if new bubbles arrived in the meantime; consider checking the current bubble count/enabled state in the lambda or cancelling/reusing a single timer to avoid hiding an active panel.
- In the ListView remove transition, the hardcoded value `to: 360` for the x animation couples the animation to a magic number; using `to: width` or a shared constant would make the slide-out behavior robust to future width/layout changes.

## Individual Comments

### Comment 1
<location path="panels/notification/bubble/bubblepanel.cpp" line_range="114-120" />
<code_context>
 {
     bool isEmpty = m_bubbles->items().isEmpty();
-    setVisible(!isEmpty && enabled());
+    const bool visible = !isEmpty && enabled();
+    if (!visible) {
+        QTimer::singleShot(400, this, [this]() {
+            setVisible(false);
+        });
+    } else {
+        setVisible(visible);
+    }
 }
</code_context>
<issue_to_address>
**issue (bug_risk):** Delayed hide timer can incorrectly hide the panel after it becomes visible again.

Because `QTimer::singleShot` is unconditional, a call with `visible == false` will still invoke `setVisible(false)` after 400 ms even if a later call with `visible == true` has already made the panel visible. This creates a race where a stale hide timer overrides a newer show.

To fix this, either cancel/gate the hide in the lambda (e.g. re-check `!m_bubbles->items().isEmpty()` and `enabled()` before calling `setVisible(false)`), or track a hide request ID/sequence and only apply the latest one.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +114 to +120
const bool visible = !isEmpty && enabled();
if (!visible) {
QTimer::singleShot(400, this, [this]() {
setVisible(false);
});
} else {
setVisible(visible);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Delayed hide timer can incorrectly hide the panel after it becomes visible again.

Because QTimer::singleShot is unconditional, a call with visible == false will still invoke setVisible(false) after 400 ms even if a later call with visible == true has already made the panel visible. This creates a race where a stale hide timer overrides a newer show.

To fix this, either cancel/gate the hide in the lambda (e.g. re-check !m_bubbles->items().isEmpty() and enabled() before calling setVisible(false)), or track a hide request ID/sequence and only apply the latest one.

@deepin-ci-robot
Copy link

deepin pr auto review

这份代码变更主要是为了优化通知气泡面板在气泡数量变化时的显示/隐藏逻辑,并添加了气泡移除时的动画效果。以下是对代码的审查意见:

1. 代码逻辑与功能审查

BubblePanel.cpp 部分:

  • 功能:当气泡列表为空时,不再立即隐藏面板,而是延迟 400ms 后隐藏。当有气泡时,立即显示。
  • 潜在问题QTimer::singleShot 是异步的。如果在定时器触发前(400ms内),onBubbleCountChanged 再次被触发且有新气泡到来,逻辑上会执行 else setVisible(visible)(即显示)。
    • 这通常是期望的行为(新消息来了立即显示)。
    • 但是,如果 QTimer::singleShot 的 lambda 已经在事件队列中排队,且此时 visiblefalse,面板会先显示(立即),然后定时器触发,面板又会被隐藏。这会导致 UI 闪烁。
    • 建议:引入一个成员变量(如 QPointer<QTimer>int m_hideTimerId)来管理定时器。在设置新的定时器前,先停止或删除之前的定时器。或者使用 QTimer 的成员变量而不是单次调用,以便在需要显示时调用 stop()

main.qml 部分:

  • 功能:为 ListView 添加了 remove 过渡动画,使气泡移除时向右滑出,并设置了 delayRemove 以确保动画完成后再从模型中移除数据。
  • 逻辑:逻辑正确,SequentialAnimation 配合 PropertyAction 设置 ListView.delayRemove 是处理移除动画的标准做法,防止动画未播完项就被销毁。

2. 代码质量与规范

  • 头文件包含
    • #include <qtimer.h>:Qt 头文件命名规范通常是大写 QTimer.h。虽然小写在某些平台/编译器下也能工作,但建议遵循 Qt 规范改为 #include <QTimer>
  • 变量命名
    • const bool visible:命名清晰,符合规范。
  • 魔法数字
    • 代码中出现了硬编码的数字 400(延迟时间)和 360(动画位移目标)。
    • 建议:将这些常量定义为类的静态常量、宏或 QML 中的属性,以便于统一管理和维护。例如 static constexpr int HIDE_DELAY_MS = 400;

3. 代码性能

  • BubblePanel.cpp
    • QTimer::singleShot 本身开销很小,性能影响可忽略。
  • main.qml
    • height: Math.max(contentHeight, childrenRect.height):在 ListView 中使用 childrenRect 通常会导致频繁的重绘和布局计算,因为 childrenRect 是一个依赖于所有子项的属性,任何子项的变化都会触发它更新。
    • 分析:对于 ListView,contentHeight 通常已经能够正确反映内容的高度(取决于 modeldelegate)。增加 childrenRect.height 可能是为了处理某些边缘情况(如空列表或布局未完成时)。
    • 建议:检查是否真的需要 childrenRect.height。如果 ListView 的布局是标准的,contentHeight 足够。如果必须使用,注意其对性能的影响。

4. 代码安全

  • 内存安全
    • QTimer::singleShot 接收 this 指针作为上下文。如果 BubblePanel 对象在 400ms 内被销毁,Qt 会自动取消该定时器回调,因此是安全的。
  • 状态一致性
    • 如逻辑审查中所述,异步操作可能导致状态不一致(显示后又隐藏)。需要处理竞态条件。

改进建议代码示例

针对 BubblePanel.cpp 的改进(防止闪烁和规范头文件):

// 头文件中
#include <QTimer> // 修正头文件大小写

// 类定义中增加成员变量
// private:
//    QTimer *m_hideTimer;

// 构造函数中初始化
// m_hideTimer = new QTimer(this);
// m_hideTimer->setSingleShot(true);
// connect(m_hideTimer, &QTimer::timeout, this, [this]() { setVisible(false); });

void BubblePanel::onBubbleCountChanged()
{
    bool isEmpty = m_bubbles->items().isEmpty();
    const bool visible = !isEmpty && enabled();

    if (visible) {
        // 如果需要显示,立即停止可能存在的隐藏定时器,防止闪烁
        if (m_hideTimer->isActive()) {
            m_hideTimer->stop();
        }
        setVisible(true);
    } else {
        // 如果需要隐藏,启动定时器
        // 如果定时器已经在运行,start 会重置时间,这也是合理的行为
        m_hideTimer->start(400); 
    }
}

针对 main.qml 的改进(去除魔法数字):

// Window 或 ListView 内部定义属性
readonly property int bubbleWidth: 360
readonly property int animationDuration: 400
readonly property int hideDelayDuration: 400 // 如果QML中也需要用到延迟逻辑

ListView {
    id: bubbleView
    width: bubbleWidth
    // ...
    
    remove: Transition {
        SequentialAnimation {
            PropertyAction { 
                property: "ListView.delayRemove"
                value: true 
            }
            
            ParallelAnimation {
                NumberAnimation {
                    property: "x"
                    to: bubbleWidth // 使用属性替代魔法数字
                    duration: animationDuration
                    easing.type: Easing.InExpo
                }
            }
            
            PropertyAction { 
                property: "ListView.delayRemove"
                value: false 
            }
        }
    }
}

总结

这段代码主要改进了 UI 交互体验。主要的改进点在于 C++ 端处理异步隐藏逻辑时的竞态条件(防止闪烁),以及对硬编码常量的提取。QML 端的动画逻辑实现正确,但需注意 childrenRect 的性能开销。

{
bool isEmpty = m_bubbles->items().isEmpty();
setVisible(!isEmpty && enabled());
const bool visible = !isEmpty && enabled();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个代码要优化下吧,例如它放在qml端,
另外,这个在暂存区域显示的时候,enabled()为false,实际上它不需要延迟隐藏,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants