第 23 课
实现菜单导航逻辑
上一课已经完成导航栏的视觉结构,本课继续为它加入实际行为。我们先明确导航系统需要保存哪些状态,再编写完整控制器,最后创建三个页面、配置导航项和指针事件。完成后,任意时刻只显示一个页面,当前栏目始终有清楚的蓝色提示。
让菜单按钮真正切换页面
把“介绍、导航、设置”三个按钮与对应页面连接起来,并加入悬停提示和持续选中效果。你会从数据、状态和事件三个角度读懂 NavbarController 的完整运行逻辑。
学完你将能够
学习目标
- 区分普通、悬停和选中三种视觉状态,并说明选中状态为什么必须持续保留。
- 解释一个 NavItem 如何把按钮、目标页面和高亮标识绑定为一组数据。
- 理解列表索引从 0 开始,以及 currentIndex 为何使用 -1 表示尚未选中。
- 说明 Start、OnValidate、点击事件和指针事件分别在什么时候执行。
- 读懂自动寻找 Indicator、验证对象引用和验证索引范围的代码。
- 按顺序说明初始化、默认选页、按钮点击、页面切换和高亮更新的完整调用链。
- 组合并保存完整的 NavbarController.cs,在 Unity 中完成编译。
- 创建三个内容页面,正确配置三组 Nav Items 与三个 Event Trigger。
- 完成首次打开、悬停、点击、重复点击、隐藏和重新打开的连续操作。
开始操作前
准备工作
- 打开主场景,确认 MenuUI 已经能够显示、隐藏和拖动。
- 确认 MenuUI 下存在 ContentPanel、NavigationBar 和 DragHandle。
- 确认 NavigationBar 下有 IntroductionButton、NavigationButton、SettingsButton。
- 确认每个按钮下都有名称以 Indicator 结尾的高亮图片。
- 确认世界空间界面能够接收鼠标、XR 射线或交互模拟器输入。
- 打开层级面板、属性面板、场景视图和控制台。
- 退出运行状态并保存场景,再开始编写脚本。
本课学习路线
按照学习模块逐步完成
每个模块都包含理解、跟随操作和独立实践。先整理引用与事件,再自动打开第一项
理解 Start、OnValidate 与自动补全
Start 负责菜单第一次启用时的运行初始化,OnValidate 负责编辑器中的配置辅助。两者都会使用自动寻找方法,但调用时机和目的不同。 private void Start()
{
InitializeNavigation();
AutoSelectFirstValidItem();
}
// 在编辑器中自动补全按钮子物体中的高亮指示器。
private void OnValidate()
{
ResolveAutomaticReferences();
}Start 的两行顺序不能颠倒;OnValidate 只负责调用自动补全。
两个生命周期方法的区别
生命周期方法由 Unity 在特定时机调用,不需要你在组件面板中手工连接。
左右滑动表格,可查看完整内容
| 方法 | 何时调用 | 本课职责 |
|---|---|---|
| Start | 组件第一次启用后、第一帧更新前 | 初始化导航,再打开第一项有效页面 |
| OnValidate | 编辑器加载脚本或修改配置时 | 尝试补全 Indicator 引用 |
为什么 Start 的顺序不能颠倒
InitializeNavigation 会补全引用、关闭全部有效页面和高亮,并注册点击事件。AutoSelectFirstValidItem 随后才寻找第一项有效配置并打开它。
如果先打开第一页,再执行初始化,初始化会把刚打开的页面再次关闭。Start 在组件生命周期中只调用一次,菜单隐藏再显示并不会销毁组件,所以 currentIndex 会保留上一次选择。
// 根据约定自动查找按钮子物体中名称以 Indicator 结尾的 Image。
private void ResolveAutomaticReferences()
{
if (navItems == null)
return;
foreach (NavItem item in navItems)
{
if (item == null || item.button == null || item.Indicator != null)
continue;
Image[] images = item.button.GetComponentsInChildren<Image>(true);
foreach (Image image in images)
{
if (image != item.button.image &&
image.name.EndsWith("Indicator", StringComparison.OrdinalIgnoreCase))
{
item.Indicator = image;
break;
}
}
}
}自动查找只补空引用,不覆盖已经手工拖入的对象。
自动查找的判断顺序
每一步都在缩小搜索范围,直到找到第一个符合命名规则的高亮图片。
左右滑动表格,可查看完整内容
| 步骤 | 代码含义 | 结果 |
|---|---|---|
| 1 | navItems == null | 列表不存在时结束整个方法 |
| 2 | foreach | 依次处理每个导航项 |
| 3 | continue | 空成员、空按钮或已有引用时跳过本项 |
| 4 | GetComponentsInChildren<Image>(true) | 搜索按钮及其全部子对象,包含隐藏对象 |
| 5 | image != item.button.image | 排除按钮根对象的目标图片 |
| 6 | EndsWith(Indicator) | 名称必须以 Indicator 结尾,比较时忽略大小写 |
| 7 | break | 找到第一个正确图片后结束内层循环 |
continue、break 与 return
三个关键字结束的范围不同。
左右滑动表格,可查看完整内容
| 关键字 | 影响范围 | 本课用途 |
|---|---|---|
| continue | 跳过本轮循环 | 当前导航项不需要自动补全 |
| break | 结束当前这一层循环 | 已经找到合适图片 |
| return | 结束整个方法 | 列表不存在或无需继续 |
推演自动寻找过程
选择一个默认隐藏的 NavigationIndicator,按代码顺序说明它如何被找到。
- 确认它位于 NavigationButton 的子层级。
- 确认对象上有 Image 组件。
- 确认名称以 Indicator 结尾。
- 说明参数 true 为什么不能省略。
危险输入在访问对象之前就被拦截
在真正切换前先验证对象和索引
导航切换会访问列表成员和场景对象。集中验证可以阻止空引用和索引越界,并让多个方法使用同一套判断规则。 private bool ValidateNavItem(NavItem item)
{
bool isValid = item != null &&
item.button != null &&
item.targetPanel != null &&
item.Indicator != null;
if (!isValid)
{
Debug.LogWarning("发现无效导航项配置,请检查以下对象:\n" +
$"Button: {item?.button?.name ?? "null"}\n" +
$"Panel: {item?.targetPanel?.name ?? "null"}\n" +
$"Indicator: {item?.Indicator?.name ?? "null"}");
}
return isValid;
}Button、Target Panel 和 Indicator 三个引用必须同时存在。
四个条件为什么按这个顺序排列
&& 表示并且,四个条件全部成立时 isValid 才为 true。item != null 放在最前面后,如果成员自身为空,后面的字段访问不会继续执行,从而避免空引用。
! 表示逻辑取反,所以 if (!isValid) 的意思是当前配置无效。方法会把缺少的 Button、Panel 或 Indicator 写入控制台,最后仍然返回 true 或 false 给调用者决定是否继续。
提示文字中的符号
这些符号让提示在对象为空时仍能安全生成。
左右滑动表格,可查看完整内容
| 符号 | 含义 | 示例结果 |
|---|---|---|
| $ | 在文字中插入表达式结果 | 显示实际对象名称 |
| ?. | 对象存在时继续访问,为空时得到空值 | 避免空引用 |
| ?? | 左边为空时使用备用值 | 显示 null |
| \n | 换行 | 三个引用分别占一行 |
private bool IsValidIndex(int index)
{
return index >= 0 && index < navItems.Count;
}索引必须不小于 0,并且严格小于成员数量。
成员数量为 3 时的索引结果
Count 表示列表中的成员数量,不是最后一个索引。
左右滑动表格,可查看完整内容
| 索引 | 结果 | 原因 |
|---|---|---|
| -1 | 无效 | 小于 0 |
| 0 | 有效 | 第一项 |
| 1 | 有效 | 第二项 |
| 2 | 有效 | 第三项 |
| 3 | 无效 | 不小于成员数量 3 |
分别推演两种无效输入
代入“缺少 Target Panel”和“传入索引 3”,说明程序在哪一步停止。
- 指出 ValidateNavItem 负责引用完整性。
- 指出 IsValidIndex 负责索引范围。
- 说明为什么验证必须发生在真正访问和切换之前。
先保护当前页面,再关闭旧项并打开新项
让页面切换与蓝色高亮同步更新
按钮点击只负责报告目标索引,真正的切换规则全部集中在 SwitchToPanel。页面和高亮由 SetItemActive 成对更新,避免两者不同步。 private void OnNavButtonClick(int targetIndex)
{
SwitchToPanel(targetIndex);
}
private void SwitchToPanel(int newIndex)
{
if (!IsValidIndex(newIndex) || newIndex == currentIndex)
return;
NavItem newItem = navItems[newIndex];
if (!ValidateNavItem(newItem))
return;
if (IsValidIndex(currentIndex))
{
NavItem previousItem = navItems[currentIndex];
if (ValidateNavItem(previousItem))
SetItemActive(previousItem, false);
}
SetItemActive(newItem, true);
currentIndex = newIndex;
}
private void SetItemActive(NavItem item, bool isSelected)
{
item.targetPanel.SetActive(isSelected);
SetBtnVisualState(item, isSelected, false);
}
private void SetBtnVisualState(NavItem item, bool isSelected, bool isHovered)
{
if (item == null || item.Indicator == null)
return;
bool shouldShow = isSelected || isHovered;
item.Indicator.gameObject.SetActive(shouldShow);
if (shouldShow)
item.Indicator.color = isSelected ? selectedColor : hoverColor;
}这段代码包含从按钮索引到页面与高亮的核心状态链。
SwitchToPanel 的保护顺序
新项验证必须发生在关闭旧项之前。
左右滑动表格,可查看完整内容
| 顺序 | 操作 | 目的 |
|---|---|---|
| 1 | 拦截越界索引或重复点击 | 避免无效和重复操作 |
| 2 | 验证 newItem 三个引用 | 新项错误时保留当前页面 |
| 3 | 找到并关闭 previousItem | 关闭旧页面与旧蓝色高亮 |
| 4 | 打开 newItem | 显示新页面与新蓝色高亮 |
| 5 | 更新 currentIndex | 让状态变量与界面保持一致 |
SetItemActive 为什么同时做两件事
SetItemActive 会调用 targetPanel.SetActive 控制整个页面,再调用 SetBtnVisualState 更新 Indicator。两种变化放在同一个方法里,可以避免只切页却忘记移动高亮。
SetBtnVisualState 先计算 shouldShow = isSelected || isHovered。条件运算符 isSelected ? selectedColor : hoverColor 表示选中时使用蓝色,否则使用深灰色,所以选中状态拥有更高优先级。
高亮状态计算表
四种输入组合最终只产生隐藏、深灰色或蓝色三种结果。
左右滑动表格,可查看完整内容
| isSelected | isHovered | 是否显示 | 颜色 |
|---|---|---|---|
| false | false | 否 | 无 |
| false | true | 是 | 深灰色 |
| true | false | 是 | 蓝色 |
| true | true | 是 | 蓝色 |
从介绍页切换到导航页
- 01
按钮监听传入索引 1。
- 02
OnNavButtonClick(1) 转交给 SwitchToPanel(1)。
- 03
确认 1 有效,并且不等于当前索引 0。
- 04
验证导航组的按钮、页面和高亮引用完整。
- 05
关闭索引 0 的介绍页面和蓝色高亮。
- 06
打开索引 1 的导航页面和蓝色高亮。
- 07
把 currentIndex 从 0 更新为 1。
推演从索引 0 切换到索引 2
逐行写出 previousItem、newItem 和 currentIndex 的变化。
- 先指出哪些条件会提前结束方法。
- 确认新项验证发生在旧项关闭之前。
- 确认页面与高亮始终成对改变。
悬停只改变临时反馈,不改变当前页面
处理指针进入、离开与三条运行路径
Pointer Enter 与 Pointer Exit 只处理未选中按钮的深灰色提示。当前按钮会在判断处提前结束,因此蓝色不会被覆盖或隐藏。 // 用于 Event Trigger:指针进入
public void OnPointerEnter(int itemIndex)
{
if (!IsValidIndex(itemIndex) || itemIndex == currentIndex)
return;
SetBtnVisualState(navItems[itemIndex], false, true);
}
// 用于 Event Trigger:指针离开
public void OnPointerExit(int itemIndex)
{
if (!IsValidIndex(itemIndex) || itemIndex == currentIndex)
return;
SetBtnVisualState(navItems[itemIndex], false, false);
}这两个 public 方法由 Event Trigger 从属性面板中调用。
点击事件与指针事件怎样绑定
两种事件来自不同位置,但最终共享同一个 currentIndex 和视觉状态方法。
左右滑动表格,可查看完整内容
| 事件 | 绑定位置 | 绑定时机 |
|---|---|---|
| Button.onClick | InitializeNavigation 中的 AddListener | 菜单第一次启用时 |
| Pointer Enter | 按钮的 Event Trigger | 编辑场景时手工配置 |
| Pointer Exit | 按钮的 Event Trigger | 编辑场景时手工配置 |
观察重点:首次启用只执行一次;按钮点击更新页面与 currentIndex;指针事件只改变未选中按钮的临时高亮。
手机上可左右滑动;点击图片可以查看完整图这些方法都不是逐帧执行
首次启用路径只在组件第一次启用时执行,点击路径只在按钮被点击时执行,指针路径只在进入或离开事件发生时执行。
事件方法结束后,程序等待下一次输入。已经设置好的页面显示状态和 Indicator 颜色会自然保留,不需要每一帧重复设置。
推演三种指针情况
假设 currentIndex = 2,分别推演进入索引 1、离开索引 1、离开索引 2。
- 进入未选中按钮时显示深灰色。
- 离开未选中按钮时隐藏深灰色。
- 离开当前按钮时提前结束并保留蓝色。
相互依赖的方法完整后再保存回 Unity
组合完整 NavbarController 并统一编译
前面已经分别理解了数据、初始化、验证、切换、视觉状态和指针事件。现在使用完整文件统一保存和编译。确认文件名与类名一致
- 01
在项目资源面板中打开 Assets/_Scripts。
- 02
如果已有 NavbarController.cs,直接打开;如果没有,新建 C# 脚本并命名为 NavbarController。
- 03
确认文件名 NavbarController.cs 与类名 NavbarController 完全一致。
- 04
用下面的完整代码替换文件。
- 05
确认最后一个右花括号也已复制,再保存。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// 导航栏控制器
public class NavbarController : MonoBehaviour
{
[Serializable]
public class NavItem
{
[Tooltip("导航栏按钮组件")]
public Button button;
[Tooltip("需要控制显示/隐藏的右侧页面")]
public GameObject targetPanel;
[Tooltip("高亮指示器;如果它是按钮的子物体,可自动查找")]
public Image Indicator;
}
[Header("导航项配置")]
public List<NavItem> navItems = new List<NavItem>();
[Header("外观设置")]
public Color selectedColor = new Color(0f, 0.282f, 1f, 1f);
public Color hoverColor = new Color(0.122f, 0.122f, 0.122f, 1f);
// 当前选中的索引,-1 表示没有选中项
private int currentIndex = -1;
private void Start()
{
InitializeNavigation();
AutoSelectFirstValidItem();
}
// 在编辑器中自动补全按钮子物体中的高亮指示器。
private void OnValidate()
{
ResolveAutomaticReferences();
}
private void InitializeNavigation()
{
ResolveAutomaticReferences();
for (int i = 0; i < navItems.Count; i++)
{
NavItem item = navItems[i];
if (!ValidateNavItem(item))
continue;
SetItemActive(item, false);
int itemIndex = i;
item.button.onClick.AddListener(() => OnNavButtonClick(itemIndex));
}
}
private void OnNavButtonClick(int targetIndex)
{
SwitchToPanel(targetIndex);
}
private void SwitchToPanel(int newIndex)
{
if (!IsValidIndex(newIndex) || newIndex == currentIndex)
return;
NavItem newItem = navItems[newIndex];
if (!ValidateNavItem(newItem))
return;
if (IsValidIndex(currentIndex))
{
NavItem previousItem = navItems[currentIndex];
if (ValidateNavItem(previousItem))
SetItemActive(previousItem, false);
}
SetItemActive(newItem, true);
currentIndex = newIndex;
}
private void SetItemActive(NavItem item, bool isSelected)
{
item.targetPanel.SetActive(isSelected);
SetBtnVisualState(item, isSelected, false);
}
private void SetBtnVisualState(NavItem item, bool isSelected, bool isHovered)
{
if (item == null || item.Indicator == null)
return;
bool shouldShow = isSelected || isHovered;
item.Indicator.gameObject.SetActive(shouldShow);
if (shouldShow)
item.Indicator.color = isSelected ? selectedColor : hoverColor;
}
private void AutoSelectFirstValidItem()
{
for (int i = 0; i < navItems.Count; i++)
{
if (ValidateNavItem(navItems[i]))
{
SwitchToPanel(i);
return;
}
}
Debug.LogError("没有找到有效的导航项配置!");
}
private bool ValidateNavItem(NavItem item)
{
bool isValid = item != null &&
item.button != null &&
item.targetPanel != null &&
item.Indicator != null;
if (!isValid)
{
Debug.LogWarning("发现无效导航项配置,请检查以下对象:\n" +
$"Button: {item?.button?.name ?? "null"}\n" +
$"Panel: {item?.targetPanel?.name ?? "null"}\n" +
$"Indicator: {item?.Indicator?.name ?? "null"}");
}
return isValid;
}
// 根据约定自动查找按钮子物体中名称以 Indicator 结尾的 Image。
private void ResolveAutomaticReferences()
{
if (navItems == null)
return;
foreach (NavItem item in navItems)
{
if (item == null || item.button == null || item.Indicator != null)
continue;
Image[] images = item.button.GetComponentsInChildren<Image>(true);
foreach (Image image in images)
{
if (image != item.button.image &&
image.name.EndsWith("Indicator", StringComparison.OrdinalIgnoreCase))
{
item.Indicator = image;
break;
}
}
}
}
private bool IsValidIndex(int index)
{
return index >= 0 && index < navItems.Count;
}
// 用于 Event Trigger:指针进入
public void OnPointerEnter(int itemIndex)
{
if (!IsValidIndex(itemIndex) || itemIndex == currentIndex)
return;
SetBtnVisualState(navItems[itemIndex], false, true);
}
// 用于 Event Trigger:指针离开
public void OnPointerExit(int itemIndex)
{
if (!IsValidIndex(itemIndex) || itemIndex == currentIndex)
return;
SetBtnVisualState(navItems[itemIndex], false, false);
}
}这是可以统一保存并交给 Unity 编译的完整文件,复制按钮会复制全部代码。
下载 NavbarController.cs
如果不方便逐行复制,可以下载完整脚本并替换项目中的同名文件。
- 包含三组导航数据结构
- 包含初始化、验证与页面切换
- 包含进入和离开悬停反馈
完整文件的四个入口
从入口顺着调用关系找到最终改变页面或高亮的方法。
左右滑动表格,可查看完整内容
| 入口 | 主要调用链 | 最终结果 |
|---|---|---|
| Start | InitializeNavigation → AutoSelectFirstValidItem | 登记点击并打开第一项有效页面 |
| Button.onClick | OnNavButtonClick → SwitchToPanel | 切换页面与蓝色高亮 |
| Pointer Enter | OnPointerEnter → SetBtnVisualState | 未选中按钮显示深灰色 |
| Pointer Exit | OnPointerExit → SetBtnVisualState | 未选中按钮隐藏深灰色 |
保存后先处理编译信息
- 01
保存 NavbarController.cs 并返回 Unity。
- 02
等待右下角编译过程结束。
- 03
打开控制台,确认没有红色编译信息。
- 04
如果有错误,先确认四条 using、括号、分号和英文双引号。
- 05
确认文件名、类名以及 Indicator 的大小写完全一致。
- 06
编译通过后再进行场景对象绑定。
梳理四个入口
从 Start、按钮点击、指针进入和指针离开分别顺着调用关系找到最终修改页面或高亮的方法。
- 指出哪条路径会更新 currentIndex。
- 指出哪条路径只改变悬停反馈。
- 确认完整文件已保存并编译通过。
进入和离开负责灰色提示,点击监听由代码登记
配置 Event Trigger 并完成连续操作
最后为三个按钮配置 Pointer Enter 与 Pointer Exit,并通过一轮连续操作确认默认页面、悬停、切换、重复点击和重新打开都符合预期。为什么还需要 Event Trigger
Button 直接提供 On Click(),但属性面板中没有直接展开的指针进入和指针离开事件。Event Trigger 可以为按钮补充 Pointer Enter 与 Pointer Exit。
这两个事件只调用 public 的 OnPointerEnter(int) 和 OnPointerExit(int)。按钮点击监听已经由 InitializeNavigation 中的 AddListener 登记,因此三个 Button 的 On Click() 可以保持为空。
为 IntroductionButton 配置进入与离开
- 01
给 IntroductionButton 添加 Event Trigger。
- 02
添加 Pointer Enter 与 Pointer Exit 两种事件。
- 03
在 Pointer Enter 中拖入 NavigationBar。
- 04
选择 NavbarController → OnPointerEnter(int),参数填写 0。
- 05
在 Pointer Exit 中选择 OnPointerExit(int),参数同样填写 0。

