UGUI源码分析:Dropdown下拉列表的实现原理

系列

UGUI源码分析系列总览
相关前置:
UGUI EventSystem源码分析
UGUI源码分析:Selectable交互组件的基类
UGUI源码分析:开关组件Toggle与ToggleGroup


UML图一览

在这里插入图片描述


Dropdown

BaseClass: Selectable

Interface:IPointerClickHandler,ISubmitHandler,ICancelHandler

Intro: UGUI中下拉列表组件

  • IPointerClickHandler:点击事件的响应接口
  • ISubmitHandlerSubmit按键点击事件的响应接口,Submit是可以在Project Settings中的Input输入设置。当组件被选中时(“选中”的详细介绍请看Selectable)可响应Submit事件。
  • ICancelHandlerCancel按键点击事件的响应接口,原理同Submit接口,此按键代表取消操作

Dropdown,是UGUI中下拉列表功能组件。它属于直接介绍的组件的混合体,Dropdown组件中运用到了Text、Toggle、Scrollbar、ScrollRect组件,更具具体需求可以舍弃除Toggle之外的组件进行自己的改造。


属性介绍

在这里插入图片描述

  • Interactable:是否可被交互(false时无法通过EventSystem进行交互)
  • Transition:状态变化过渡模式(相关详情
  • Navigation:导航(相关详情)
  • Template :下拉列表模板
  • Caption Text :当前选中的选项文字描述
  • Caption Image :当前选中的选项图片描述
  • Item Text :用于绑定DropdownItem 的Text组件
  • Item Image :用于绑定DropdownItem的 Image组件
  • Value :当前选择索引
  • Options :选项数据list
  • On Value Changed :value变化的事件监听

初始化

Dropdown的初始化过程仅有一个Awake,做了帮助实现动画过渡模块初始化。

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);
}

Show()

由事件为导向的触发:在点击事件确定事件中都是相同的处理,执行了 Show() 方法显示下拉列表

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

