CocosCreator ToggleContainer避坑指南:TypeScript实战单选/多选组件封装
1. 项目概述:从ToggleContainer的“坑”说起
在CocosCreator里做UI,ToggleContainer这个组件几乎是绕不开的,尤其是当你需要实现单选、多选这类交互时。乍一看,官方文档和示例似乎挺清晰,但真到了项目里,尤其是配合TypeScript进行严谨开发时,各种“坑”就冒出来了。比如,为什么我的单选组里可以同时选中多个?为什么Toggle的状态切换了,但业务逻辑没触发?为什么用代码动态创建和管理的ToggleContainer总是不听使唤?这些问题,我都踩过,而且不止一次。
这个组件设计的初衷是好的,它把一组Toggle(开关)的管理逻辑封装起来,让你不用自己去写一堆“点这个、关那个”的循环代码。但它的灵活性和默认配置,恰恰是新手甚至有一定经验的开发者容易栽跟头的地方。今天这篇内容,就是把我这些年用ToggleContainer趟过的雷、总结的经验,结合TypeScript的完整实践代码,系统地梳理一遍。目标很明确:让你看完之后,不仅能正确区分和使用单选框、多选框,更能掌握一套稳健、可复用的代码方案,彻底避开那些常见的陷阱。
2. ToggleContainer核心机制深度解析
要避坑,首先得明白它到底是怎么工作的。ToggleContainer本身不是一个可见的UI元素,而是一个逻辑容器组件。它的核心作用是管理其节点下所有直接子节点上挂载的Toggle组件。
2.1 单选框模式与多选框模式的本质区别
很多人以为区别只是一个allowSwitchOff属性,其实远不止于此。关键在于ToggleContainer如何响应其子Toggle的点击事件。
在单选框模式下,ToggleContainer内部维护着一个“互斥”的逻辑。当任何一个子Toggle被设置为isChecked = true时,容器会自动将其他所有子Toggle的isChecked设置为false。这个过程是容器驱动的,是自动的。你手动去设置另一个Toggle为true,容器也会帮你关掉前一个。这里的“单选”是强制的,一组内必须且只能有一个被选中(除非allowSwitchOff为true,允许一个都没有)。
在多选框模式下,ToggleContainer基本“放弃”了管理选中状态的职责。它不会自动帮你取消其他Toggle的选中。每个Toggle都是独立的,选中状态由Toggle自身的点击事件或你的代码直接控制。此时,ToggleContainer更像是一个事件转发器或一个便捷的查询工具,它提供了一个checkEvents事件,用来批量监听其下所有Toggle的状态变化,仅此而已。
关键理解:单选框模式是“管理者”,多选框模式是“观察者”。这个根本定位决定了你后续所有代码的写法。
2.2 关键属性拆解与配置陷阱
allowSwitchOff(允许取消选择):- 单选框模式下的作用:默认为
false。当设置为true时,你点击当前已选中的Toggle,它会取消选中,并且不会自动选中组内其他Toggle。于是,整个组可以处于“无选中”状态。这是实现“单选且可取消”的关键。 - 多选框模式下的作用:几乎无影响。因为多选框模式下,容器不管理互斥,每个Toggle独立控制自己的
isChecked,自然可以自由地选中或取消。
- 单选框模式下的作用:默认为
checkEvents(选中事件): 这是一个Component.EventHandler数组,用于监听容器内任何一个Toggle的选中状态变化。无论单/多选模式,只要子Toggle的isChecked发生改变,就会触发这里绑定的事件。- 坑点1:事件触发非常频繁。在多选框模式下,你快速点击多个Toggle,会瞬间触发多次事件。如果你的回调函数里有耗时操作(如网络请求),需要谨慎处理,可能需要防抖。
- 坑点2:回调参数。事件回调会接收到一个
Toggle类型的参数,它就是状态发生变化的那个Toggle组件实例。但请注意,这个参数在Toggle被销毁后可能为null,需要做空值判断。
toggleItems(子元素数组): 这是一个Toggle[]类型的数组,通常由编辑器自动填充,包含了容器下所有子节点上的Toggle组件。你也可以在代码中动态修改它。- 动态管理的坑:如果你在运行时动态添加或移除子Toggle节点,必须同步更新
toggleItems数组,否则容器可能无法正确管理或监听新加的Toggle。这是一个极易遗漏的步骤。
- 动态管理的坑:如果你在运行时动态添加或移除子Toggle节点,必须同步更新
2.3 Toggle与ToggleContainer的事件流
理解事件触发顺序对调试至关重要:
- 用户点击一个Toggle。
- 该Toggle自身的
clickEvents被触发(如果设置了)。 - Toggle自身的
isChecked属性开始变化。 - ToggleContainer检测到其管理的子Toggle状态变化。
- ToggleContainer触发自身的
checkEvents。 - 如果处于单选框模式,ToggleContainer会执行互斥逻辑,修改其他Toggle的
isChecked,这将再次触发步骤4和5(对于状态被改变的Toggle)。
这意味着,在单选框模式下,一次点击可能引发多次checkEvents触发(一次是点击的目标,一次或多次是被容器强制关闭的其他Toggle)。你的业务逻辑需要能妥善处理这种情况。
3. 单选框与多选框的实战选型与设计
知道了原理,我们来看看在真实项目中如何做选择。这不是一个非此即彼的问题,而是一个基于交互设计的设计决策。
3.1 何时使用单选框模式
单选框的核心特征是“多选一,且选项互斥”。它的选择结果是一个确定的值。
典型场景:
- 性别选择:“男”、“女”、“保密”(通常不可取消)。
- 游戏难度选择:“简单”、“普通”、“困难”、“地狱”。
- 订单排序方式:“最新”、“最热”、“价格从低到高”。
- 设置项中的互斥选项:如“音效开/关”、“通知样式A/B”。
设计要点:
- 默认选中:通常需要设置一个默认选中项,以符合“必选其一”的预期。可以在编辑器预设,或在代码初始化时设置。
allowSwitchOff慎用:除非业务明确允许“不选择”(如某些筛选条件),否则保持为false。允许取消会增加用户的理解成本和操作步骤。- 视觉反馈强化:被选中的Toggle需要有非常明确的视觉区别(如颜色、边框、缩放),因为用户需要立刻知道当前是哪一个。
3.2 何时使用多选框模式
多选框的核心特征是“独立选择,可多选”。它的选择结果是一个集合。
典型场景:
- 兴趣标签选择:用户可以选择多个感兴趣的领域。
- 文件或物品的多选:在列表中选择多个项目进行操作。
- 技能或装备的多选配置:允许同时激活多个被动技能或装备多个饰品。
- 筛选器的多条件选择:如同时按“价格区间”、“品牌”、“颜色”进行筛选。
设计要点:
- 状态可空:初始状态通常全未选中,用户从零开始构建选择集。
- “全选/反选”功能:当选项较多时,提供一个“全选”Toggle是非常友好的设计。这需要你手动写代码遍历所有
toggleItems来设置状态。 - 结果汇总与展示:需要有一个区域实时显示已选中的项目数量或具体内容,因为用户可能记不住自己选了什么。
3.3 混合模式与复杂场景
有时界面需要更复杂的交互。例如,一个设置面板:
- 有一组“图形质量”的单选(低、中、高、自定义)。
- 当选择“自定义”时,下方展开一组多选框(阴影、抗锯齿、垂直同步等)供用户细调。
这种场景下,“图形质量”用一个ToggleContainer(单选)管理。“自定义”下的细项用另一个ToggleContainer(多选)管理,并通过代码控制第二个容器的显示/隐藏和交互状态。关键在于用多个ToggleContainer来划分不同的选择维度,而不是试图用一个容器实现所有逻辑。
4. TypeScript完整实践:从基础封装到高级应用
理论说再多,不如一行代码。下面我将提供一个从基础到进阶的TypeScript完整实现,包含健壮的类型定义和错误处理。
4.1 基础封装:创建可复用的ToggleGroupManager
首先,我们创建一个管理器类,将ToggleContainer的常用操作封装起来,并加入类型安全。
// ToggleGroupManager.ts import { _decorator, Component, Node, Toggle, ToggleContainer, EventHandler } from 'cc'; const { ccclass, property } = _decorator; export type ToggleValueType = string | number; // 假设每个Toggle对应一个值 export interface IToggleItem { node: Node; toggle: Toggle; value: ToggleValueType; } @ccclass('ToggleGroupManager') export class ToggleGroupManager extends Component { @property(ToggleContainer) public container: ToggleContainer | null = null; @property({ type: [Node] }) private toggleNodes: Node[] = []; // 在编辑器里关联子节点 private _toggleItems: Map<Toggle, IToggleItem> = new Map(); private _selectedValues: Set<ToggleValueType> = new Set(); // 用于多选 private _selectedValue: ToggleValueType | null = null; // 用于单选 // 事件回调定义 public onToggleChanged: ( (selectedValue: ToggleValueType | null, selectedValues: ToggleValueType[]) => void ) | null = null; protected onLoad() { this._initializeToggles(); this._setupContainerEvent(); } /** * 初始化,建立Toggle与值的映射关系 * 这里假设每个Toggle节点上有一个自定义组件(如ToggleData)来存储其代表的值 * 或者通过节点名、顺序来映射。这里展示通过顺序索引映射。 */ private _initializeToggles() { if (!this.container) { console.error('ToggleContainer is not assigned!'); return; } // 确保容器引用最新的子节点 this.container.toggleItems = []; for (let i = 0; i < this.toggleNodes.length; i++) { const node = this.toggleNodes[i]; const toggle = node.getComponent(Toggle); if (!toggle) { console.warn(`Node ${node.name} does not have a Toggle component.`); continue; } this.container.toggleItems.push(toggle); // 创建映射关系:这里简单用索引作为值,实际项目请替换为你的业务数据 const item: IToggleItem = { node, toggle, value: i, // 或从node.getComponent(YourDataComp).value获取 }; this._toggleItems.set(toggle, item); // 也可以监听每个Toggle自身的事件(如果需要) // toggle.node.on(Toggle.EventType.TOGGLE, this._onSingleToggle, this); } } private _setupContainerEvent() { if (!this.container) return; // 清除可能存在的旧事件 this.container.checkEvents = []; const eventHandler = new EventHandler(); eventHandler.target = this.node; // 事件接收者节点 eventHandler.component = 'ToggleGroupManager'; // 本组件脚本名 eventHandler.handler = '_onContainerToggleChanged'; // 回调方法名 eventHandler.customEventData = ''; // 可传递自定义数据 this.container.checkEvents.push(eventHandler); } // 容器事件回调 private _onContainerToggleChanged(toggle: Toggle) { const item = this._toggleItems.get(toggle); if (!item) return; const isSingleMode = !this.container?.allowSwitchOff || this.container?.toggleItems.some(t => t.isChecked); // 简化判断,实际应根据业务逻辑 if (isSingleMode) { // 单选框逻辑 this._selectedValue = toggle.isChecked ? item.value : null; this._selectedValues.clear(); if (toggle.isChecked) { this._selectedValues.add(item.value); } } else { // 多选框逻辑 if (toggle.isChecked) { this._selectedValues.add(item.value); } else { this._selectedValues.delete(item.value); } // 单选模式下_selectedValue意义不大,可置为null或第一个选中的值 this._selectedValue = this._selectedValues.size > 0 ? Array.from(this._selectedValues)[0] : null; } // 触发外部回调 if (this.onToggleChanged) { this.onToggleChanged( this._selectedValue, Array.from(this._selectedValues) ); } // 可以在这里更新UI,比如显示当前选中的值 this._updateSelectionDisplay(); } private _updateSelectionDisplay() { // 实现你的UI更新逻辑,例如更新一个Label文本 // console.log(`单选值: ${this._selectedValue}, 多选集合: [${Array.from(this._selectedValues).join(',')}]`); } // ---------- 对外提供的方法 ---------- /** * 获取当前选中的值(单选模式下返回单个值,多选模式下返回第一个选中的值或null) */ public getSelectedValue(): ToggleValueType | null { return this._selectedValue; } /** * 获取当前选中的值集合(多选模式) */ public getSelectedValues(): ToggleValueType[] { return Array.from(this._selectedValues); } /** * 设置选中状态(根据值) * @param value 要选中的值 * @param isMulti 是否以多选模式操作。true: 添加选中;false: 单选模式,只选中这一个。 */ public selectByValue(value: ToggleValueType, isMulti: boolean = false): boolean { for (const item of this._toggleItems.values()) { if (item.value === value) { if (isMulti) { // 多选模式:切换状态 item.toggle.isChecked = !item.toggle.isChecked; } else { // 单选模式:确保只有这个被选中 if (!item.toggle.isChecked) { item.toggle.isChecked = true; } // 注意:在单选框模式下,设置一个为true,容器会自动关闭其他,但这里直接触发事件逻辑 } return true; } } console.warn(`Value ${value} not found in toggle group.`); return false; } /** * 清除所有选中状态 */ public clearSelection() { this._selectedValues.clear(); this._selectedValue = null; if (this.container) { for (const toggle of this.container.toggleItems) { toggle.isChecked = false; } } this._updateSelectionDisplay(); } }这个管理器提供了:
- 类型安全:使用TypeScript接口和泛型(示例中为
ToggleValueType)。 - 统一事件处理:通过一个
onToggleChanged回调暴露变化,参数清晰。 - 便捷的API:可以通过值来选中/取消选中,获取当前选择结果。
- 与编辑器友好集成:通过
@property装饰器暴露配置项。
4.2 在编辑器中的配置与使用
- 创建一个空节点作为容器,挂载
ToggleContainer组件。 - 创建多个Toggle节点作为其子节点(例如,一些Button节点,添加Toggle组件)。
- 创建一个管理节点,挂载上面编写的
ToggleGroupManager脚本。 - 在
ToggleGroupManager组件的container属性中,拖入第1步的容器节点。 - 在
toggleNodes数组中,按顺序拖入所有的Toggle子节点。 - 在你的业务逻辑脚本中,获取
ToggleGroupManager组件实例,并监听其onToggleChanged事件。
// 业务逻辑示例:SomeUI.ts import { _decorator, Component, Label } from 'cc'; import { ToggleGroupManager } from './ToggleGroupManager'; const { ccclass, property } = _decorator; @ccclass('SomeUI') export class SomeUI extends Component { @property(ToggleGroupManager) public difficultyGroup: ToggleGroupManager | null = null; // 难度单选组 @property(ToggleGroupManager) public tagGroup: ToggleGroupManager | null = null; // 标签多选组 @property(Label) public selectionLabel: Label | null = null; protected onLoad() { if (this.difficultyGroup) { this.difficultyGroup.onToggleChanged = (singleVal, _) => { console.log(`难度变更为: ${singleVal}`); this.updateDisplay(); }; } if (this.tagGroup) { this.tagGroup.onToggleChanged = (_, multiVals) => { console.log(`选中标签: ${multiVals.join(', ')}`); this.updateDisplay(); }; } } private updateDisplay() { if (!this.selectionLabel) return; const diff = this.difficultyGroup?.getSelectedValue() ?? '未选择'; const tags = this.tagGroup?.getSelectedValues().join(', ') ?? '无'; this.selectionLabel.string = `难度: ${diff}\n标签: ${tags}`; } // 一个“全选”按钮的回调 public onSelectAllTags() { if (!this.tagGroup) return; // 这里需要知道所有可能的值,假设是0,1,2,3 for (let i = 0; i < 4; i++) { this.tagGroup.selectByValue(i, true); // 多选模式添加 } } }4.3 动态创建与管理ToggleGroup
有时选项需要根据数据动态生成。这时就不能依赖编辑器绑定了。
// DynamicToggleGroup.ts import { _decorator, Component, Prefab, instantiate, Node, Toggle } from 'cc'; import { ToggleGroupManager } from './ToggleGroupManager'; const { ccclass, property } = _decorator; @ccclass('DynamicToggleGroup') export class DynamicToggleGroup extends Component { @property(Prefab) public toggleItemPrefab: Prefab | null = null; // 一个预设好的Toggle项Prefab @property(Node) public containerNode: Node | null = null; // 作为父容器的节点 private _groupManager: ToggleGroupManager | null = null; protected start() { this._groupManager = this.getComponent(ToggleGroupManager); if (!this._groupManager) { this._groupManager = this.addComponent(ToggleGroupManager); } if (!this.containerNode) { this.containerNode = this.node; } this._groupManager.container = this.containerNode.getComponent(ToggleContainer); if (!this._groupManager.container) { console.error('Container node must have a ToggleContainer component.'); return; } this.generateToggles(['选项A', '选项B', '选项C', '选项D']); } /** * 根据数据动态生成Toggle项 * @param items 选项文本数组 */ public generateToggles(items: string[]) { if (!this._groupManager || !this.toggleItemPrefab || !this.containerNode) return; // 清除旧项 this.containerNode.removeAllChildren(); this._groupManager['_toggleItems'].clear(); // 注意:这里访问了私有属性,更好的做法是在管理器提供清理接口 const toggleItems: Toggle[] = []; for (let i = 0; i < items.length; i++) { const itemNode = instantiate(this.toggleItemPrefab); itemNode.parent = this.containerNode; itemNode.name = `ToggleItem_${i}`; const toggleComp = itemNode.getComponent(Toggle); if (!toggleComp) continue; // 假设Prefab里有个Label子节点来显示文本 const label = itemNode.getComponentInChildren(Label); if (label) { label.string = items[i]; } // 这里可以给itemNode添加一个自定义组件来存储其值 // const dataComp = itemNode.getComponent(ToggleData) || itemNode.addComponent(ToggleData); // dataComp.value = i; toggleItems.push(toggleComp); } // **关键步骤**:必须将动态生成的Toggle数组赋给Container if (this._groupManager.container) { this._groupManager.container.toggleItems = toggleItems; } // 同时需要更新管理器的内部映射(这里需要扩展管理器提供方法) this._groupManager['_updateToggleItemsMap'](toggleItems); // 假设有这个方法 } }动态创建的核心在于:生成节点后,必须将得到的Toggle组件数组,显式地赋值给ToggleContainer组件的toggleItems属性,并同步更新你自己的管理逻辑。
5. 避坑指南与常见问题排查
结合上面的代码,我们来系统性地盘点那些最容易出问题的地方和解决方案。
5.1 坑一:单选框模式下,多个Toggle被同时选中
现象:明明allowSwitchOff是false,却可以同时选中多个。原因排查:
- 子Toggle节点层级错误:
ToggleContainer只管理直接子节点上的Toggle。如果Toggle嵌套在孙子节点或更深层,容器将无法管理它。检查节点层级。 toggleItems数组未正确同步:如果Toggle是动态创建或通过代码添加的,没有将其加入到容器的toggleItems数组中。容器根本“不知道”这个Toggle的存在。- 手动干扰了Toggle状态:在代码中直接设置
toggle.isChecked = true,但可能在其他地方有逻辑错误,导致互斥逻辑被绕过。确保状态变更都通过容器或统一的管理器进行。 - 存在多个ToggleContainer影响:一个Toggle节点同时被多个ToggleContainer管理(例如,不小心被嵌套在了两个容器里),会导致行为不可预测。
解决方案:
- 使用我们封装的
ToggleGroupManager,通过它提供的selectByValue等方法来操作选中状态,避免直接操作toggle.isChecked。 - 动态操作节点后,务必调用类似
_initializeToggles的方法来重建容器与Toggle的关联。 - 在编辑器中仔细检查节点父子关系。
5.2 坑二:事件不触发或触发多次
现象:点击Toggle,checkEvents里绑定的函数没执行,或者执行了无数次。原因与解决:
- 事件未绑定或绑定错误:
- 检查编辑器绑定:确保
checkEvents数组里正确设置了目标节点、组件名和方法名。组件名是脚本的文件名(不含.ts),区分大小写。 - 检查代码绑定:如果代码绑定,确保
EventHandler对象创建正确,并push到了checkEvents数组。注意,直接赋值this.container.checkEvents = [handler]会覆盖原有事件。
- 检查编辑器绑定:确保
- 回调函数声明错误:
checkEvents要求的方法必须是组件类的公共方法。如果是私有方法private _onToggle(),事件系统将无法调用。确保方法是public的,或者至少不是private。 - 触发多次(单选框模式):如前所述,单选框模式下,选中一个会触发其事件,容器关闭另一个也会触发另一个的事件。这是正常现象。如果你的逻辑不希望被触发多次,可以在回调函数开头进行判断,或者使用一个标志位来忽略由容器互斥逻辑引发的后续事件。
private _processingEvent = false; private _onContainerToggleChanged(toggle: Toggle) { if (this._processingEvent) return; // 防止重入 this._processingEvent = true; // ... 你的业务逻辑 ... this._processingEvent = false; }
5.3 坑三:动态生成的Toggle无法交互或状态异常
现象:运行时创建的Toggle,点击没反应,或者状态显示不正常。原因与解决:
- 缺少Toggle组件:实例化Prefab后,确认节点上确实有
Toggle组件。 - 未添加到容器管理列表:这是最主要的原因。必须在执行
itemNode.parent = this.containerNode;之后,将获取到的toggleComp添加到this.container.toggleItems数组中。 - 节点未激活:确保实例化后的节点
active属性为true。 - 碰撞组件问题:Toggle的交互依赖节点上的碰撞组件(如
UITransform)。检查你的Toggle预制体是否包含必要的碰撞组件,并且尺寸正确。
5.4 坑四:与ScrollView等滚动容器嵌套时的问题
现象:Toggle在ScrollView里,有时候点击会同时触发滚动和切换,体验很差。解决方案:
- 使用
ScrollView的cancelInnerEvents属性:将其设置为true,当在滚动区域内滑动时,会阻止子节点(如Toggle)的点击事件被触发,优化滚动体验。 - 合理设计触摸逻辑:区分“点击”和“拖拽”。可以通过监听
ScrollView的scroll-start等事件,在开始滚动时暂时禁用Toggle的interactable属性,滚动结束后再启用。但这需要较精细的控制。 - 调整Toggle碰撞区域:确保Toggle的碰撞组件大小合适,不要过大,避免误触。
5.5 性能与内存管理
- 事件泄露:如果你在代码中使用了
toggle.node.on(Toggle.EventType.TOGGLE, ...),一定要在组件销毁时(onDestroy)或节点移除时使用toggle.node.off进行注销。使用ToggleContainer的checkEvents通常由引擎自动管理,风险较小。 - 动态创建与销毁:大量动态创建Toggle时,务必使用对象池。销毁节点前,记得将其从
ToggleContainer.toggleItems数组中移除,并将管理器内的相关引用置空,避免内存泄漏。
6. 进阶技巧与最佳实践
6.1 与数据驱动框架结合
在MVVM或类似数据驱动的架构中,我们希望Toggle的选中状态与一个数据模型绑定。
// 假设有一个可观察的数据模型 class SettingsModel { @observable difficulty: number = 1; // 0:简单, 1:普通, 2:困难 @observable enabledFeatures: Set<string> = new Set(['sound', 'vibration']); } // 在UI组件中 public model: SettingsModel = new SettingsModel(); protected onLoad() { // 监听模型变化,更新Toggle this.model.on('difficultyChanged', (newVal) => { this.difficultyGroup.selectByValue(newVal, false); }); this.model.on('enabledFeaturesChanged', (newSet) => { this.featureGroup.clearSelection(); newSet.forEach(feature => { this.featureGroup.selectByValue(feature, true); }); }); // 监听Toggle变化,更新模型 this.difficultyGroup.onToggleChanged = (val) => { if (val !== null) this.model.difficulty = val as number; }; this.featureGroup.onToggleChanged = (_, vals) => { this.model.enabledFeatures = new Set(vals as string[]); }; }6.2 自定义Toggle外观与交互
CocosCreator的Toggle组件允许你自定义CheckMark(选中标记)节点和Background(背景)节点。你可以充分利用这一点:
CheckMark:可以不是对勾,可以是任何你想要的图标、图片甚至动画节点。Background:可以绑定一个Sprite组件,根据isChecked状态切换不同的精灵图帧,实现更丰富的视觉效果。- 添加额外反馈:在Toggle的
clickEvents或通过代码监听状态变化,添加音效、粒子特效或简单的缩放动画,提升交互体验。
// 在ToggleGroupManager的_onContainerToggleChanged中 private _onContainerToggleChanged(toggle: Toggle) { const item = this._toggleItems.get(toggle); if (item) { // 添加一个简单的选中动画 item.node.stopAllActions(); if (toggle.isChecked) { item.node.scale = new Vec3(1.1, 1.1, 1); tween(item.node).to(0.1, { scale: new Vec3(1, 1, 1) }).start(); } // 播放音效 AudioManager.instance.playEffect('toggle_click'); } // ... 其余逻辑 ... }6.3 单元测试建议
对于复杂的ToggleGroup逻辑,编写单元测试是保证质量的好方法。
// 使用Jest或类似框架 describe('ToggleGroupManager', () => { let manager: ToggleGroupManager; let mockToggles: Toggle[]; beforeEach(() => { manager = new ToggleGroupManager(); // 创建模拟的Toggle和Container mockToggles = [/* ... 创建模拟对象 ... */]; manager['_toggleItems'] = new Map(); // 访问私有属性进行测试准备 // ... 初始化manager ... }); test('should select single value correctly', () => { manager.selectByValue(1, false); expect(manager.getSelectedValue()).toBe(1); expect(manager.getSelectedValues()).toEqual([1]); // 验证其他Toggle是否被取消选中(模拟容器行为) }); test('should handle multi-select correctly', () => { // 模拟多选模式 manager.selectByValue(1, true); manager.selectByValue(2, true); expect(manager.getSelectedValues().sort()).toEqual([1, 2]); }); });ToggleContainer是CocosCreator UI系统中一个功能明确但细节颇多的组件。处理得当,它能极大简化单选/多选交互的开发;处理不当,则会让你的项目充满难以调试的Bug。核心在于理解其“管理者”与“观察者”的双重角色,并始终确保动态操作时数据(toggleItems数组)与视图(节点树)的同步。采用一个强类型的Manager类进行封装,是隔离复杂度、提高代码可维护性和复用性的最佳实践。希望这篇结合了原理、代码和实战经验的指南,能让你在下次使用ToggleContainer时,更加得心应手。