前言

image.png

本节我们将分析 UGUI 中的最后一个组件:Dropdown。Dropdown 意为下拉菜单,由多个我们之前分析过的 UI 组件共同组成,如 ButtonToggleScrollRect 等。Dropdown 是一个非常常用的 UI 组件,我们可以用它来实现下拉菜单、选项卡等功能。

Dropdown

在场景中新建一个 Dropdown 组件,可以看到其组成如下:

image.png

其中,Label 即代表显示在 Dropdown 主界面上的文本,Arrow 代表下拉箭头。Template 是一个比较特殊的对象,他是一个预制好的下拉列表模板,当点击 Dropdown 时,会根据此模板生成对应的下拉列表。

Template 本身是一个 ScrollRect,Item 即为下拉列表中的每个选项。

再来查看 Dropdown 的属性:

image.png

其中 Template 即为上面提到的下拉列表模板,Caption Text 与 Caption Image 分别代表 Dropdown 主界面上的文本和图片,而 Item Text 与 Item Image 则代表每个选项的文本与图片。在默认情况下,Caption Text 被设置为 Label,而 Caption Image 为空,即 Item 的文本会显示在 Dropdown 主界面上。如果想要设置每个选项都为图片的形式,可以新建一个 Image,然后将其拖入 Caption Image 中,并且对 Item 进行相同的设置。

Value 即代表当前选中的选项的索引,Alpha Fade Speed 代表下拉列表的淡入淡出速度,Options 代表下拉列表的选项,可以通过代码动态添加。

Dropdown 在类中定义了两个类用于管理下拉列表的选项。其中 DropdownItem 代表实际显示的选项,而 OptionData 代表选项的数据。

在处理动态的 UI 元素时,将数据与显示分离是一个很好的做法。

DropdownItem 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected internal class DropdownItem : MonoBehaviour, IPointerEnterHandler, ICancelHandler
{
[SerializeField]
private Text m_Text;
[SerializeField]
private Image m_Image;
[SerializeField]
private RectTransform m_RectTransform;
[SerializeField]
private Toggle m_Toggle;

public Text text { get { return m_Text; } set { m_Text = value; } }
public Image image { get { return m_Image; } set { m_Image = value; } }
public RectTransform rectTransform { get { return m_RectTransform; } set { m_RectTransform = value; } }
public Toggle toggle { get { return m_Toggle; } set { m_Toggle = value; } }

public virtual void OnPointerEnter(PointerEventData eventData)
{
EventSystem.current.SetSelectedGameObject(gameObject);
}

public virtual void OnCancel(BaseEventData eventData)
{
Dropdown dropdown = GetComponentInParent<Dropdown>();
if (dropdown)
dropdown.Hide();
}
}

DropdownItem 中包含了一个 Text 与一个 Image,分别代表选项的文本与图片;此外还有一个 Toggle 组件,用于显示选中的 checkmark。在 OnPointerEnter() 中,即当鼠标进入选项时,会将当前事件系统的选中物体设置为自身。而在 OnCancel() 事件中,如按下 ESC 键时,会隐藏 Dropdown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class OptionData
{
[SerializeField]
private string m_Text;
[SerializeField]
private Sprite m_Image;

/// <summary>
/// The text associated with the option.
/// </summary>
public string text { get { return m_Text; } set { m_Text = value; } }

/// <summary>
/// The image associated with the option.
/// </summary>
public Sprite image { get { return m_Image; } set { m_Image = value; } }

public OptionData()
{
}

public OptionData(string text)
{
this.text = text;
}

public OptionData(Sprite image)
{
this.image = image;
}

/// <summary>
/// Create an object representing a single option for the dropdown list.
/// </summary>
/// <param name="text">Optional text for the option.</param>
/// <param name="image">Optional image for the option.</param>
public OptionData(string text, Sprite image)
{
this.text = text;
this.image = image;
}
}

OptionData 中包含了一个 string 与一个 Sprite,分别代表选项的文本与图片。OptionData 提供了多个构造函数,可以根据不同的参数来创建选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OptionDataList
{
[SerializeField]
private List<OptionData> m_Options;

/// <summary>
/// The list of options for the dropdown list.
/// </summary>
public List<OptionData> options { get { return m_Options; } set { m_Options = value; } }


public OptionDataList()
{
options = new List<OptionData>();
}
}

