一種局部ui界面切換解決方案

前段時間開發的動態加載ui界面的功能,在程序面版不太多的情況下還是減少了不少工作量,但由於開發的程序日漸複雜,才終於理解了那些可視化編程插件的意義。畢竟程序再怎麼精練也有太多的重複功能,少量的變化就造成了開發過程中無止境的勞動。爲此目前也在研究相關的程序化開發程序的插件功能,這兩天遇到的面板開發數量已經達RunTimeUIPanel-ugui這個模塊工作的極限 ---- ---- 面板分解粒度過細。

這幾日,想在一個面板組合中實現界面之間的切換,但由於切換過來切換過去比較亂,也都是些重複的東西,寫了些就放棄了。但功能還是需要實現的,所以就想到把界面切換這種功能抽象出來,和界面啓動時動畫播放播放一樣,寫在一個單獨的腳本中。由於unity3d中最爲常用的就是利用button和toggle這兩個控件來實現界面的打開和關閉,所以這個模塊目前只支持這種方式的界面切換,因爲每個面板都可以打開一組子面板,這樣像極了xml中的節點的效果,於是我將這個模塊取名叫NodePanels.

下面介紹一個這個 簡單 卻實用的 NodePanels 

一、使用詳情

1.Demo背景

假設有一個面板集合,包含三個面板,程序運行時就顯示其中一個主要的面板,這個主要的面板上面有一個Button和一個Toggle,如果圖一所示:

[圖一]

當點擊這個面板上的Red這個Button時需要隱藏自身並打開一個紅色面板,如圖二所示

[圖二]

當點擊圖一面板中的green這個Toggle時需要打開一個彈窗,如圖三所示

[圖三]

2.腳本掛載

這樣的切換,其實是無限重複的一種狀態,寫程序一定會遇到,NodePanels的功能就是要將這樣重複的功能封裝起來,下次遇到直接使用就可。要使用這個模塊要進行如下的操作:首先在三個面板身上都掛上一個NodePanel這個腳本,在面板一上掛好後設置如圖四所示。

[圖四]

Start表示一開始就打開,程序中不會進行任何操作,hideSelf選中後將會在打開目標面板後隱藏自身,這個功能暫時只是目標面板的打開方式爲Button時生效。Red面板設置如圖五所示。

[圖五]

這裏只需要設置自身的打開方式爲Button就可以了,closeButton在自己打開的方式爲Button時生效,用於關閉自身並打開父級窗口。

Green面板的設置如圖六所示。

[圖六]

如果打開方式爲Toggle,則無需進行其他選擇,當然作爲一個nodePanel,都可以自己作爲父級再次包含子面板,而達到無限擴展的效果。但目的僅是在高度關聯的一組面板間切換,而沒有考慮複用的情況。

二、代碼說明

1.功能主體

以下這個腳本就是上面那個個NodePanel腳本,內部記錄了相關的控件和麪板信息,並在Awake中啓動時進行註冊相關的事件,關閉和開啓後,在其他腳本的OnEnable和OnDisable中可以處理對應的事件

public sealed class NodePanel : MonoBehaviour
    {
        public Button closeBtn;
        public OpenType openType;
        public List<NodePanelPair> relatedPanels;
        private UnityAction onClose;
        private void Awake()
        {
            if (openType == OpenType.ByButton){
                if(closeBtn) closeBtn.onClick.AddListener(Close);
            }
            for (int i = 0; i < relatedPanels.Count; i++)
            {
                NodePanelPair pair = relatedPanels[i];
                if (pair.nodePanel.openType == OpenType.ByButton)
                {
                    if (pair.openBtn != null) pair.openBtn.onClick.AddListener(() =>
                    {
                        if (pair.hideSelf)
                        {
                            if (pair.nodePanel.Open(() => { UnHide();}))
                            {
                                Hide();
                            }
                        }
                        else
                        {
                            pair.nodePanel.UnHide();
                        }
                    });
                }
                if ((pair.nodePanel.openType & OpenType.ByToggle) == OpenType.ByToggle)
                {
                    if (pair.openTog != null) pair.openTog.onValueChanged.AddListener((x) =>
                    {
                        if (x)
                        {
                            pair.nodePanel.UnHide();
                        }
                        else
                        {
                            pair.nodePanel.Close();
                        }
                    });
                }
            }
        }
        public void Hide()
        {
            gameObject.SetActive(false);
        }
        public void UnHide()
        {
            gameObject.SetActive(true);
        }

        public bool Open(UnityAction onClose)
        {
            IOpenAble panel = gameObject.GetComponent<IOpenAble>();
            if ((panel != null && panel.OpenAble()) || panel == null)
            {
                UnHide();
                this.onClose = onClose;
                return true;
            }
            else
            {
                return false;
            }
        }
        public void Close()
        {
            ICloseAble panel = gameObject.GetComponent<ICloseAble>();
            if ((panel != null && panel.CloseAble()) || panel == null)
            {
                for (int i = 0; i < relatedPanels.Count; i++){
                    relatedPanels[i].nodePanel.Hide();
                }
                if (onClose != null) onClose.Invoke();
                Hide();
            }
        }
    }


