From 279b3a07eb06f86b8825aa0a53e71cd3137e43a7 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 29 May 2026 11:52:56 +0800
Subject: [PATCH] Reduce app grid scroll churn

---
 TODO.md                            |   10 ++++++----
 Apptag/AppGridCollectionView.swift |   11 ++++++++---
 Apptag/ContentView.swift           |    4 +++-
 3 files changed, 17 insertions(+), 8 deletions(-)

diff --git a/Apptag/AppGridCollectionView.swift b/Apptag/AppGridCollectionView.swift
index a37c414..54a218b 100644
--- a/Apptag/AppGridCollectionView.swift
+++ b/Apptag/AppGridCollectionView.swift
@@ -272,6 +272,7 @@
     private var lastLayoutSize: NSSize = .zero
     private var lastReportedBoundsOrigin: NSPoint?
     private var scrollUnfreezeWorkItem: DispatchWorkItem?
+    private var scrollActivityIsActive = false
 
     override var isFlipped: Bool { true }
 
@@ -385,14 +386,18 @@
     }
 
     private func handleScrollActivity() {
-        if coordinator?.setScrollBubbleDisabled(true) == true {
-            refreshVisibleRuntimeState()
+        if !scrollActivityIsActive {
+            scrollActivityIsActive = true
+            if coordinator?.setScrollBubbleDisabled(true) == true {
+                refreshVisibleRuntimeState()
+            }
+            coordinator?.onScrollActivity()
         }
-        coordinator?.onScrollActivity()
 
         scrollUnfreezeWorkItem?.cancel()
         let workItem = DispatchWorkItem { [weak self] in
             guard let self else { return }
+            self.scrollActivityIsActive = false
             self.replayPointerHover()
             if self.coordinator?.setScrollBubbleDisabled(false) == true {
                 self.refreshVisibleRuntimeState()
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 275e8d3..90ef74e 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -1732,7 +1732,9 @@
     }
 
     private func handleAppGridScrollActivity() {
-        appGridInteraction.hoveredBubble = nil
+        if appGridInteraction.hoveredBubble != nil {
+            appGridInteraction.hoveredBubble = nil
+        }
     }
 
     private func handleBubbleHover(app: AppInfo, frame: CGRect, event: AppBubbleHoverEvent) {
diff --git a/TODO.md b/TODO.md
index 9f8bcd5..beb4643 100644
--- a/TODO.md
+++ b/TODO.md
@@ -13,12 +13,14 @@
 
 ## Todo
 
-- [2026-05-29] 第二批:AppGrid 滚动流畅性优化。
-  - 目标: 降低滚动期间 SwiftUI 状态写入和 visible item 刷新频率,改善偶发卡顿/抖动。
-  - 验收: 滚动手感稳定;hover 气泡不在滚动中闪现;不影响标签 hover 定位和拖拽。
-
 ## Done
 
+- [2026-05-29] 第二批:AppGrid 滚动流畅性优化。
+  - 提交: 本次提交
+  - 结果: AppGrid 滚动期间只在滚动 burst 开始时通知 SwiftUI 清理 hover 气泡,不再每次 bounds 变化重复写状态;滚动停止后再恢复 hover replay 和 visible item runtime state。
+  - 范围: 不改布局算法、不改拖拽、不改 hover 气泡展示规则,只降低滚动期间的刷新频率。
+  - 验证: `bash build.sh`、`codesign --verify --deep --strict`、`Scripts/window_logic_qa.sh` 全部通过;滚动后 Space/Esc 连续循环 QA 保持通过。
+
 - [2026-05-29] 第一批:未分类拖拽确认记忆 + 滚动后键盘兜底。
   - 提交: 本次提交
   - 结果: “拖到未分类”弹窗新增独立“不再提醒”checkbox 和 `skipUncategorizedDropConfirm` 持久化;overlay 键盘处理增加 AppKit `sendEvent` 兜底;AppGrid 滚轮进入时重新声明 key/front,避免滚动后 Space/Esc 偶发丢失。

--
Gitblit v1.9.3