CustomPropertyDrawer

NZQLA / 2023-08-18 / 原文

Unity3D CustomPropertyDrawer 自定义属性绘制器

api文档

- 该文档中的```EditorGUI.BeginProperty()```和```EditorGUI.EndProperty()```,不好用
  • 参考案例:
    • 直接看Unity中你感兴趣的渲染方式的实现方式: Packages/com.unity.ugui/Editor/UI/PropertyDrawers/...
      • FontDataDrawer
      • SpriteStateDrawer
      • ColorBlockDrawer
      • NavigationDrawer
      • 也可以直接在Project视图搜索Drawer,搜索范围选择In Packagers

        Serach Drawer In Project

需要做的事

- 绘制
- 重写```GetPropertyHeight``` 返回真正的高度
    - 不要忘记标题占了1行
    - 考虑展开 / 收缩 的情况, 返回不同的高度,
        - 收缩时,高度=```EditorGUIUtility.singleLineHeight```
        - 展开时,高度=(1 + 标题以外所有内容的行数) x ```EditorGUIUtility.singleLineHeight ``` + ```EditorGUIUtility.standardVerticalSpacing``` x (所有行数-1)

基本常识:

- ```OnGUI(Rect position, SerializedProperty property, GUIContent label)```中
    - ```position``` 是这个变量的起始位置
    - ```property``` 是这个变量的```SerializedProperty```对象
        - 通过这个对象,可以获取到变量的值 ```property.FindPropertyRelative(paramName).(intValue / stringValue / vector3Value / enumValueIndex / ...)```
    - ```lable.text``` 是变量名称
- 默认每行高度是: ```EditorGUIUtility.singleLineHeight```
- 默认行间距是: ```EditorGUIUtility.standardVerticalSpacing```
- 想要缩进/反缩进 1个单位, 可以使用: ```EditorGUI.indentLevel++``` 和 ```EditorGUI.indentLevel--```
- 对于不需要自定义渲染方式的字段  使用 ```EditorGUI.PropertyField```执行默认的渲染方案, 
- 所有要绘制的内容,推荐使用```EditorGUI```类, 而不是```GUI```类
	- ```EditorGUI```类的方法, 会自动处理缩进的问题
	- ```GUI```类的方法, 不会自动处理缩进的问题
        - 比如``` GUI.Button(rect, "↑"))```,不受缩进的影响,所有要额外把```rect.x```加上缩进的距离```35```比较合适

小技巧

- 绘制展开 / 折叠按钮
	- 使用```EditorGUI.Foldout``` -> ```  if (foldout = EditorGUI.Foldout(rect, foldout,$"{label.text} 更多内容:{}")```
        - 标题行除了显示变量名称以外, <font color="#aaaa00">其实可以显示更多信息, 以便在没有展开的情况下就可以把 关键信息 显示到标题行的标题后面</font>

⚠️ 注意:

- 必须十分清除, 自己的绘制方案, 占用了多少高度, 否则会出现绘制不全的情况
- 绘制时
    - 关于```layout```周边的方法
        - 在```EditorGUI.BeginProperty()```和```EditorGUI.EndProperty```包裹的范围里,不可以使用 ```layout``` 相关的方法, 否则会报错:```ArgumentException: Getting control 1's position in a group with only 1 controls when doing repaint```
        - 在```EditorGUI.BeginProperty()```外面, 执行```layout```相关的方法, 不生效不显示

简单举例

        [Serializable]
        public class RotaAtAxisData
        {
            public Transform self;

            public Vector3 axis = Vector3.up;
            public float angleTotal;
            public float duration = 1;
            public Space space = Space.World;
        }

默认显示如下:

img

重写后↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓如下:

img

  • 变量名字后面直接把关键信息,显示在变量名称后面,即便不展开,也可以看到关键信息

  • 允许点击按钮,直接选择6个标准轴向作为旋转轴

  • 使用2个Toggle来渲染space,要选择Space.World / Space.Self时,直接点就行,下拉框需要点2次,不方便

  • Bug:

    • 当视图过于小时,AxisXYZ会被挤到下一行,导致覆盖下一行内容,这个问题暂时没有解决方案
  • 完整代码如下:

using System;
using System.Collections;
using UnityEngine;
using UnityEditor;

