【UGUI】自定义 ListView 架构:设计、原理与可扩展性

文章目录

    • [1. 概述](#1. 概述)
    • [2. 四层职责模型](#2. 四层职责模型)
    • [3. 事件流详解](#3. 事件流详解)
      • [3.1 完整调用链](#3.1 完整调用链)
      • [3.2 NotifyClicked 与 OnClicked:为什么需要两个?](#3.2 NotifyClicked 与 OnClicked:为什么需要两个?)
      • [3.3 OnItemClicked:私有路由器](#3.3 OnItemClicked:私有路由器)
      • [3.4 Select / Deselect:状态机](#3.4 Select / Deselect:状态机)
      • [3.5 SuppressSelectionEvents:静默状态恢复](#3.5 SuppressSelectionEvents:静默状态恢复)
    • [4. Item 状态机](#4. Item 状态机)
      • [4.1 三态](#4.1 三态)
      • [4.2 通过 OnStateChanged 实现视觉定制](#4.2 通过 OnStateChanged 实现视觉定制)
    • [5. 数据流:Item 如何获取内容](#5. 数据流:Item 如何获取内容)
    • [6. 为什么不直接用 Unity 的 ScrollView?](#6. 为什么不直接用 Unity 的 ScrollView?)
      • [6.1 选中状态管理](#6.1 选中状态管理)
      • [6.2 数据驱动的 Item 生命周期](#6.2 数据驱动的 Item 生命周期)
      • [6.3 类型安全的泛型 Key](#6.3 类型安全的泛型 Key)
      • [6.4 一致的三态视觉契约](#6.4 一致的三态视觉契约)
      • [6.5 事件架构](#6.5 事件架构)
      • [6.6 动画协调](#6.6 动画协调)
      • [6.7 能力对比总览](#6.7 能力对比总览)
    • [7. 使用的设计模式](#7. 使用的设计模式)
      • [7.1 模板方法(Template Method)](#7.1 模板方法(Template Method))
      • [7.2 观察者模式(Observer / Event-Driven)](#7.2 观察者模式(Observer / Event-Driven))
      • [7.3 简化的状态模式(State Pattern)](#7.3 简化的状态模式(State Pattern))
      • [7.4 泛型即契约(Generics as Contract)](#7.4 泛型即契约(Generics as Contract))
    • [8. 可扩展方向](#8. 可扩展方向)
      • [8.1 多选模式](#8.1 多选模式)
      • [8.2 虚拟滚动(对象池化)](#8.2 虚拟滚动(对象池化))
      • [8.3 拖拽排序](#8.3 拖拽排序)
      • [8.4 滑动操作](#8.4 滑动操作)
      • [8.5 搜索 / 过滤](#8.5 搜索 / 过滤)
      • [8.6 排序](#8.6 排序)
      • [8.7 空状态 / 加载状态](#8.7 空状态 / 加载状态)
      • [8.8 键盘 / 手柄导航](#8.8 键盘 / 手柄导航)
      • [8.9 右键菜单](#8.9 右键菜单)
    • [9. 架构决策与权衡](#9. 架构决策与权衡)
      • [9.1 Dictionary vs List 存储 Item](#9.1 Dictionary vs List 存储 Item)
      • [9.2 C# Event vs UnityEvent](# Event vs UnityEvent)
      • [9.3 布局采用组合而非继承](#9.3 布局采用组合而非继承)
      • [9.4 没有 ViewModel 层](#9.4 没有 ViewModel 层)
    • [10. 类图](#10. 类图)

1. 概述

本项目在 Unity ScrollRect 之上实现了一套泛型双层抽象:ListView<TKey, TItem> + ListViewItem<TKey>。它为任何需要以下能力的滚动列表提供可复用的基础设施:

  • 数据驱动的 Item 增删
  • 单选状态管理(支持两种选中模式)
  • 三态视觉生命周期(Normal / Selected / Disabled)
  • 从 UI 点击到业务逻辑的干净事件传播链
  • 错落式入场动画

目前系统中有四个具体列表:

ListView Key 类型 Item 类型 用途
ConnectedDeviceListView uint(DeviceId) ConnectedDeviceItem 实时已连接传感器
MyDeviceListView uint(DeviceId) MyDeviceItem 历史设备列表
DeviceLibraryListView ProductType(枚举) DeviceLibraryItem 产品目录
SessionListView string(FilePath) SessionListItem 录制会话文件

2. 四层职责模型

架构在四个层次上强制实施关注点分离:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│  第四层:业务逻辑(ListView 子类)                                │
│  HandleItemSelected() --- 选中状态变化之后做什么                    │
│  例:切换 3D 可视化、发布领域事件、页面导航                        │
├─────────────────────────────────────────────────────────────────┤
│  第三层:选中状态机(ListView 基类)                               │
│  Select() / Deselect() --- 状态的唯一写入路径                      │
│  维护 _selectedKey、_hasSelection,触发 OnItemSelected            │
├─────────────────────────────────────────────────────────────────┤
│  第二层:点击路由(ListView 基类,private)                        │
│  OnItemClicked() --- 过滤 Disabled、处理 Toggle 模式               │
│  将原始点击翻译为 Select() / Deselect() 调用                     │
├─────────────────────────────────────────────────────────────────┤
│  第一层:UI 输入(ListViewItem 基类 + 子类)                      │
│  Button.onClick → NotifyClicked() → OnClicked 事件               │
│  纯信号:"这个 Item 被物理点击了"                                  │
└─────────────────────────────────────────────────────────────────┘

为什么要分四层?

每一层回答一个不同的问题:

  • 第一层回答:"有东西被点了吗?" ------ 纯 UI 输入,不含逻辑。
  • 第二层回答:"这次点击应该改变选中状态吗?" ------ 策略判断(是否禁用?是否已选中?Toggle 模式?)。
  • 第三层回答:"当前选中状态是什么?" ------ 单一事实来源,确定性状态转换。
  • 第四层回答:"应用应该对这次选中做什么?" ------ 业务逻辑,与 UI 机制完全解耦。

这意味着你可以:

  • 从代码直接调用 Select(key) 而无需模拟点击(直接进入第三层,跳过第一、二层)
  • 更改选中策略(StickySelect vs ToggleSelect)而不触碰业务逻辑
  • 新增一种 Item 视觉样式而不触碰选中逻辑
  • 替换整个选中后的业务响应而不触碰状态机

3. 事件流详解

3.1 完整调用链

复制代码
用户点击某个具体 Item 上的 Button(如 ConnectedDeviceItem)
    │
    │  Button.onClick.AddListener(NotifyClicked)     [Awake, 每个子类]
    ▼
NotifyClicked()                                       [ListViewItem:76]
    │  protected --- 只有 Item 自身能触发
    │  OnClicked?.Invoke(Key)
    ▼
OnClicked(ListViewItem 上的 C# event)                [ListViewItem:38]
    │  public event --- 外部代码可以 += 但不能 Invoke
    │  由 ListView.AddItem() 订阅:item.OnClicked += OnItemClicked
    ▼
OnItemClicked(TKey key)                               [ListView:196]
    │  private --- 内部路由,不可重写
    │  守卫:item 存在?未被禁用?
    │  点击已选中项 + ToggleSelect → Deselect()
    │  点击已选中项 + StickySelect → return(无操作)
    │  否则 → Select(key)
    ▼
Select(TKey key)                                      [ListView:131]
    │  public --- 也可由代码直接调用
    │  1. 旧选中项视觉复位(SetState Normal)
    │  2. 更新 _selectedKey、_hasSelection
    │  3. 新选中项视觉高亮(SetState Selected)
    │  4. 若 !SuppressSelectionEvents → OnItemSelected?.Invoke(key)
    ▼
OnItemSelected(ListView 上的 C# event)               [ListView:42]
    │  public event --- 由 ListView 子类订阅
    ▼
HandleItemSelected(TKey key)                          [各 ListView 子类]
    │  业务逻辑:发布领域事件、切换渲染器等
    ▼
(领域层响应:SensorSelectedEvent、可视化器切换等)

3.2 NotifyClicked 与 OnClicked:为什么需要两个?

这是经典的触发器/信号分离模式:

csharp 复制代码
// ListViewItem.cs
protected void NotifyClicked() => OnClicked?.Invoke(Key);  // 触发器(protected)
public event Action<TKey> OnClicked;                         // 信号(public event)

NotifyClicked 是触发器 ------ protected,只有 Item 子类自己能拉动它。设计上就是为了直接作为 UnityAction 传给 Button.onClick

csharp 复制代码
button.onClick.AddListener(NotifyClicked);  // 每个子类一行搞定

OnClicked 是信号 ------ 一个 C# event,这意味着:

  • 外部代码(ListView)可以订阅(+=)和取消订阅(-=
  • 外部代码不能 调用它 ------ 只有声明类自身能调用 Invoke()
  • 外部代码不能 置空它 ------ 类外 = null 会编译报错

如果 OnClicked 是一个普通的 Action<TKey> 字段而非 event,任何持有 Item 引用的代码都可以:

  • 伪造点击:item.OnClicked.Invoke(someKey) ------ 绕过真实 UI
  • 清除所有订阅者:item.OnClicked = null ------ 静默破坏 ListView
  • 替换委托:item.OnClicked = myHandler ------ 覆盖 ListView 的订阅

event 关键字阻止了以上三种情况。protected NotifyClicked() 方法则提供了一个受控的、唯一的触发入口。

3.3 OnItemClicked:私有路由器

csharp 复制代码
private void OnItemClicked(TKey key)  // ListView:196

这个方法刻意设为 private ------ 不是 protected,不是 virtual。它是基类的内部实现细节,子类永远不需要重写或调用它。它的工作纯粹是机械性的:

  1. 守卫:item 存在吗?是 Disabled 状态吗?→ 拒绝
  2. 点击已选中项 :这是当前已选中的 item 吗?
    • StickySelect → 无操作(选中是"粘性"的,点击不能取消)
    • ToggleSelect → 调用 Deselect()(再次点击切换取消)
  3. 点击新 item → 调用 Select(key)

保持 private 的好处是,基类保证了所有点击到选中的路由都走同一条代码路径。没有任何子类能意外创建一条绕过 Disabled 检查或 SelectionMode 逻辑的替代路径。

3.4 Select / Deselect:状态机

csharp 复制代码
public void Select(TKey key)   // ListView:131
public void Deselect()          // ListView:153

这两个方法是 public 的,因为选中不仅仅由点击驱动。代码需要:

  • 设备连接时自动选中第一个(ConnectedDeviceListView:172
  • 数据刷新后恢复之前的选中项(MyDeviceListView:117SessionListView:30
  • 录制进行中时程序化回退选中(ConnectedDeviceListView:231

状态机简单但严格:

  • 单一事实来源_selectedKey + _hasSelection
  • 视觉同步 :对受影响的 Item 调用 SetState(Selected) / SetState(Normal)
  • 事件发射OnItemSelected / OnItemDeselected,受 SuppressSelectionEvents 门控

3.5 SuppressSelectionEvents:静默状态恢复

csharp 复制代码
protected bool SuppressSelectionEvents;  // ListView:33

这个标志为一个特定场景而存在:重建列表时不触发业务副作用

MyDeviceListView.RefreshFromHistory() 中的示例:

csharp 复制代码
SuppressSelectionEvents = true;
Clear();                              // 正常情况下会触发 OnItemDeselected
// ... 重建所有 item ...
Select(selectedBefore.Value);         // 正常情况下会触发 OnItemSelected
SuppressSelectionEvents = false;

如果不抑制,Clear() 会触发 OnItemDeselected(关闭详情面板、停止可视化器),Select() 会触发 OnItemSelected(重新打开、重新启动)------ 导致可见的闪烁和不必要的开销。这个标志让代码静默恢复状态,就像什么都没发生过一样。

另一个用途:ConnectedDeviceListView 在录制进行中时回退选中:

csharp 复制代码
SuppressSelectionEvents = true;
Select(_previousSelectedKey.Value);   // 回退但不重新触发业务逻辑
SuppressSelectionEvents = false;

4. Item 状态机

4.1 三态

csharp 复制代码
public enum ItemState { Normal, Selected, Disabled }
复制代码
         Select()              Deselect() / 另一个 item 被选中
Normal ──────────► Selected ──────────────────────► Normal
  │                                                    ▲
  │  SetDisabled(true)              SetDisabled(false)  │
  ▼                                                    │
Disabled ──────────────────────────────────────────────┘

关键不变量:

  • Disabled 的 item 不能被选中 ------ Select() 检查 item.State == ItemState.Disabled 后直接返回
  • 选中一个 Disabled item 是无操作,不是错误
  • 禁用一个已选中的 item 会先自动取消选中 ------ SetDisabled() 在设置 Disabled 状态前先调用 Deselect()
  • 状态转换是幂等的 ------ SetState() 检查 if (State == state) return

4.2 通过 OnStateChanged 实现视觉定制

csharp 复制代码
protected abstract void OnStateChanged(ItemState state);  // ListViewItem:71

每个 Item 子类根据自身的视觉设计做不同实现:

SessionListItem ------ 简单的 Sprite 切换:

csharp 复制代码
protected override void OnStateChanged(ItemState state)
{
    background.sprite = State == ItemState.Selected ? frameSprites[1] : frameSprites[0];
}

ConnectedDeviceItem ------ 复杂的多因子视觉(ItemState × SensorStatus 交叉):

csharp 复制代码
protected override void OnStateChanged(ItemState state)
{
    ApplyOnlineDot();       // 根据 SensorStatus 显示/隐藏在线点
    ApplyInteractable();    // 根据 State 和 SensorStatus 设置按钮可交互性
    ApplyFrame();           // 3 种 Sprite:normal、grey(禁用/断开)、green(选中)
    ApplyTextColor();       // 根据 State 和 SensorStatus 设置文字颜色
}

MyDeviceItem ------ 按状态控制按钮可交互性 + 边框 + 颜色:

csharp 复制代码
protected override void OnStateChanged(ItemState state)
{
    // Normal:白色背景、frame[0]、按钮启用
    // Selected:白色背景、frame[1]、按钮启用
    // Disabled:暗色背景、frame[0]、所有按钮禁用
}

这是模板方法模式 ------ 基类控制状态变化何时 发生,子类控制看起来是什么样。


5. 数据流:Item 如何获取内容

基类刻意不知道 Item 显示什么数据。它只管理:

  • 创建/销毁(AddItem / RemoveItem / Clear
  • 选中状态
  • TKey 身份标识

内容绑定完全在子类层:

复制代码
ListView 子类                         ListViewItem 子类
─────────────────                    ──────────────────────
var item = AddItem(key);      →      item 被创建,Key 被设置
item.SetData(someStruct);     →      填充 TMP_Text、Image 等
item.SetSensorStatus(status); →      更新状态相关的视觉

每个 Item 子类定义自己的数据结构和 SetData() 方法:

Item 数据结构 主要字段
ConnectedDeviceItem ConnectedDeviceData DeviceId, ProductType, SensorName, PortName, Status
MyDeviceItem MyDeviceCardData DeviceId, DeviceName, ProductType, LastConnectedTicks, IsOnline
DeviceLibraryItem DeviceLibraryCardData ProductType, DisplayName, SensorPointCount, SdkSupported
SessionListItem SessionItemData FilePath, FileName, DeviceId, RecordTime, Duration, Integrity

这使基类保持泛型 ------ 它不需要知道传感器、会话或产品的任何信息。


6. 为什么不直接用 Unity 的 ScrollView?

Unity 的 ScrollRect + LayoutGroup 提供的能力:

  • 带惯性的可滚动视口
  • 自动子物体布局(垂直/水平/网格)
  • 内容尺寸自适应

仅此而已。让一个列表在真实应用中可用所需的其他一切,都得自己构建。以下是自定义 ListView 提供而原生 ScrollRect 不具备的能力:

6.1 选中状态管理

Unity 的 ScrollRect 中没有"哪个 item 被选中了"的概念。你需要自己:

  • 追踪选中项
  • 点击新 item 时处理旧 item 的取消选中
  • 决定点击已选中项时的行为
  • 阻止选中被禁用的 item
  • 处理选中项被移除的情况

所有这些都在 ListView 基类中,写一次,到处复用。

6.2 数据驱动的 Item 生命周期

用原生 ScrollRect,你需要手动 Instantiate() 预制体、维护自己的 DictionaryList、接线点击处理器、销毁时清理。每个列表都要重新实现一遍。ListView 提供:

csharp 复制代码
var item = AddItem(key);     // 实例化、注册、接线点击处理器、播放动画
RemoveItem(key);              // 取消注册、处理选中清理、销毁
Clear();                      // 批量清理并正确处理取消选中

6.3 类型安全的泛型 Key

ListView<TKey, TItem> 对 Key 类型是泛型的。ConnectedDeviceListViewuint(DeviceId),SessionListViewstring(FilePath),DeviceLibraryListViewProductType(枚举)。编译器保证你不会意外把 DeviceId 传给会话列表。

6.4 一致的三态视觉契约

系统中每个 item 都遵循相同的 Normal → Selected → Disabled 生命周期。基类强制执行状态转换;子类只实现视觉。没有这个,每个列表都会发明自己的状态管理,导致行为不一致(某些列表可能忘记处理"禁用一个已选中 item"的边界情况等)。

6.5 事件架构

原生 ScrollRect 不提供任何将"item X 被选中"传播到业务逻辑的机制。你得逐个 item 接线 Button.onClick,管理回调生命周期,并祈祷不会泄漏监听器。ListView 提供了一条干净的事件链,订阅/取消订阅的生命周期与 AddItem/RemoveItem/OnDestroy 绑定。

6.6 动画协调

错落式入场动画(PlayEnterAnimation 带逐项延迟)需要知道 item 在列表中的索引并协调各 item 的时序。在基类中实现很简单,每个列表重新实现则很痛苦。

6.7 能力对比总览

关注点 Unity ScrollRect ListView 基类 ListView 子类 ListViewItem 子类
滚动 + 布局 (委托给 ScrollRect) --- ---
Item 实例化/销毁 --- ---
基于 Key 的查找 ✓(Dictionary<TKey, TItem> --- ---
选中状态机 --- ---
选中模式(Sticky/Toggle) --- ---
禁用状态处理 --- ---
点击 → 选中路由 --- ---
事件抑制 --- ---
选中后的业务逻辑 --- ---
数据绑定(SetData) --- ---
视觉状态渲染 --- ---
入场动画 ✓(协调) --- ✓(渲染)

7. 使用的设计模式

7.1 模板方法(Template Method)

基类定义算法骨架;子类填充具体步骤:

  • ListViewItem.SetState() → 调用 OnStateChanged()(抽象方法,子类实现视觉)
  • ListView.AddItem() → 创建 item、接线事件 → 子类在之后调用 item.SetData()

7.2 观察者模式(Observer / Event-Driven)

四级事件链,每级有不同的可见性:

事件 声明位置 可见性 订阅者
Button.onClick Unity public NotifyClicked(单个 item)
OnClicked ListViewItem public event OnItemClicked(单个 ListView)
OnItemSelected ListView public event HandleItemSelected(子类 + 外部)
领域事件 CustomEventBus 全局 任意订阅者

7.3 简化的状态模式(State Pattern)

ItemState 枚举 + OnStateChanged() 分发 = 轻量级状态模式,无需独立的状态对象。在这里是合适的,因为状态少、转换简单。

7.4 泛型即契约(Generics as Contract)

ListView<TKey, TItem> where TItem : ListViewItem<TKey> ------ 泛型约束确保编译期安全:

  • ListView 和其 Item 之间的 Key 类型匹配
  • Item 保证拥有 ListViewItem 的 API(Key、State、OnClicked、SetState)
  • 无需类型转换,无运行时类型检查

8. 可扩展方向

8.1 多选模式

当前仅支持单选。扩展方式:

csharp 复制代码
public enum SelectionMode { StickySelect, ToggleSelect, MultiSelect }

需要的改动:

  • _selectedKeyHashSet<TKey> _selectedKeys
  • OnItemSelectedOnSelectionChanged(IReadOnlySet<TKey> selected)
  • OnItemClicked 中处理 Shift+Click / Ctrl+Click
  • 新增 SelectAll() / DeselectAll() 公开 API

影响范围:仅基类改动。基础多选不需要修改任何子类。

8.2 虚拟滚动(对象池化)

当前所有 item 都被实例化。对于数百个 item 的列表,这会浪费内存和布局计算时间。

虚拟滚动的做法:

  • 只实例化足够填满可见视口 + 缓冲区的 item
  • item 滚出视口时回收(Pool() / Activate()
  • 回收时重新绑定数据(SetData() 在每个 item 上已经存在)

当前的 AddItem / RemoveItem API 会变为内部使用;新增 SetDataSource(IList<TData>) API 驱动列表。

8.3 拖拽排序

需要新增:

  • ListViewItem 上的 IDraggableItem 接口
  • 拖拽手柄检测 + 视觉反馈
  • OnItemReordered(TKey key, int oldIndex, int newIndex) 事件
  • 可选:将顺序持久化到 PlayerPrefs 或配置文件

8.4 滑动操作

移动端风格的滑动展开操作(删除、归档、重命名):

  • ISwipeableItem 接口,GetSwipeActions() 返回操作描述符
  • 基类处理手势检测和操作面板动画
  • 子类定义每个 item 可用的操作

8.5 搜索 / 过滤

在基类中添加过滤谓词:

csharp 复制代码
public void SetFilter(Func<TKey, TItem, bool> predicate)
{
    foreach (var (key, item) in Items)
        item.gameObject.SetActive(predicate(key, item));
}

Item 保留在字典中(保持选中状态),但从视图中隐藏。ClearFilter() 恢复全部。

8.6 排序

csharp 复制代码
public void Sort(Comparison<TItem> comparison)
{
    var sorted = Items.Values.OrderBy(x => x, Comparer<TItem>.Create(comparison));
    int i = 0;
    foreach (var item in sorted)
        item.transform.SetSiblingIndex(i++);
}

利用 Unity LayoutGroup 尊重 sibling 顺序的特性。

8.7 空状态 / 加载状态

csharp 复制代码
[SerializeField] private GameObject emptyStateView;
[SerializeField] private GameObject loadingView;

protected void ShowEmptyState(bool show) => emptyStateView?.SetActive(show && Items.Count == 0);
protected void ShowLoading(bool show) => loadingView?.SetActive(show);

子类在数据加载期间调用。基类可以在 RemoveItem / ClearItems.Count == 0 时自动显示空状态。

8.8 键盘 / 手柄导航

csharp 复制代码
public void SelectNext()
{
    // 找到当前索引,选中 index+1(跳过 Disabled)
}

public void SelectPrevious()
{
    // 找到当前索引,选中 index-1(跳过 Disabled)
}

可与 Unity Input System 集成,支持方向键 / D-pad 导航。

8.9 右键菜单

右键或长按显示每个 item 的上下文菜单:

csharp 复制代码
// 在 ListViewItem 基类中
public event Action<TKey> OnContextMenu;
protected void NotifyContextMenu() => OnContextMenu?.Invoke(Key);

遵循与 NotifyClicked / OnClicked 相同的触发器/信号模式。


9. 架构决策与权衡

9.1 Dictionary vs List 存储 Item

基类使用 Dictionary<TKey, TItem> 而非 List<TItem>。这意味着:

  • O(1) 按 Key 查找(对 Select(key)GetItem(key)RemoveItem(key) 至关重要)
  • 不保证顺序(顺序由 Unity LayoutGroup + sibling index 决定)
  • 无基于索引的访问(需要 Items.Values.ElementAt(i)

对于 item 由业务 Key 标识而非位置标识的数据驱动列表,这是正确的权衡。

9.2 C# Event vs UnityEvent

代码库使用 C# event(而非 UnityEvent)进行所有组件间通信。原因:

  • 类型安全(泛型 Action<TKey> vs 装箱的 UnityEvent
  • 无序列化开销
  • Inspector 中不会出现始终在代码中接线的事件的杂乱条目
  • 正确的 += / -= 生命周期管理

9.3 布局采用组合而非继承

基类不控制布局。它完全委托给 container 上的 Unity LayoutGroup。这意味着:

  • 垂直列表?在预制体的 container 上添加 VerticalLayoutGroup
  • 网格?添加 GridLayoutGroup
  • 自定义?添加任何 LayoutGroup 子类

ListView 不关心。它只是将 item 实例化为 container 的子物体,让 Unity 处理定位。

9.4 没有 ViewModel 层

当前设计中 Item 直接持有 UI 引用(TMP_TextImage 等)并在 SetData() 中绑定数据。没有中间的 ViewModel。这是有意为之 ------ 对于 10-50 个 item、数据简单的列表,ViewModel 层增加复杂度却没有收益。如果 item 发展出复杂的派生状态或需要对显示逻辑做单元测试,ViewModel 才值得引入。


10. 类图

复制代码
                    ┌──────────────────────────┐
                    │   ListViewItem<TKey>      │  抽象基类
                    │──────────────────────────│
                    │ + Key: TKey               │
                    │ + State: ItemState        │
                    │ + OnClicked: event        │
                    │──────────────────────────│
                    │ # NotifyClicked()         │  ← 子类的触发器
                    │ + SetState(ItemState)     │  ← 由 ListView 调用
                    │ # OnStateChanged()        │  ← 抽象方法,视觉钩子
                    │ + PlayEnterAnimation()    │
                    └──────────┬───────────────┘
                               │ 继承
            ┌──────────────────┼──────────────────┬─────────────────┐
            ▼                  ▼                  ▼                 ▼
   ConnectedDeviceItem    MyDeviceItem    DeviceLibraryItem   SessionListItem
   (3-Sprite 边框、       (在线点、        (产品图片、        (完整性颜色、
    传感器状态、            跳转按钮、        SDK 标签)          时长格式化)
    文字颜色逻辑)          时间格式化)


                    ┌──────────────────────────┐
                    │   ListView<TKey, TItem>   │  抽象基类
                    │──────────────────────────│
                    │ # Items: Dictionary       │
                    │ + SelectedKey: TKey       │
                    │ + HasSelection: bool      │
                    │ + OnItemSelected: event   │
                    │ + OnItemDeselected: event │
                    │──────────────────────────│
                    │ # AddItem(key): TItem     │
                    │ + RemoveItem(key)         │
                    │ # Clear()                 │
                    │ + Select(key)             │
                    │ + Deselect()              │
                    │ + SetDisabled(key, bool)  │
                    │ - OnItemClicked(key)      │  ← 私有路由器
                    └──────────┬───────────────┘
                               │ 继承
            ┌──────────────────┼──────────────────┬─────────────────┐
            ▼                  ▼                  ▼                 ▼
  ConnectedDeviceListView  MyDeviceListView  DeviceLibraryListView  SessionListView
  (传感器事件、            (历史同步、       (静态目录、          (批量 SetData、
   可视化器控制、            在线追踪、         配置驱动)            保持选中)
   录制守卫、                跳转控制台)
   重连逻辑)
相关推荐
telllong2 小时前
深入理解React Fiber架构:从栈调和到时间切片
前端·react.js·架构
赋创小助手2 小时前
OpenClaw部署架构详解:从桌面到数据中心的AI Agent服务器选型指南
服务器·人工智能·架构·agent·openclaw
Wenzar_4 小时前
**零信任架构下的微服务权限控制:用Go实现基于JWT的动态访问策略**在现代云原生环境中,
java·python·微服务·云原生·架构
Juicedata5 小时前
分布式架构下配额设计:JuiceFS 的实现与典型案例
分布式·架构
Harvy_没救了5 小时前
【网络架构】Keepalived + LVS(DR) + MariaDB 双主备实践
网络·架构·lvs
seeInfinite10 小时前
MOE架构
架构
哑巴湖小水怪11 小时前
Android的架构是四层还是五层
android·架构
小白学大数据11 小时前
现代Python爬虫开发范式:基于Asyncio的高可用架构实战
开发语言·爬虫·python·架构
sghuter11 小时前
数字资源分发架构解密
后端·架构·dubbo