聊天侧边栏体验优化
记录日期:2026-04-02
格式参考:docs/开发记录博客/agents.md(问题背景 → 问题列表 → 根因链 → 方案 → 采纳 → 用例 → 小结)。
1. 问题背景
神秘人 agent-chat 页通过右上角图标打开 历史会话侧栏(列表 + 遮罩)。联调与真机反馈:打开/关闭不跟手、偶有顿挫,与主对话区的流畅度不一致;期望侧栏动画 稳定 60fps、交互反馈明确。
实现位置:zhiyuxing-app/app/(tabs)/agent-chat.tsx(侧栏 UI、打开/关闭逻辑)。
2. 问题列表
2.1 打开侧栏瞬间卡顿、动画不平滑
根因(证据级)
- why1:bug 表现为一次
setSidebarOpen(true)后,首帧或前几帧 遮罩/面板不同步 或 整体迟滞。 - why2:为什么——侧栏曾被包在
Modal(transparent+animationType="none")里;每次visible=true会走 原生 Modal 挂载/层级切换,与后续react-native的Animated+requestAnimationFrame拼出来的打开动画 叠在一起,主线程调度成本高。 - why3:为什么 JS 驱动动画更抖——
Animated.spring/timing虽可useNativeDriver: true,但与useLayoutEffect里同步setValue组合时,若与 Modal 首帧布局竞态,仍易出现 一帧跳动或体感「粘滞」。
方案(每题不超过两个)
- 短期方案:去掉
Modal,在同屏内用StyleSheet.absoluteFill+ 高zIndex/elevation(Web 辅以position: fixed)做全屏层;动画改由react-native-reanimated的useSharedValue+withSpring/withTiming驱动(UI 线程路径为主)。
优点:少一层 Modal 生命周期,打开链路更短、可预测。缺点:须自行处理 无障碍(如 accessibilityViewIsModal)与 Android 返回键(原 Modal 的 onRequestClose 需补回)。
- 长远方案:抽到独立
DrawerOverlay组件 + 设计 tokens(时长/弹簧参数),必要时上 Reanimated Layout 或共享元素过渡。
优点:多屏复用、参数可 A/B。缺点:拆分与测试成本。
采纳方案
- 已采纳 长远方案:独立组件
AgentChatHistoryDrawer(zhiyuxing-app/src/components/agent-chat/AgentChatHistoryDrawer.tsx)+ 动画参数drawerTokens.ts;仍为 绝对定位全屏层 + Reanimated(无 Modal),参数集中可复用与调参。
补充用例
- 真机:连续快速 开→关→开,无明显掉帧与错位;弱网/低电量机型抽样目检。
- 代码:
pnpm exec eslint app/(tabs)/agent-chat.tsx src/components/agent-chat/AgentChatHistoryDrawer.tsx通过。
2.2 关闭路径不一致、部分操作「瞬关」无动画
根因
- why1:bug:点遮罩/返回键有动画,但 切换会话 / 新建会话 若直接
setSidebarOpen(false),侧栏 瞬时消失。 - why2:为什么——历史代码里
closeSidebar仅存setState,未与dismissDrawer(带动画的收起)统一。
方案
- 短期方案:选中会话、新建会话 等需关栏的路径,一律调用
dismissDrawer()(内部withTiming到 0 再setSidebarOpen(false))。
优点:行为一致。缺点:关栏总时长约 ~240ms,逻辑上稍晚卸载列表(可接受)。
- 长远方案:侧栏状态机(
idle/opening/open/closing)集中管理,禁止直接写sidebarOpen。
优点:防回归。缺点:状态增多。
采纳方案
- 已采纳 长远方案:侧栏
closed/opening/open/closing状态机(useReducer),禁止页面内再写setSidebarOpen;关栏统一requestClose()→CLOSE_START→ 动画结束 →CLOSED;业务侧仅drawerRef.current?.requestClose()。
补充用例
- 手测:侧栏打开 → 点另一条会话 → 观察关栏动画完整播完再进入新会话上下文。
2.3 历史列表首开展示时掉帧
根因
- why1:bug:会话较多时,第一次打开侧栏 FlatList 布局+挂载 与动画同帧竞争。
- why2:为什么——默认
FlatList批量渲染与测量在主线程;未限制initialNumToRender等时,首帧工作量大。
方案
- 短期方案:为侧栏
FlatList设置initialNumToRender、maxToRenderPerBatch、windowSize;Android 上removeClippedSubviews(按官方收益与副作用取舍)。
优点:低风险。缺点:极长列表仍依赖虚拟化本身上限。
- 长远方案:会话列表数据分页、骨架屏、或
FlashList。
优点:大数据量更稳。缺点:引入依赖与接口改造。
采纳方案
- 已采纳 长远方案:
listAgentChatSessionsPage(agent-chat-persistence.ts,常量AGENT_CHAT_SESSION_PAGE_SIZE)+ 触底onEndReached追加;列表为@shopify/flash-list;首屏同步前sessionsAwaitingFirstPaint时 骨架屏。
补充用例
- 本地造 30+ 条会话(或 Mock),首开侧栏感知对比优化前后。
2.4 欲「拖动手势关栏」与列表滚动抢事件
根因
- why1:若
Pan包在整个侧栏面板外,纵向滑列表易与 横向关栏 手势冲突。 - why2:为什么——
FlatList内部滚动与父级Gesture.Pan默认未声明 同时识别 关系时,会出现 滚动发涩 或 误触关栏。
方案
- 短期方案:仅「历史记录」标题栏包一层
GestureDetector,Pan.activeOffsetX/failOffsetY约束激活条件;Web 暂不启用手势(避免与浏览器滚动差异),仍依赖点遮罩/按钮。
优点:实现快、列表滚动不受影响。缺点:只能从 头部区域 右滑关栏,不是整屏边缘拖拽。
- 长远方案:列表外包
NativeViewGestureHandler,与PansimultaneousHandlers或 Reanimated 2 竞争/组合规则显式配置。
优点:可做到「列表边缘拖关」类产品体验。缺点:接入与调试成本高。
采纳方案
- 已采纳 长远方案:整块面板
GestureDetector+Gesture.Pan(),列表外包NativeViewGestureHandler并与 PansimultaneousWithExternalGesture(ref);Web 仍仅点遮罩/按钮关栏(避免与浏览器滚动差异)。
补充用例
- 真机:侧栏内 快速上下滑列表,不应频繁误关;在 面板区(含标题) 横向拖动可关栏或回弹。
2.5 Android 物理返回与 Modal 语义丢失
根因
- why1:移除
Modal后,系统 不再自动把 返回键 关联到onRequestClose。 - why2:bug 表现为侧栏打开时按返回 直接退出页面(或与预期不符),而非先关侧栏。
方案
- 短期方案:
sidebarOpen为真 时注册BackHandler,消费事件并调用dismissDrawer()。
优点:与旧 Modal 体验对齐。缺点:须注意 卸载时 remove 监听,避免泄漏。
采纳方案
- 保持:侧栏在 非
closed相位 时由AgentChatHistoryDrawer注册BackHandler,消费事件并dispatch(CLOSE_START)。
补充用例
- Android:打开侧栏 → 按返回 → 应先关侧栏;再按返回 → 按堆栈返回上一页(与路由一致)。
2.6(可选体验)打开时缺少触感反馈
根因
- 非 bug:纯 反馈弱;用户心理模型上「打开抽屉」常与 轻触感 搭配。
方案
- 短期方案:
openSidebar内(非 Web)Haptics.impactAsync(Light)。
优点:零结构改动。缺点:部分设备无马达或用户关闭触感。
采纳方案
- 保持:
drawerRef.open()→ 组件内(非 Web)Haptics.impactAsync(Light)。
补充用例
- 真机开关触感系统设置下各测一次,无崩溃即可。
3. 小结
- 最大收益:去 Modal + Reanimated —— 减少层级切换与 JS/布局竞态,侧栏开闭 主观顺滑度提升最明显。
- 一致性:状态机 +
requestClose(),避免 瞬关 与散落布尔位。 - 列表:分页 + FlashList + 骨架 控制首屏与长列表成本。
- 手势与平台:整板 Pan + 列表 NativeView simultaneous;Android BackHandler 在抽屉组件内;Web 用 fixed 遮罩 覆盖视口。
- 工程卫生:侧栏 独立组件 + tokens,
eslint零告警(见 2.1 补充用例)。
参考:zhiyuxing-app/app/(tabs)/agent-chat.tsx、zhiyuxing-app/src/components/agent-chat/AgentChatHistoryDrawer.tsx、zhiyuxing-app/src/lib/agent-chat-persistence.ts、docs/开发记录博客/agents.md。