namespace BaseToolsForUnity
{
    /// <summary>
    /// RotaAtAxisData 检视视图个性化渲染器<br/>
    /// </summary>
    [CustomPropertyDrawer(typeof(TransformTween.RotaAtAxisData))]
    public class RotaAtAxisDataDrawer : PropertyDrawer
    {
        // 自身的所有变量
        private SerializedProperty self;
        private SerializedProperty axis;
        private SerializedProperty angleTotal;
        private SerializedProperty duration;
        private SerializedProperty space;

        private float axisButtonWidth = 20;
        private float axisButtonSpaceHor = 5;
        private float spaceToggleWidth = 70;

        /// <summary>
        /// <see langword="true"/>:展开<br/>
        /// </summary>
        private bool foldout = false;

        private void Init(SerializedProperty property)
        {
            self = property.FindPropertyRelative("self");
            axis = property.FindPropertyRelative("axis");
            angleTotal = property.FindPropertyRelative("angleTotal");
            duration = property.FindPropertyRelative("duration");
            space = property.FindPropertyRelative("space");
        }



        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            Init(property);

            var rect = position;
            rect.height = EditorGUIUtility.singleLineHeight;

            //if (foldout = EditorGUI.Foldout(rect, foldout, new GUIContent($"{label.text}({typeof(TransformTween.RotaAtAxisData).Name})")))
            //if (foldout = EditorGUI.Foldout(rect, foldout, new GUIContent($"{label.text} Axis:[{axis.vector3Value}] Angle:[{angleTotal.floatValue}] Time:[{duration.floatValue}]")))
            // 折叠 / 展开 (附加关键信息到标题行后面)
            if (foldout = EditorGUI.Foldout(rect, foldout, EditorGUIUtility.TrTextContent($"{label.text} Axis:[{axis.vector3Value}] Angle:[{angleTotal.floatValue}] Time:[{duration.floatValue}] Space:[{((Space)space.enumValueIndex)}]")))
            {
                //// 绘制标题
                //EditorGUI.LabelField(rect, $"{label.text}({typeof(TransformTween.RotaAtAxisData).Name})");

                // 开始缩进 // 开始绘制内部的字段
                ++EditorGUI.indentLevel;

                // 绘制 self 字段 
                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(rect, self);

                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;

                #region 绘制 轴向选择 按钮 到1行
                var rectBackUp = rect;
                rect.x = 35;
                rect.width = axisButtonWidth;
                if (GUI.Button(rect, "↑"))
                {
                    axis.vector3Value = Vector3.up;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "↓"))
                {
                    axis.vector3Value = Vector3.down;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "F"))
                {
                    axis.vector3Value = Vector3.forward;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "B"))
                {
                    axis.vector3Value = Vector3.back;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "←"))
                {
                    axis.vector3Value = Vector3.left;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "→"))
                {
                    axis.vector3Value = Vector3.right;
                }
                // 恢复 rect
                rect = rectBackUp;
                #endregion


                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(rect, axis);

                // 绘制要旋转的总角度
                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(rect, angleTotal);

                // 绘制动画时间
                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(rect, duration);

                // 绘制空间类型
                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                // 绘制空间类型的 名称
                EditorGUI.LabelField(rect, $"{space.displayName}");

                // 绘制空间类型 世界空间
                rect.width = spaceToggleWidth;
                rect.x += EditorGUIUtility.labelWidth;
                var isWorld = EditorGUI.ToggleLeft(rect, Space.World.ToString(), (Space)space.enumValueIndex == Space.World);
                space.enumValueIndex = isWorld ? (int)Space.World : (int)Space.Self;

                // 绘制空间类型 自身空间
                rect.x += spaceToggleWidth;
                var isLocal = EditorGUI.ToggleLeft(rect, Space.Self.ToString(), (Space)space.enumValueIndex == Space.Self);
                space.enumValueIndex = !isLocal ? (int)Space.World : (int)Space.Self;

                // 结束缩进 // 结束绘制内部的字段
                --EditorGUI.indentLevel;
            }
        }

        /// <summary>
        /// 重写 GetPropertyHeight 方法
        /// </summary>
        /// <param name="property"></param>
        /// <param name="label"></param>
        /// <returns></returns>
        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            Init(property);
            // 折叠状态
            if (!foldout)
            {
                return EditorGUIUtility.singleLineHeight;
            }

            // 展开状态
            // 行数 = 视觉上看到的行数 + 1(标题行)
            // 间隙数量 = 行数 -1
            var height = EditorGUIUtility.singleLineHeight * 8 +
                EditorGUIUtility.standardVerticalSpacing * 7;
            return height;
        }

    }

}