观察重点:两个事件都调用 NavigationBar 上的 NavbarController,第一个按钮的两个整数参数都为 0。
点击图片可以查看完整截图三个按钮的 Event Trigger 参数
复制组件后必须修改整数参数,确保它与 navItems 中的成员位置一致。
左右滑动表格,可查看完整内容
| 按钮 | Pointer Enter | Pointer Exit |
|---|---|---|
| IntroductionButton | 0 | 0 |
| NavigationButton | 1 | 1 |
| SettingsButton | 2 | 2 |
按固定顺序观察七种行为
- 01
保存场景并进入运行状态。
- 02
打开菜单,确认第一项有效页面显示且对应高亮为蓝色。
- 03
进入未选中按钮,确认出现深灰色;移开后深灰色消失。
- 04
点击导航与设置按钮,确认页面和蓝色高亮同步切换。
- 05
重复点击当前按钮,确认界面保持稳定。
- 06
进入、离开当前按钮,确认蓝色不会消失。
- 07
隐藏菜单再重新打开,确认保留上一次选择。
运行时观察表
页面、高亮和 currentIndex 应始终保持一致。
左右滑动表格,可查看完整内容
| 操作 | 页面变化 | 高亮变化 | currentIndex |
|---|---|---|---|
| 第一次打开 | 第一项有效页面显示 | 第一项变蓝 | 第一项有效索引 |
| 进入未选中按钮 | 不变 | 该按钮暂时变灰 | 不变 |
| 离开未选中按钮 | 不变 | 灰色隐藏 | 不变 |
| 点击新按钮 | 旧页关、新页开 | 旧蓝关、新蓝开 | 更新 |
| 重复点击当前按钮 | 不变 | 不变 | 不变 |
| 进入或离开当前按钮 | 不变 | 蓝色保持 | 不变 |
| 隐藏后重新打开 | 保留上次页面 | 保留上次蓝色 | 保留 |
常见现象与处理方法
根据现象定位对应配置,不要同时改动多个地方。
左右滑动表格,可查看完整内容
| 现象 | 优先确认 | 处理方法 |
|---|---|---|
| 找不到 NavbarController 组件 | 控制台、文件名和类名 | 先解决编译错误并确认名称一致 |
| 打开后没有页面 | Nav Items 数量与三项引用 | 至少保证一项 Button、Panel、Indicator 完整 |
| 三个页面同时显示 | 组件是否启用、Target Panel 是否正确 | 确认 Start 能执行初始化 |
| 点击导航却打开设置 | 同一 Element 的三组引用 | 重新按 0、1、2 配对 |
| 悬停没有深灰色 | Event Trigger、目标方法与参数 | 补齐 Pointer Enter 并使用正确索引 |
| 离开后灰色不消失 | Pointer Exit 方法和参数 | 进入与离开使用相同索引 |
| 当前蓝色被改成灰色 | 当前索引提前结束条件 | 保留 itemIndex == currentIndex 时 return |
| Indicator 未自动出现 | 子层级、Image 与名称后缀 | 按规则改名或手工拖入正确引用 |
完成一轮可复现展示
连续完成两轮相同操作,确认导航状态稳定并能再次复现。
- 打开菜单并观察默认页面。
- 进入、离开一个未选中按钮。
- 点击两个不同按钮并观察页面与蓝色高亮。
- 重复点击当前按钮。
- 隐藏菜单并重新打开。
- 确认控制台没有新增错误信息。
本课成果清单
保存场景前逐项确认菜单导航已经形成完整、稳定的交互。
- NavbarController.cs 文件完整并且编译无错误。
- NavItem 包含 Button、Target Panel 和 Indicator。
- Start 先初始化,再自动选择第一项有效页面。
- 三组 Nav Items 的对象与顺序完全一致。
- 任意时刻只有一个内容页面显示。
- 未选中按钮悬停时显示深灰色,离开后隐藏。
- 点击后页面与蓝色高亮同步切换。
- 重复点击当前按钮不会造成闪烁或重置。
- 当前按钮的蓝色不会被悬停状态覆盖。
- 隐藏再打开菜单时保留上一次选择。
把知识变成作品
本课练习
请按顺序完成以下任务,并保存自己的学习成果。- 01
不看代码,画出“按钮点击 → OnNavButtonClick → SwitchToPanel → SetItemActive”的调用链。
- 02
把 currentIndex 分别设想为 -1、0、1、2,说明每个值代表什么。
- 03
故意把 Element 1 的 Target Panel 改成 SettingsPage,观察现象后恢复正确配对。
- 04
暂时删除一个按钮的 Pointer Exit,观察灰色高亮现象后恢复配置。
- 05
向同学解释为什么选中状态不能只依赖 Button 的 Selected Color。
- 06
增加一张纸面上的第四组 NavItem,列出需要新增的按钮、页面、高亮和事件参数。
把本课进度保存下来
登录后可以保存浏览内容、有效学习时间、完成状态和答题参与记录。

