美文网首页unity
[Unity 3d] UIBlocker - 解决 UGUI 层

[Unity 3d] UIBlocker - 解决 UGUI 层

作者: 雨落随风 | 来源:发表于2023-12-26 02:31 被阅读0次

    前言:

    在 Unity UGUI 中,处理 UI 层级管理时,我们想让一些 UI 元素浮在最上面,但是又不想破坏其他 UI 元素的层级关系,当如果不做点什么,我们往往陷入困境。

    举个实例:我们有一个表格,表格上方是数据筛选器,筛选器里面有一个日期选择器,我们想让日期选择器显示时覆盖表格的内容,但是又不想改变把日期选择器拖到这个面板的最底层,因为那样会影响我们的 UI 布局。这该如何实现呢? 这便是本文要解决的痛点!


    被 SheetContent 遮蔽的只剩一角的 Calender

    启发:

    看到 Unity 的 Dropdown 组件,我发现了一个妙招。Dropdown 运行时生成一个名为 "Blocker" 的组件,自动占据当前层级,无需手动调整。这启发了我,我们也可以创建类似的 "Blocker" 组件,来管理我们的 UI 元素。

    设计:

    通用 Blocker 组件的构想:
    基于 Dropdown 的启发,我设计了通用的 "Blocker" 组件。它不仅仅可以阻止鼠标事件,还能自动管理 UI 层级。比如,无论面板在那个层级下,都可以自动将此面板置顶显示,并且点击面板外区域时支持隐藏面板,避免误触其他组件,或者抖动面板,模拟模态面板独占并强提示的行为。

    此外,该组件在运行时会自动充满当前图层,无需手动调整。支持多个 "Blocker" 组件的嵌套使用,我还添加了一个 IBlockable 接口,让继承了 IBlockable 的面板能够响应 Blocker 的点击。

    最后,Blocker 应该可以淡入淡出,和修改配色。

    实现:

    一个通用的 "Blocker" 组件的实现的原理简单来说,就是巧妙的使用了 Canvas 组件的 sortingOrder 实现了 新增 Blocker 以及 Root Canvas 和 继承了 IBlockable 的面板三者的渲染先后关系,这样一来这个面板唤起时就会自动创建 Blocker 把自己展示到最顶层而不用关系自己位于 UI 层级树的哪个层级了!

            public Blocker(IBlockable target, Color color)
            {
                this.target = target;
                blockers.Add(target, this);
    
                // check target wether its UI or not 
                var go = target as MonoBehaviour;
                var rect = go.GetComponent<RectTransform>();
                if (!go || !rect)
                {
                    throw new Exception("target must be a UI component");
                }
    
                // should not blocked before
                innercanvas = go.GetComponent<Canvas>();
                if (innercanvas && innercanvas.enabled)
                {
                    throw new Exception("target should not be blocked before");
                }
    
                // get target's root canvas
                rootCanvas = go.GetComponentsInParent<Canvas>()
                    .Where(c => c.isRootCanvas)
                    .FirstOrDefault();
                if (!rootCanvas)
                {
                    throw new Exception("target must be in a canvas");
                }
    
                // 1. Create blocker GameObject.
                blocker = new GameObject("Blocker", typeof(RectTransform));
    
                // 2. Set blocker's RectTransform properties.
                var rectTransform = blocker.GetComponent<RectTransform>();
                rectTransform.SetParent(rootCanvas.transform, false);
                rectTransform.SetAsLastSibling();
                rectTransform.anchorMin = Vector3.zero;
                rectTransform.anchorMax = Vector3.one;
                rectTransform.sizeDelta = Vector2.zero;
    
                // 3. Add Canvas component.
                Canvas canvas = blocker.AddComponent<Canvas>();
                blocker.AddComponent<GraphicRaycaster>();
                canvas.overrideSorting = true;
    
                // 4. Add Canvas component for target panel.
                innercanvas = go.gameObject.AddComponent<Canvas>();
                innercanvas.overrideSorting = true;
                innercanvas.sortingOrder = 25000 + blockers.Count;
                raycaster = go.gameObject.AddComponent<GraphicRaycaster>();
    
                // 5. Set the sorting layer of blocker's Canvas to be Lower just one unit than the target panel's Canvas.
                canvas.sortingLayerID = innercanvas.sortingLayerID;
                canvas.sortingOrder = innercanvas.sortingOrder - 1;
                background = blocker.AddComponent<Image>();
                color.a = 0f;
                background.color = color;
                button = blocker.AddComponent<Button>();
                button.onClick.AddListener(target.HandleBlockClickedAsync);
                blocker.hideFlags = HideFlags.HideInHierarchy;
            }
    
    

    示例:

    以下是一个使用了 Blocker 组件的模态窗口 (NotificationPanel)示例代码,它展示了如何优雅的通过 Blcoker 来解决层级问题:

    NotificationPanel

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using UnityEngine;
    using UnityEngine.UI;
    using zFramework.Ex;
    using zFramework.UI;
    
    namespace zFramework.Example
    {
        public class NotificationPanel : MonoBehaviour, IBlockable
        {
            public Text title;
            public Text content;
            public Button confirmButton;
            public Button cancelButton;
            public Toggle toggle;
            private CancellationTokenSource cts;
    
            private void Start() => toggle.onValueChanged.AddListener((value) => closeByBlock = value);
    
            public bool closeByBlock = false;
            public bool useBlocker = true;
            public async Task<int> ShowAsync(string title, string content)
            {
                cts = new CancellationTokenSource();
                this.title.text = title;
                this.content.text = content;
    
                // reset panel 
                transform.localScale = Vector3.one * 0.1f;
                gameObject.SetActive(true);
                // must blocker first, other wise you may click the other button before the panel fadein
                // delay 0.1f means wait for panel show about 0.1f then blocker start fadein
                // You set the block fade-in duration to 0.3f and delay to 0.1f, so the blocker will appear along with the panel suddenly.
                if (useBlocker) await this.BlockAsync(Color.black, 0.8f, 0.3f, 0.1f);
    
                await transform.DoScaleAsync(Vector3.one, 0.5f, Ease.OutBack);
                var index = await TaskExtension.WhenAny(confirmButton.OnClickAsync(cts.Token), cancelButton.OnClickAsync(cts.Token));
                // if you want blocker fadeout along with panel , you should use "_= " to make them run in parallel
                _ = transform.DoScaleAsync(Vector3.one * 0.01f, 0.5f, Ease.InBack);
                //If the panel fadeout duration is less than that of the blocker, the blocker will fade out first and then the panel will suddenly become inactive.
                // so that blocker fadeout duration should same to panel fadeout duration
                await this.UnblockAsync(0.5f);
                gameObject.SetActive(false);
                cts?.Dispose();
                return index; // result should never be wait 
            }
    
            public async void HandleBlockClickedAsync()
            {
                if (closeByBlock)
                {
                    cts?.Cancel();
                }
                else
                {
                    await transform.DoShackPositionAsync(0.3f, Vector3.one * 20);
                }
            }
        }
    }
    

    PanelManager : 简单的 panel 唤起演示脚本

    using System;
    using UnityEngine;
    using UnityEngine.UI;
    using zFramework.Example;
    
    public class PanelController : MonoBehaviour
    {
        public Button button;
        public Button button2;
        public NotificationPanel panel;
        public NotificationPanel panel2;
    
        private void Start()
        {
            button.onClick.AddListener(OnClick);
            button2.onClick.AddListener(OnClick2);
        }
    
        // Open a panel which is not blocked by blocker
        private async void OnClick2()
        {
            if (!panel2.gameObject.activeSelf)
            {
                var title = "Panel without a Blocker";
                var content = "This panel will be overlaid by other UI as it does not use a blocker!";
                var idx = await panel2.ShowAsync(title, content);
    
                Debug.Log("user selected : " +idx + (idx == 0 ? "确定" : (idx == -1 ? "用户取消操作" : "取消")));
            }
        }
    
        // Open a panel which is blocked by blocker
        public async void OnClick()
        {
            if (!panel.gameObject.activeSelf)
            {
                var title = "Panel with a Blocker";
                var content = "This panel will be rendered on the top layer!";
                var idx = await panel.ShowAsync(title, content);
                Debug.Log("user selected : " + idx + (idx == 0 ? "确定" : (idx == -1 ? "用户取消操作" : "取消")));
            }
        }
    }
    

    这个 Panel 示例脚本充分利用了 Blocker 组件。点击 "Open Panel" 按钮,模态窗口无论在哪个层级都将置顶显示,其他 UI 元素被阻挡。点击 "Close Panel" 按钮或模态窗口外区域,即可关闭模态窗口。

    没有使用 Blocker 使用了Blocker
    这个层级下的面板完全被遮挡 实现了面板不 care 层级永远置顶

    值得一提的是,NotificationPanel 模态窗口具备一项有趣特性:通过面板右下角 toggle 开关可以切换点击模态窗口外区域后面板自身的行为:关闭或者抖动窗口,使操作更灵活。

    结论

    • 利用 "Blocker" 组件,我们轻松管理 Unity UGUI 的 UI 层级,提升用户操作体验,避免常见的 必须改变层级才能改变渲染先后关系的问题
    • 希望这篇博客能为你解决问题提供帮助。欢迎在评论区分享你的问题或建议。

    开源代码

    愿你的编程之路愉快!

    相关文章

      网友评论

        本文标题:[Unity 3d] UIBlocker - 解决 UGUI 层

        本文链接:https://www.haomeiwen.com/subject/ksqlndtx.html