UGUI内核大探究(十三)Dropdown

Dropdown(下拉框)可谓是UGUI的集大成者,在Unity Editor里新建一个Dropdown,会随之附赠Text(Label对象)、Image(Arrow对象)、ScrollRect(Template对象)、Toggle(Template\Viewport\Content\item)和ScrollBar(Template\Scrollbar)。点击运行展开下拉框后还会创建一个Button(Blocker),而且还根据Template再实例化一个可见的Dropdown List。如此复杂的一个组件,竟然代码只有600余行,不得不让我们感叹Unity官方深谙组合之道。本文就探究一下Dropdown的神奇之处。

按照惯例,附上UGUI源码下载地址。

我们首先看一下Dropdown的内部类DropdownItem(下拉项)。运行状态下展开下拉框,可以看到它被加到Item上面。

DropdownItem继承自MonoBehaviour和IPointerEnterHandler, ICancelHandler两个接口。

它包含了四个属性:text、image、rectTransform和toggle。

OnPointerEnter(当鼠标进入)方法继承自IPointerEnterHandler,调用EventSystem的SetSelectedGameObject将本对象设置为选中的对象(祥参UGUI内核大探究(一)EventSystem)。具体表现就是Item对象的背景颜色变了。

OnCancel(取消键按下)方法继承自ICancelHandler,获取父对象中的dropdown组件,调用Hide方法。具体表现就是选项表(Dropdown List)隐藏了。

Dropdown继承自Selectable和IPointerClickHandler, ISubmitHandler, ICancelHandler三个接口。

Dropdown重写了Awake方法,新建了一个FloatTween类型的TweenRunner变量m_AlphaTweenRunner并初始化,这个变量在显示/隐藏选项表(Dropdown List)的时候执行透明度渐变效果。然后设置了m_CaptionImage是否可用,这个变量对应于编辑器里的Caption Image,如果选中的选项(Options)设置了图片的话,就会使用m_CaptionImage显示在Dropdown的标题上。最后设置m_Template为false,这个变量对应于Template对象,用于作为模板实例化选项表。

OnPointerClick(继承自IPointerClickHandler,点击时)和OnSubmit(继承自ISubmitHandler,确认键按下时)调用了Show方法,而ICancelHandler(继承自ICancelHandler,取消键按下时)调用了Hide方法。

Show是Dropdown里最重要的一个方法,虽然很长但是值得贴一下。

        // Show the dropdown.
        //
        // Plan for dropdown scrolling to ensure dropdown is contained within screen.
        //
        // We assume the Canvas is the screen that the dropdown must be kept inside.
        // This is always valid for screen space canvas modes.
        // For world space canvases we don't know how it's used, but it could be e.g. for an in-game monitor.
        // We consider it a fair constraint that the canvas must be big enough to contains dropdowns.
        public void Show()
        {
            if (!IsActive() || !IsInteractable() || m_Dropdown != null)
                return;
 
            if (!validTemplate)
            {
                SetupTemplate();
                if (!validTemplate)
                    return;
            }
 
            // Get root Canvas.
            var list = ListPool<Canvas>.Get();
            gameObject.GetComponentsInParent(false, list);
            if (list.Count == 0)
                return;
            Canvas rootCanvas = list[0];
            ListPool<Canvas>.Release(list);
 
            m_Template.gameObject.SetActive(true);
 
            // 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);
            bool outside = false;
            RectTransform rootCanvasRectTransform = rootCanvas.transform as RectTransform;
            for (int i = 0; i < 4; i++)
            {
                Vector3 corner = rootCanvasRectTransform.InverseTransformPoint(corners[i]);
                if (!rootCanvasRectTransform.rect.Contains(corner))
                {
                    outside = true;
                    break;
                }
            }
            if (outside)
            {
                RectTransformUtility.FlipLayoutOnAxis(dropdownRectTransform, 0, false, false);
                RectTransformUtility.FlipLayoutOnAxis(dropdownRectTransform, 1, 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(0.15f, 0f, 1f);
 
            // Make drop-down template and item template inactive
            m_Template.gameObject.SetActive(false);
            itemTemplate.gameObject.SetActive(false);
 
            m_Blocker = CreateBlocker(rootCanvas);
        }

 

Show的步骤:
1、调用SetupTemplate方法,设置模板。SetupTemplate方法里判断了一系列限定,接着为item对象添加了DropdownItem组件,并为DropdownItem的四个属性赋值,然后为自己添加Canvas组件,设置overrideSorting为true,并sortingOrder为30000,这可以让选项表尽可能的显示在最前面,然后添加GraphicRaycaster和CanvasGroup组件,为了接受到鼠标事件。

2、调用CreateDropdownList方法,以m_Template为模板创建m_Dropdown(选项表)。并为m_Dropdown修改名字,设置父对象。然后在子对象里找到DropdownItem保存为itemTemplate,以itemTemplate为模板,创建每一个Item(数据为OptionData,对应编辑器里的Options下的Option),为Item的Toggle的onValueChanged事件添加监听OnSelectItem(根据选中的Toggle,找到它在父对象Content中的Index,为Dropdown设置值value,并隐藏Dropdown List),最后设置导航。

3、根据Item的数量设置Content的尺寸,Content是Scroll Rect(祥参UGUI内核大探究(十一)ScrollRect与ScrollBar)里面用于显示内容的对象。并且如果Dropdown List的高度大于Content的高度,便修正它的高度与Content相同。然后判断Dropdown List的四角是否超出了rootCanvas(Dropdown最上层的Canvas)的边界,便翻转Dropdown List,这种时候,我们将会看到选项表在Dropdown的上面,如图。

然后设置Item的位置和尺寸。
 

4、Alpha渐变(m_AlphaTweenRunner)显示Dropdown List,并将m_Template和itemTemplate设置为无效的。

5、调用CreateBlocker创建Blocker。Blocker在rootCanvas下一级,尺寸与rootCanvas相同,sortingOrder比Dropdown List的小1(29999)。添加了Image组件,颜色为全透明,添加了Button组件,添加了onClick的监听,回调Hide方法。由此我们可知道Blocker是用于阻挡住鼠标事件,即Dropdown List显示时,点击选项表以外的区域,都只是隐藏选项表,不会触发其他的组件。

Hide方法要简单的多:

        // Hide the dropdown.
        public void Hide()
        {
            if (m_Dropdown != null)
            {
                AlphaFadeList(0.15f, 0f);
                StartCoroutine(DelayedDestroyDropdownList(0.15f));
            }
            if (m_Blocker != null)
                DestroyBlocker(m_Blocker);
            m_Blocker = null;
            Select();
        }
Alpha渐变隐藏Dropdown List,并在渐变结束后Destroy所有的Item和Dropdown List。接着DestroyBlocker。最后设置本对象为Select(高亮状态)。


Dropdown的值value是一个属性(Property),对应变量m_Value。它的set访问器(参考C#语法小知识(六)属性与索引器)里,会将参数值限定在0到options.Count(选项数量) - 1之间。刷新,并发送m_OnValueChanged事件(可在编辑器里设置)。

刷新Refresh方法里,会在options里找到value值对应的OptionData,为m_CaptionText设置文本和m_CaptionImage设置图片,即在Dropdown上显示选中的选项。
 

展开阅读全文

没有更多推荐了,返回首页