Show()方法:显示下拉列表的方法,也是整个Dropdown组件最关键的方法,它做到了以下几点内容

  1. 若没有对列表模板(Template)进行过初始化设置便进行初始化:检测模板是否符合要求(Item含有Toggle组件、父级不是RectTransform、ItemText与ItemImage如果存在必须在Item内部),为ToggleItem添加DropdownItem组件并做相关绑定,将列表模板处于UI的最高层(popupCanvas.sortingOrder = 30000;
public void Show()
{
    if (!IsActive() || !IsInteractable() || m_Dropdown != null)
        return;
    //初始状态时validTemplate为false来触发对于列表模板的初始化设置
    if (!validTemplate)
    {
        //模板初始化方法:检测并设置模板,初始化模板绑定相关组件并调整模板UI层级,若没有通过检查则模板标记为不可用状态。
        SetupTemplate();
        //若检测不通过则无法正常显示下拉列表
        if (!validTemplate)
            return;
    }

	....
}
//模板初始化
private void SetupTemplate()
{
    validTemplate = false;
    if (!m_Template)
    {
        return;
    }
    GameObject templateGo = m_Template.gameObject;
    templateGo.SetActive(true);
    Toggle itemToggle = m_Template.GetComponentInChildren<Toggle>();

    validTemplate = true;
    //各种条件检查模板是否满足要求
    if (!itemToggle || itemToggle.transform == template)
    {
        validTemplate = false;
    }
    else if (!(itemToggle.transform.parent is RectTransform))
    {
        validTemplate = false;
    }
    else if (itemText != null && !itemText.transform.IsChildOf(itemToggle.transform))
    {
        validTemplate = false;
    }
    else if (itemImage != null && !itemImage.transform.IsChildOf(itemToggle.transform))
    {
        validTemplate = false;
    }

    if (!validTemplate)
    {
        templateGo.SetActive(false);
        return;
    }
    //获取模板中的Item并添加DropdownItem组件
    DropdownItem item = itemToggle.gameObject.AddComponent<DropdownItem>();
    item.text = m_ItemText; // 绑定text
    item.image = m_ItemImage;// 绑定image
    item.toggle = itemToggle;// 绑定toggle
    item.rectTransform = (RectTransform)itemToggle.transform; // 绑定transform

    Canvas popupCanvas = GetOrAddComponent<Canvas>(templateGo);
    popupCanvas.overrideSorting = true;
    popupCanvas.sortingOrder = 30000; // 让列表模板处于UI的最高层

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

    validTemplate = true;
}
  1. 复制模板与创建Item:此过程会根据选项内容数据调整区域大小,这个过程中会检查列表模板与CanvasRect的位置关系,如果模板处于Canvas的外部则会对其进行坐标轴翻转
public void Show()
{
	....
	
 	var list = ListPool<Canvas>.Get();
    gameObject.GetComponentsInParent(false, list);
    if (list.Count == 0)
        return;
    //获取父级路径下最近的canvas
    Canvas rootCanvas = list[0];
    ListPool<Canvas>.Release(list);
    //显示模板准备复制列表
    m_Template.gameObject.SetActive(true);
    //复制列表模板
    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);
    // 创建列表Item
    DropdownItem itemTemplate = m_Dropdown.GetComponentInChildren<DropdownItem>();

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

    Rect dropdownContentRect = contentRectTransform.rect;
    Rect itemTemplateRect = itemTemplate.rectTransform.rect;

    //计算Item与背景边界的偏移量
    Vector2 offsetMin = itemTemplateRect.min - dropdownContentRect.min + (Vector2)itemTemplate.rectTransform.localPosition;
    Vector2 offsetMax = itemTemplateRect.max - dropdownContentRect.max + (Vector2)itemTemplate.rectTransform.localPosition;
    Vector2 itemSize = itemTemplateRect.size;

    //清空DropdownItem List 准备开始选项Itme的创建
    m_Items.Clear();

    Toggle prev = null;
    for (int i = 0; i < options.Count; ++i)
    {
        OptionData data = options[i];
        //创建Item
        DropdownItem item = AddItem(data, value == i, itemTemplate, m_Items);
        if (item == null)
            continue;
        // 设置toggle初始状态以及注册事件监听
        item.toggle.isOn = value == i;
        item.toggle.onValueChanged.AddListener(x => OnSelectItem(item.toggle));
        //标记当前选项
        if (item.toggle.isOn)
            item.toggle.Select();
        // 设置Item的导航
        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;
    }
    // 计算内容区域的高度
    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);

    // 当列表处于canvas外部时,将其按坐标轴进行翻转
    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] || corner[axis] > rootCanvasRect.max[axis])
            {
                outside = true;
                break;
            }
        }
        if (outside)
            RectTransformUtility.FlipLayoutOnAxis(dropdownRectTransform, axis, false, false);
    }

	....
}

例:白色框区域代表Canvas区域,此时模板处于Canvas的外部。
在这里插入图片描述
当运行时点击Dropdown,出现的列表被水平翻转了。
在这里插入图片描述

  1. 创建阻拦层:阻拦层是用于监听用户点击事件并执行下拉列表的隐藏,它的层级仅次于列表层(blockerCanvas.sortingOrder = dropdownCanvas.sortingOrder - 1; 即 29999
public void Show()
{
	....
	
    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);
    }

    // 下拉列表渐出效果
    AlphaFadeList(0.15f, 0f, 1f);
    // 隐藏模板
    m_Template.gameObject.SetActive(false);
    itemTemplate.gameObject.SetActive(false);
    // 创建拦截模板,用于监听点击事件来隐藏下拉列表,层级会低于下拉列表(2999)
    m_Blocker = CreateBlocker(rootCanvas);
}

选择交互

当下拉列表中的Item被选择时(即Toggle监听事件触发时)会执行选择操作更新选项值并隐藏列表

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)
        {
            selectedIndex = i - 1;
            break;
        }
    }

    if (selectedIndex < 0)
        return;
	//更新 当前选项值
    value = selectedIndex;
    // 隐藏下拉列表
    Hide();
}

value值发生变化时,会执行 RefreshShownValue() 刷新显示,并执行事件m_OnValueChanged.Invoke(m_Value);

//更新当前选项的信息至captionText、captionImage
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);
    }
}

.
.
.
.
.


嗨,我是作者Vin129,逐儿时之梦正在游戏制作的技术海洋中漂泊。知道的越多,不知道的也越多。希望我的文章对你有所帮助:)


原创文章 39 获赞 59 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_28820675/article/details/106112571