OptionDataList 用于管理多个选项,其中包含了一个 List<OptionData>,用于存储多个选项。

生命周期函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected override void Awake()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
return;
#endif

m_AlphaTweenRunner = new TweenRunner<FloatTween>();
m_AlphaTweenRunner.Init(this);

if (m_CaptionImage)
m_CaptionImage.enabled = (m_CaptionImage.sprite != null);

if (m_Template)
m_Template.gameObject.SetActive(false);
}

相比于其他的组件,Dropdown 的对 Awake() 函数也进行了重写,主要是初始化了 m_AlphaTweenRunner 用作淡入淡出的动画。此外,还对 m_CaptionImagem_Template 进行了初始化。

1
2
3
4
5
6
protected override void Start()
{
base.Start();

RefreshShownValue();
}

Start() 函数中,调用了 RefreshShownValue() 函数,用于刷新界面的显示。

1
2
3
4
5
6
7
8
9
10
11
protected override void OnDisable()
{
//Destroy dropdown and blocker in case user deactivates the dropdown when they click an option (case 935649)
ImmediateDestroyDropdownList();

if (m_Blocker != null)
DestroyBlocker(m_Blocker);
m_Blocker = null;

base.OnDisable();
}

OnDisable() 函数中,会销毁下拉列表与遮罩。这里的 blocker 是一个覆盖区域为整个屏幕的 Button,它的作用就是当我们将下拉列表唤出时,无论点击哪个地方都会隐藏下拉列表。

RefreshShownValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void RefreshShownValue()
{
OptionData data = s_NoOptionData;

if (options.Count > 0)
data = options[Mathf.Clamp(m_Value, 0, options.Count - 1)];

if (m_CaptionText)
{
if (data != null && data.text != null)
m_CaptionText.text = data.text;
else
m_CaptionText.text = "";
}

if (m_CaptionImage)
{
if (data != null)
m_CaptionImage.sprite = data.image;
else
m_CaptionImage.sprite = null;
m_CaptionImage.enabled = (m_CaptionImage.sprite != null);
}
}

RefreshShownValue() 函数用于刷新界面的显示,主要是根据当前选中的选项来更新 CaptionTextCaptionImage 的显示。为了确保数据与显示的一致性,应该在每次更改选项时都调用此函数。

ImmediateDestroyDropdownList

1
2
3
4
5
6
7
8
9
10
11
12
private void ImmediateDestroyDropdownList()
{
for (int i = 0; i < m_Items.Count; i++)
{
if (m_Items[i] != null)
DestroyItem(m_Items[i]);
}
m_Items.Clear();
if (m_Dropdown != null)
DestroyDropdownList(m_Dropdown);
m_Dropdown = null;
}

ImmediateDestroyDropdownList() 函数用于立即销毁下拉列表,首先会遍历所有的选项,逐一调用 DestroyItem() 函数销毁选项,然后调用 DestroyDropdownList() 函数销毁下拉列表。

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// Convenience method to explicitly destroy the previously generated Items.
/// </summary>
/// <remarks>
/// Override this method to implement a different way to dispose of an option item.
/// Likely no action needed since destroying the dropdown list destroys all contained items as well.
/// </remarks>
/// <param name="item">The Item to destroy.</param>
protected virtual void DestroyItem(DropdownItem item)
{}

在默认的实现中,DestroyItem() 函数为空,即不会对选项进行额外的处理。这主要是因为所有的选项均为 m_Dropdown 的子物体,销毁 m_Dropdown 时会自动销毁所有的选项。

1
2
3
4
protected virtual void DestroyDropdownList(GameObject dropdownList)
{
Destroy(dropdownList);
}

DestroyDropdownList() 则是直接调用了 Object.Destroy() 函数销毁下拉列表物体。

1
2
3
4
protected virtual void DestroyBlocker(GameObject blocker)
{
Destroy(blocker);
}

DestroyBlocker() 也是同理,直接调用了 Object.Destroy() 函数销毁遮罩物体。

需要注意的是,这些Dropdown 中与销毁功能有关的函数均为虚函数,可以在子类中进行重写

