1. 程式人生 > >一種區域性ui介面切換解決方案

一種區域性ui介面切換解決方案

前段時間開發的動態載入ui介面的功能,在程式面版不太多的情況下還是減少了不少工作量,但由於開發的程式日漸複雜,才終於理解了那些視覺化程式設計外掛的意義。畢竟程式再怎麼精練也有太多的重複功能,少量的變化就造成了開發過程中無止境的勞動。為此目前也在研究相關的程式化開發程式的外掛功能,這兩天遇到的面板開發數量已經達

這幾日,想在一個面板組合中實現介面之間的切換,但由於切換過來切換過去比較亂,也都是些重複的東西,寫了些就放棄了。但功能還是需要實現的,所以就想到把介面切換這種功能抽象出來,和介面啟動時動畫播放播放一樣,寫在一個單獨的指令碼中。由於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