2.外用接口

下面這兩個腳本,需要由目標面板上的腳本來繼承,也就是說,目前腳本將有權力控制對應的面板是否能關閉和打開,可以防止一些問題還沒有處理完的時候就觸發了關閉,和防止一此問題還沒有處理就被打開。

  public interface IOpenAble
    {
        bool OpenAble();
    }

  public interface ICloseAble
    {
        bool CloseAble();
    }
3.界面重繪

雖然這個面板的功能已經有了,但由於Button打開的面板和Toggle打開的面板需要的參數不相同,所以還需要將面板進行一定的重繪。這裏依舊使用Rotorz這個列表插件。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using System.Collections.Generic;
using UnityEditor;
using Rotorz.ReorderableList;
using NodePanels;

[CustomPropertyDrawer(typeof(NodePanelPair)), CanEditMultipleObjects]
public class NodePanelPairDrawer : PropertyDrawer
{

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        float height = 0 ;
        var nodePanelProp = property.FindPropertyRelative("nodePanel");
        if (nodePanelProp == null||nodePanelProp.objectReferenceValue == null){
            height += EditorGUIUtility.singleLineHeight;
        }
        else 
        {
            height += 2 * EditorGUIUtility.singleLineHeight;
            var obj = new SerializedObject(nodePanelProp.objectReferenceValue);
            var typeProp = obj.FindProperty("openType");

            OpenType type = (OpenType)typeProp.intValue;

            if ((type & OpenType.ByButton) == OpenType.ByButton)
            {
                height += EditorGUIUtility.singleLineHeight;
            }
        }
        
        return height;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var rect = position;
        rect.height = EditorGUIUtility.singleLineHeight;

        var hideSelfProp = property.FindPropertyRelative("hideSelf");
        var openBtnProp = property.FindPropertyRelative("openBtn");
        var openTogProp = property.FindPropertyRelative("openTog");
        var nodePanelProp = property.FindPropertyRelative("nodePanel");

        if (nodePanelProp == null || nodePanelProp.objectReferenceValue == null) {
            EditorGUI.PropertyField(rect, nodePanelProp);
            return;
        }
        else
        {
            var obj = new SerializedObject(nodePanelProp.objectReferenceValue);
            var typeProp = obj.FindProperty("openType");

            OpenType type = (OpenType)typeProp.intValue;

            if (type  == OpenType.ByToggle)
            {
                EditorGUI.PropertyField(rect, openTogProp);
            }
            if (type == OpenType.ByButton)
            {
                EditorGUI.PropertyField(rect, hideSelfProp);

                rect.y += EditorGUIUtility.singleLineHeight;
                EditorGUI.PropertyField(rect, openBtnProp);
            }

            rect.y += EditorGUIUtility.singleLineHeight;
            EditorGUI.PropertyField(rect, nodePanelProp);
        }
       
    }
}

[CustomEditor(typeof(NodePanel)), CanEditMultipleObjects]
public class NodePanelDrawer : Editor
{
    SerializedProperty script;

    SerializedProperty openTypeProp;
    SerializedProperty relatedPanelsProp;
    SerializedPropertyAdaptor adapt;
    SerializedProperty closeBtnProp;
    private void OnEnable()
    {
        script = serializedObject.FindProperty("m_Script");
        openTypeProp = serializedObject.FindProperty("openType");
        closeBtnProp = serializedObject.FindProperty("closeBtn");
        relatedPanelsProp = serializedObject.FindProperty("relatedPanels");
        adapt = new SerializedPropertyAdaptor(relatedPanelsProp);
    }
    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(script);
        EditorGUILayout.PropertyField(openTypeProp);
        OpenType type = (OpenType)openTypeProp.intValue;

        if (type == OpenType.ByButton){
            EditorGUILayout.PropertyField(closeBtnProp);
        }
        ReorderableListGUI.Title("相關面板");
        ReorderableListGUI.ListField(adapt);
        serializedObject.ApplyModifiedProperties();
    }
}


三、問題解釋

1.如果需要在面板間傳送數據

一開始我被侷限在了數據傳遞這個過程,還寫了一種不同於OpenType.ByButton和ByToggle的ByScript,通過事件註冊的方式進行數據傳遞,但後來一樣,既然是高度關聯的一組面板,直接共享一個數據對象就可以了,有腳本中直接關聯根目節點的那個腳本,於是就不存在數據傳遞的問題了,同時還實現了界面之間的解耦合。

2.有時並不是想打開就打開,想關閉就關閉

由於將界面打開的功能分離出來就不容易很好的控制界面是否能夠打開和關閉,所以還是需要在原腳本中去實現以上兩個接口,當然了,不實現也不會錯,默認就當成返回爲true了。

3.複雜度不足以應付產品

的確,這個模塊是非常簡單的功能,不能夠應付大量的面板切換,所以最好只用於少量的關聯高的一組面板中。像那種可以重用和相對獨立的面板之間的切換還是要另尋他法的,現在我的項目暫時就是和前面那個框架一起用了。

最後,源碼的下載地址如下,本文的demo就在其中:

https://github.com/zouhunter/NodePanels

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章