Dropdown 实现了 IPointerClickHandlerISubmitHandlerICancelHandler 接口,分别用于处理鼠标点击、提交与取消事件。

在具体分析交互的实现之前,我们先来分析一下:Dropdown 需要负责哪些交互操作?

  1. 当点击 Dropdown 时,需要显示下拉列表。(OnPointerClickOnSubmit
  2. 当点击下拉列表中的选项时,需要更新当前选中的选项。
  3. 当点击 Dropdown 以外的区域时,需要隐藏下拉列表。
  4. 当按下 ESC 键时,需要隐藏下拉列表。(OnCancel

其中,DropItem 本身的交互已经有其上所挂载的 Toggle 组件处理,因此 Dropdown 只需要整体页面的开关逻辑及与 Toggle 的信息交互即可。

查看 Dropdown 的关于交互的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public virtual void OnPointerClick(PointerEventData eventData)
{
Show();
}

public virtual void OnSubmit(BaseEventData eventData)
{
Show();
}

public virtual void OnCancel(BaseEventData eventData)
{
Hide();
}

可以看到,这些接口的实现函数均直接调用了 Show()Hide() 函数。这两个函数分别用于显示与隐藏下拉列表。

Show

总体流程

Show() 函数的主要功能就是根据 m_Template 生成下拉列表,并根据 m_Options 的内容来初始化下拉列表的选项。

点击展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
public void Show()
{
if (!IsActive() || !IsInteractable() || m_Dropdown != null)
return;

// Get root Canvas.
var list = ListPool<Canvas>.Get();
gameObject.GetComponentsInParent(false, list);
if (list.Count == 0)
return;

// case 1064466 rootCanvas should be last element returned by GetComponentsInParent()
Canvas rootCanvas = list[list.Count - 1];
for (int i = 0; i < list.Count; i++)
{
if (list[i].isRootCanvas)
{
rootCanvas = list[i];
break;
}
}

ListPool<Canvas>.Release(list);

if (!validTemplate)
{
SetupTemplate();
if (!validTemplate)
return;
}

m_Template.gameObject.SetActive(true);

// popupCanvas used to assume the root canvas had the default sorting Layer, next line fixes
// (case 958281 - [UI] Dropdown list does not copy the parent canvas layer when the panel is opened)
m_Template.GetComponent<Canvas>().sortingLayerID = rootCanvas.sortingLayerID;

// Instantiate the drop-down template
m_Dropdown = CreateDropdownList(m_Template.gameObject);
m_Dropdown.name = "Dropdown List";
m_Dropdown.SetActive(true);

// Make drop-down RectTransform have same values as original.
RectTransform dropdownRectTransform = m_Dropdown.transform as RectTransform;
dropdownRectTransform.SetParent(m_Template.transform.parent, false);

// Instantiate the drop-down list items

// Find the dropdown item and disable it.
DropdownItem itemTemplate = m_Dropdown.GetComponentInChildren<DropdownItem>();

GameObject content = itemTemplate.rectTransform.parent.gameObject;
RectTransform contentRectTransform = content.transform as RectTransform;
itemTemplate.rectTransform.gameObject.SetActive(true);

// Get the rects of the dropdown and item
Rect dropdownContentRect = contentRectTransform.rect;
Rect itemTemplateRect = itemTemplate.rectTransform.rect;

// Calculate the visual offset between the item's edges and the background's edges
Vector2 offsetMin = itemTemplateRect.min - dropdownContentRect.min + (Vector2)itemTemplate.rectTransform.localPosition;
Vector2 offsetMax = itemTemplateRect.max - dropdownContentRect.max + (Vector2)itemTemplate.rectTransform.localPosition;
Vector2 itemSize = itemTemplateRect.size;

m_Items.Clear();

Toggle prev = null;
for (int i = 0; i < options.Count; ++i)
{
OptionData data = options[i];
DropdownItem item = AddItem(data, value == i, itemTemplate, m_Items);
if (item == null)
continue;

// Automatically set up a toggle state change listener
item.toggle.isOn = value == i;
item.toggle.onValueChanged.AddListener(x => OnSelectItem(item.toggle));

// Select current option
if (item.toggle.isOn)
item.toggle.Select();

// Automatically set up explicit navigation
if (prev != null)
{
Navigation prevNav = prev.navigation;
Navigation toggleNav = item.toggle.navigation;
prevNav.mode = Navigation.Mode.Explicit;
toggleNav.mode = Navigation.Mode.Explicit;

prevNav.selectOnDown = item.toggle;
prevNav.selectOnRight = item.toggle;
toggleNav.selectOnLeft = prev;
toggleNav.selectOnUp = prev;

prev.navigation = prevNav;
item.toggle.navigation = toggleNav;
}
prev = item.toggle;
}

// Reposition all items now that all of them have been added
Vector2 sizeDelta = contentRectTransform.sizeDelta;
sizeDelta.y = itemSize.y * m_Items.Count + offsetMin.y - offsetMax.y;
contentRectTransform.sizeDelta = sizeDelta;

float extraSpace = dropdownRectTransform.rect.height - contentRectTransform.rect.height;
if (extraSpace > 0)
dropdownRectTransform.sizeDelta = new Vector2(dropdownRectTransform.sizeDelta.x, dropdownRectTransform.sizeDelta.y - extraSpace);

// Invert anchoring and position if dropdown is partially or fully outside of canvas rect.
// Typically this will have the effect of placing the dropdown above the button instead of below,
// but it works as inversion regardless of initial setup.
Vector3[] corners = new Vector3[4];
dropdownRectTransform.GetWorldCorners(corners);

RectTransform rootCanvasRectTransform = rootCanvas.transform as RectTransform;
Rect rootCanvasRect = rootCanvasRectTransform.rect;
for (int axis = 0; axis < 2; axis++)
{
bool outside = false;
for (int i = 0; i < 4; i++)
{
Vector3 corner = rootCanvasRectTransform.InverseTransformPoint(corners[i]);
if ((corner[axis] < rootCanvasRect.min[axis] && !Mathf.Approximately(corner[axis], rootCanvasRect.min[axis])) ||
(corner[axis] > rootCanvasRect.max[axis] && !Mathf.Approximately(corner[axis], rootCanvasRect.max[axis])))
{
outside = true;
break;
}
}
if (outside)
RectTransformUtility.FlipLayoutOnAxis(dropdownRectTransform, axis, false, false);
}

for (int i = 0; i < m_Items.Count; i++)
{
RectTransform itemRect = m_Items[i].rectTransform;
itemRect.anchorMin = new Vector2(itemRect.anchorMin.x, 0);
itemRect.anchorMax = new Vector2(itemRect.anchorMax.x, 0);
itemRect.anchoredPosition = new Vector2(itemRect.anchoredPosition.x, offsetMin.y + itemSize.y * (m_Items.Count - 1 - i) + itemSize.y * itemRect.pivot.y);
itemRect.sizeDelta = new Vector2(itemRect.sizeDelta.x, itemSize.y);
}

// Fade in the popup
AlphaFadeList(m_AlphaFadeSpeed, 0f, 1f);

// Make drop-down template and item template inactive
m_Template.gameObject.SetActive(false);
itemTemplate.gameObject.SetActive(false);

m_Blocker = CreateBlocker(rootCanvas);
}

函数体非常长,我们来逐一分析其中的关键部分:

1
2
3
4
5
6
7
8
9
10
m_Template.gameObject.SetActive(true);

m_Template.GetComponent<Canvas>().sortingLayerID = rootCanvas.sortingLayerID;

m_Dropdown = CreateDropdownList(m_Template.gameObject);
m_Dropdown.name = "Dropdown List";
m_Dropdown.SetActive(true);

RectTransform dropdownRectTransform = m_Dropdown.transform as RectTransform;
dropdownRectTransform.SetParent(m_Template.transform.parent, false);

首先,以 m_Template 为模板新实例化了一个下拉列表 m_Dropdown,并将其层级结构设置为与 m_Template 相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DropdownItem itemTemplate = m_Dropdown.GetComponentInChildren<DropdownItem>();

GameObject content = itemTemplate.rectTransform.parent.gameObject;
RectTransform contentRectTransform = content.transform as RectTransform;
itemTemplate.rectTransform.gameObject.SetActive(true);

// Get the rects of the dropdown and item
Rect dropdownContentRect = contentRectTransform.rect;
Rect itemTemplateRect = itemTemplate.rectTransform.rect;

// Calculate the visual offset between the item's edges and the background's edges
Vector2 offsetMin = itemTemplateRect.min - dropdownContentRect.min + (Vector2)itemTemplate.rectTransform.localPosition;
Vector2 offsetMax = itemTemplateRect.max - dropdownContentRect.max + (Vector2)itemTemplate.rectTransform.localPosition;
Vector2 itemSize = itemTemplateRect.size;

其次,获取了 m_Dropdown 中的 DropdownItem 组件作为选项模板的模板,并且计算了一些布局的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
m_Items.Clear();

Toggle prev = null;
for (int i = 0; i < options.Count; ++i)
{
OptionData data = options[i];
DropdownItem item = AddItem(data, value == i, itemTemplate, m_Items);
if (item == null)
continue;

// Automatically set up a toggle state change listener
item.toggle.isOn = value == i;
item.toggle.onValueChanged.AddListener(x => OnSelectItem(item.toggle));

// Select current option
if (item.toggle.isOn)
item.toggle.Select();

// Automatically set up explicit navigation
if (prev != null)
{
Navigation prevNav = prev.navigation;
Navigation toggleNav = item.toggle.navigation;
prevNav.mode = Navigation.Mode.Explicit;
toggleNav.mode = Navigation.Mode.Explicit;

prevNav.selectOnDown = item.toggle;
prevNav.selectOnRight = item.toggle;
toggleNav.selectOnLeft = prev;
toggleNav.selectOnUp = prev;

prev.navigation = prevNav;
item.toggle.navigation = toggleNav;
}
prev = item.toggle;
}

接着,会以 itemTemplate 为模板,根据 options 中的数据生成选项。可以看到,这里为每个选项对应的 Toggle 组件都添加了 OnSelectItem() 回调,用于 ToggleDropdown 的信息交互。此外还为每个选项设置了导航信息,使得可以通过方向键进行选项的切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Reposition all items now that all of them have been added
Vector2 sizeDelta = contentRectTransform.sizeDelta;
sizeDelta.y = itemSize.y * m_Items.Count + offsetMin.y - offsetMax.y;
contentRectTransform.sizeDelta = sizeDelta;

float extraSpace = dropdownRectTransform.rect.height - contentRectTransform.rect.height;
if (extraSpace > 0)
dropdownRectTransform.sizeDelta = new Vector2(dropdownRectTransform.sizeDelta.x, dropdownRectTransform.sizeDelta.y - extraSpace);

// Invert anchoring and position if dropdown is partially or fully outside of canvas rect.
// Typically this will have the effect of placing the dropdown above the button instead of below,
// but it works as inversion regardless of initial setup.
Vector3[] corners = new Vector3[4];
dropdownRectTransform.GetWorldCorners(corners);

RectTransform rootCanvasRectTransform = rootCanvas.transform as RectTransform;
Rect rootCanvasRect = rootCanvasRectTransform.rect;
for (int axis = 0; axis < 2; axis++)
{
bool outside = false;
for (int i = 0; i < 4; i++)
{
Vector3 corner = rootCanvasRectTransform.InverseTransformPoint(corners[i]);
if ((corner[axis] < rootCanvasRect.min[axis] && !Mathf.Approximately(corner[axis], rootCanvasRect.min[axis])) ||
(corner[axis] > rootCanvasRect.max[axis] && !Mathf.Approximately(corner[axis], rootCanvasRect.max[axis])))
{
outside = true;
break;
}
}
if (outside)
RectTransformUtility.FlipLayoutOnAxis(dropdownRectTransform, axis, false, false);
}

for (int i = 0; i < m_Items.Count; i++)
{
RectTransform itemRect = m_Items[i].rectTransform;
itemRect.anchorMin = new Vector2(itemRect.anchorMin.x, 0);
itemRect.anchorMax = new Vector2(itemRect.anchorMax.x, 0);
itemRect.anchoredPosition = new Vector2(itemRect.anchoredPosition.x, offsetMin.y + itemSize.y * (m_Items.Count - 1 - i) + itemSize.y * itemRect.pivot.y);
itemRect.sizeDelta = new Vector2(itemRect.sizeDelta.x, itemSize.y);
}

然后会利用计算的布局属性对选项依次布局。注意,这里并没有使用 LayoutGroup 组件,而是直接计算了每个选项的位置。

1
2
3
4
5
6
7
8
// Fade in the popup
AlphaFadeList(m_AlphaFadeSpeed, 0f, 1f);

// Make drop-down template and item template inactive
m_Template.gameObject.SetActive(false);
itemTemplate.gameObject.SetActive(false);

m_Blocker = CreateBlocker(rootCanvas);

最后,调用 AlphaFadeList() 函数实现下拉列表的淡入效果,并且将模板再次隐藏。此外,还会创建一个遮罩 m_Blocker,用于接收 Dropdown 以外的点击事件。

SetupTemplate

SetupTemplate() 函数用于初始化下拉列表的模板,即 m_Template。函数中会确认模板各组成部分的正确性,并为其添加相应的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private void SetupTemplate()
{
validTemplate = false;

// 检查模板层级结构及组件
// ...

if (!validTemplate)
{
templateGo.SetActive(false);
return;
}

DropdownItem item = itemToggle.gameObject.AddComponent<DropdownItem>();
item.text = m_ItemText;
item.image = m_ItemImage;
item.toggle = itemToggle;
item.rectTransform = (RectTransform)itemToggle.transform;

// Find the Canvas that this dropdown is a part of
Canvas parentCanvas = null;
Transform parentTransform = m_Template.parent;
while (parentTransform != null)
{
parentCanvas = parentTransform.GetComponent<Canvas>();
if (parentCanvas != null)
break;

parentTransform = parentTransform.parent;
}

Canvas popupCanvas = GetOrAddComponent<Canvas>(templateGo);
popupCanvas.overrideSorting = true;
popupCanvas.sortingOrder = 30000;

// If we have a parent canvas, apply the same raycasters as the parent for consistency.
if (parentCanvas != null)
{
Component[] components = parentCanvas.GetComponents<BaseRaycaster>();
for (int i = 0; i < components.Length; i++)
{
Type raycasterType = components[i].GetType();
if (templateGo.GetComponent(raycasterType) == null)
{
templateGo.AddComponent(raycasterType);
}
}
}
else
{
GetOrAddComponent<GraphicRaycaster>(templateGo);
}

GetOrAddComponent<CanvasGroup>(templateGo);
templateGo.SetActive(false);

validTemplate = true;
}

SetupTemplate() 函数中,如果模板的层级结构与组件正确,则分别会为其添加 DropdownItemCanvasGraphicRaycasterCanvasGroup 组件。其中,CanvasGroup 用于控制 Dropdown 整体的透明度,从而实现淡入淡出的效果。

此外,需要注意的是在添加完 Canvas 组件后,这里将其 sortingOrder 设置为了 30000,这是为了确保 Dropdown 的显示层级高于其他 UI 组件。其次在后续对 m_Blocker 的设置中,也可以看到将其设置为了 29999,以保证 Dropdown 区域的优先级高于 m_Blocker,而 m_Blocker 的优先级又高于其他 UI 组件

OnSelectItem

OnSelectItem() 函数在 Show() 中被注册到了 ToggleonValueChanged 事件中,用于处理选项的选中事件。来看看它是如何完成 DropdownToggle 的信息交互的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void OnSelectItem(Toggle toggle)
{
if (!toggle.isOn)
toggle.isOn = true;

int selectedIndex = -1;
Transform tr = toggle.transform;
Transform parent = tr.parent;
for (int i = 0; i < parent.childCount; i++)
{
if (parent.GetChild(i) == tr)
{
// Subtract one to account for template child.
selectedIndex = i - 1;
break;
}
}

if (selectedIndex < 0)
return;

value = selectedIndex;
Hide();
}

可以看到,这里是通过遍历 toggle 父物体的全部子物体,来确定当前选中的选项的索引。然后将选项的索引赋值给 value,并调用 Hide() 函数隐藏下拉列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public int value
{
get
{
return m_Value;
}
set
{
Set(value);
}
}

void Set(int value, bool sendCallback = true)
{
if (Application.isPlaying && (value == m_Value || options.Count == 0))
return;

m_Value = Mathf.Clamp(value, 0, options.Count - 1);
RefreshShownValue();

if (sendCallback)
{
// Notify all listeners
UISystemProfilerApi.AddMarker("Dropdown.value", this);
m_OnValueChanged.Invoke(m_Value);
}
}

valueset 函数中,会调用 RefreshShownValue() 函数刷新界面的显示,并且触发 m_OnValueChanged 事件,通知所有的回调函数。

CreateBlocker

Dropdown.gif

CreateBlocker() 函数会创建一个遮罩,用于接收 Dropdown 区域以外的点击事件。遮罩是通过创建一个 Button 组件,并且将其大小设置为整个屏幕而实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
protected virtual GameObject CreateBlocker(Canvas rootCanvas)
{
// Create blocker GameObject.
GameObject blocker = new GameObject("Blocker");

// Setup blocker RectTransform to cover entire root canvas area.
RectTransform blockerRect = blocker.AddComponent<RectTransform>();
blockerRect.SetParent(rootCanvas.transform, false);
blockerRect.anchorMin = Vector3.zero;
blockerRect.anchorMax = Vector3.one;
blockerRect.sizeDelta = Vector2.zero;

// Make blocker be in separate canvas in same layer as dropdown and in layer just below it.
Canvas blockerCanvas = blocker.AddComponent<Canvas>();
blockerCanvas.overrideSorting = true;
Canvas dropdownCanvas = m_Dropdown.GetComponent<Canvas>();
blockerCanvas.sortingLayerID = dropdownCanvas.sortingLayerID;
blockerCanvas.sortingOrder = dropdownCanvas.sortingOrder - 1;

// Find the Canvas that this dropdown is a part of
Canvas parentCanvas = null;
Transform parentTransform = m_Template.parent;
while (parentTransform != null)
{
parentCanvas = parentTransform.GetComponent<Canvas>();
if (parentCanvas != null)
break;

parentTransform = parentTransform.parent;
}

// If we have a parent canvas, apply the same raycasters as the parent for consistency.
if (parentCanvas != null)
{
Component[] components = parentCanvas.GetComponents<BaseRaycaster>();
for (int i = 0; i < components.Length; i++)
{
Type raycasterType = components[i].GetType();
if (blocker.GetComponent(raycasterType) == null)
{
blocker.AddComponent(raycasterType);
}
}
}
else
{
// Add raycaster since it's needed to block.
GetOrAddComponent<GraphicRaycaster>(blocker);
}


// Add image since it's needed to block, but make it clear.
Image blockerImage = blocker.AddComponent<Image>();
blockerImage.color = Color.clear;

// Add button since it's needed to block, and to close the dropdown when blocking area is clicked.
Button blockerButton = blocker.AddComponent<Button>();
blockerButton.onClick.AddListener(Hide);

return blocker;
}

这里有两点需要注意:其一,blockerCanvas.sortingOrder = dropdownCanvas.sortingOrder - 1 这句代码,将遮罩的优先级设置为了 Dropdown 的优先级减一,这就达成了 Dropdown 区域的优先级高于遮罩的效果,因此点击选项本身不会触发遮罩的点击事件。其二,遮罩的 Button 组件上注册了 Hide() 函数,用于隐藏下拉列表,这就实现了点击遮罩区域隐藏下拉列表的效果。

Hide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void Hide()
{
if (m_Dropdown != null)
{
AlphaFadeList(m_AlphaFadeSpeed, 0f);

// User could have disabled the dropdown during the OnValueChanged call.
if (IsActive())
StartCoroutine(DelayedDestroyDropdownList(m_AlphaFadeSpeed));
}
if (m_Blocker != null)
DestroyBlocker(m_Blocker);
m_Blocker = null;
Select();
}

private IEnumerator DelayedDestroyDropdownList(float delay)
{
yield return new WaitForSecondsRealtime(delay);
ImmediateDestroyDropdownList();
}

Hide() 的逻辑比较简单,首先会调用 AlphaFadeList() 函数实现下拉列表的淡出效果,然后通过协程 DelayedDestroyDropdownList() 函数延迟销毁下拉列表。最后销毁遮罩 m_Blocker

这里之所以要延迟销毁下拉列表,是为了让淡出效果能够完全展示,同时也能够等待事件处理完毕。