抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

基于 VisualElement 的编辑器 UI 框架,目的是简化编辑器开发时的 UI 构建。利用特性标签进行 UI 的初始化。

对于变量,我们只需要打上对应的标签,就能够在 UI 面板上显示对应的 UI。

对于方法,我们只需要打上 Button 标签,就能够生成一个实现该方法的按钮。

同时,本框架还支持对 UI 的样式和布局进行有限的修改,同样也是通过特性实现。

UI 界面的渲染顺序按照编辑器脚本对象的声明顺序排序,布局的结构也要按照顺序声明,可以看作是以代码的形式绘制 UI。

本框架支持自定义拓展功能,具体的使用说明和拓展规则见:

原理

初始化特性

我们通过基类初始化获取 Type,然后通过 Type 获取 members 并且按照声明顺序排序,之后通过遍历,在其循环体内根据对应的特性类别进行 UI 的渲染函数初始化。

初始化分为三种情况:

  1. 普通 UI。
  2. 列表。
  3. Box。

所有初始化的 UI 元素都会存储到基类中,通过字段名可以获取到对应的 UI。

初始化 UI

初始化 UI 就是判断 member 特性是否包含 E_Editor ,并且字段不是 List 类型。

然后我们根据 E_Editor 的类型来生成不同的 UI。每个 UI 我们都要调用样式的初始化方法,这样才能实现通过特性修改样式。由于 VisualElement 具有复杂的嵌套关系,因此在调用样式初始化方法的时候要注意传入的 UI 主体是正确的。

UI 的形式如下:

graph LR;
    table_1["父容器
标签输入框
"]

每个 UI 都有父容器,父容器本身不提供样式修改,只是构成 UI 的最上层单位。如果有需要的话可以自己添加样式修改的逻辑。

初始化列表

初始化 UI 就是判断 member 特性是否包含 E_Editor ,并且字段是 List 类型。

列表的初始化与普通 UI 初始化略有不同,不过其内部 Item 的生成逻辑和普通 UI 是相同的。

目前列表不支持虚拟化。

列表结构如下:

graph TD;
    subgraph outerTable["父容器"]
        subgraph innerTable1["列表"]
            table_1["列表元素1"]
            table_2["列表元素2"]
        end
    end

列表还衍生了添加列表元素和移除列表元素两个方法。

添加列表元素通过字段名获取对应的列表和字段,然后通过和列表初始化一样的方法来添加列表元素;移除列表元素的逻辑和添加类似,只是逻辑比添加简单一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// 添加列表元素
/// </summary>
/// <param name="id"></param>
protected void ListAdd(string id)
{
VisualElement element = GetElement(id);
VisualElement list = element.Children().ElementAt(1);

FieldInfo field = _type.GetField(id, _flag);

E_Editor editor = field.GetCustomAttribute<E_Editor>();

IList data = field.GetValue(this) as IList;
data.Add(null);

Action<object> setData = o => { data[data.Count - 1] = o; };

VisualElement subBox = new VisualElement();
list.Add(subBox);

GenerateItem(subBox, field, editor.GetEType(), null, setData, true);

RegisterDrag(subBox, list, data);
}

/// <summary>
/// 移除最后一个数组元素
/// </summary>
/// <param name="id"></param>
protected void ListRemove(string id)
{
VisualElement element = GetElement(id);
VisualElement list = element.Children().ElementAt(1);

if (list.childCount == 0)
{
Debug.LogWarning("没有元素了");
return;
}

list.RemoveAt(list.childCount - 1);

FieldInfo field = _type.GetField(id, _flag);
IList data = field.GetValue(this) as IList;
data.RemoveAt(data.Count - 1);
}

初始化 Box

初始化 Box 需要判断是否具有 VE_Box 特性,一般来说 VE_Box 不会与 E_Editor 混用。

初始化 Box 需要单独一个字段来创建 Box,字段为 string 类型,字段的值就是 Box 的名称,用于后面查找 Box。Box 的显示名称需要配合 E_Name 来设置。

如果有传入 _boxName ,那么创建的 Box 就会去查找相关的 Box,然后添加到里面,否则添加到根节点。

对于 UI 元素添加到 Box 来说,只需要打上 VE_Box 的特性,然后在构造函数传入 Box 的名字就可以。

初始化样式

样式的初始化分为两种:

  1. UI 元素样式初始化。
  2. Box 样式初始化。

Box 样式初始化相对简单,只需要根据对应的特性设置 style 即可。

UI 元素样式不仅需要设置 style,还因为设置 style 后会影响原来的伪类的样式,因此需要重新设置监听并修改不同情况下的 UI 元素样式。

Button UI 因为本身劫持了所有左键事件,因此需要额外的处理,详情见源码

初始化拖拽

初始化拖拽我们对每个列表元素监听三个事件:

  1. 鼠标按下。
  2. 鼠标移动。
  3. 鼠标抬起。

鼠标按下的时候我们记录当前鼠标的位置、当前的元素(拖拽元素)和当前元素的 left 和 top。

鼠标移动的时候我们判断是否是第一次执行移动函数,是的话我们把所有元素都变成绝对布局,并且设置好 left 和 top。

然后我们添加一个空的元素到拖拽元素的索引位置作为占位元素,拖拽元素移动到容器最尾端。这样做是为了让拖拽元素显示在 UI 层级的最上层,并且不会扰乱原来列表的索引(因为 VisualElement 没有 zIndex)。

接着我们捕捉鼠标,这样其他 UI 元素不会被鼠标事件影响,鼠标超出当前元素也能够继续捕获事件。为什么不放在鼠标按下的时候,是因为当列表需要打开对象选择器的时候,捕获鼠标就无法触发这个事件。

鼠标在移动的时候实时修改拖拽元素的 left 和 top,就可以实现元素跟着鼠标移动了。

在鼠标抬起的时候,我们遍历列表子对象,查看是否有和拖拽元素包围盒相交的对象,如果有的话交换两个元素。

交换的过程如下:

graph TD;

A["查找相交元素索引"]
B["移除拖拽元素"]
C["插入拖拽元素到相交元素索引处"]
D["插入相交元素到拖拽元素的原索引处"]
E["交换列表中两个元素的值"]

A --> B --> C --> D --> E

然后我们把所有元素都变为相对布局,置 left 和 top 为 0。

如果父容器中存在占位元素,就移除(因为有可能只有按下没有移动,就不存在占位元素)。在有占位元素的情况下,判断是否有经过元素交换,没有的话就把拖拽元素插入回原来的索引。

最后重置各种状态,包括释放鼠标。

由于focus事件会阻止事件冒泡,会导致鼠标抬起事件无法触发,拖拽状态无法重置,因此我们在根节点监听鼠标左键按下的事件,然后在拖拽的鼠标移动事件中添加该判断条件,当focus的时候根节点的鼠标按下监听被阻止,此时判定鼠标移动的时候就判定为没有拖拽,从而解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/// <summary>
/// 注册拖拽
/// </summary>
/// <param name="ele"></param>
/// <param name="parent"></param>
private void RegisterDrag(VisualElement ele, VisualElement parent, IList data)
{
VisualElement empty = new VisualElement();

int defIndex = 0;

bool isCapture = false;

ele.RegisterCallback<PointerDownEvent>(evt =>
{
if (evt.button == 0) // 左键
{
_dragElement = ele;

_defPos = evt.position;
_defTL = new Vector2(_dragElement.resolvedStyle.left - _dragElement.resolvedStyle.marginLeft,
_dragElement.resolvedStyle.top - _dragElement.resolvedStyle.marginTop);
}
});

ele.RegisterCallback<PointerMoveEvent>(evt =>
{
if (!isCapture && _dragElement != null)
{
ele.CapturePointer(evt.pointerId);
isCapture = true;
foreach (var child in parent.Children())
{
child.style.position = Position.Absolute;
// Debug.Log(child.resolvedStyle.left);
child.style.left = child.resolvedStyle.left - child.resolvedStyle.marginLeft;
child.style.top = child.resolvedStyle.top - child.resolvedStyle.marginTop;
}

//占位
defIndex = parent.IndexOf(_dragElement);
parent.Insert(defIndex, empty);
_dragElement.BringToFront();
}

if (_dragElement != null && ele.HasPointerCapture(evt.pointerId))
{
Vector2 delta = new Vector2(evt.position.x, evt.position.y) - _defPos;
_dragElement.style.top = _defTL.y + delta.y;
_dragElement.style.left = _defTL.x + delta.x;
}
});

ele.RegisterCallback<PointerUpEvent>(evt =>
{
if (_dragElement != null)
{
bool isExchange = false;
foreach (var child in parent.Children())
{
if (child != _dragElement && child.worldBound.Contains(evt.position))
{
//UI 交换
int insertIndex = parent.IndexOf(child);
parent.Remove(_dragElement);
parent.Insert(insertIndex, _dragElement);
parent.Insert(defIndex, child);

//数据交换
(data[defIndex], data[insertIndex]) = (data[insertIndex], data[defIndex]);
isExchange = true;
break;
}
}

foreach (var child in parent.Children())
{
child.style.position = Position.Relative;
child.style.left = 0;
child.style.top = 0;
}

if (parent.IndexOf(empty) != -1)
{
parent.Remove(empty);
if (!isExchange)
{
parent.Insert(defIndex, _dragElement);
}
}

_dragElement.ReleasePointer(evt.pointerId);
_dragElement = null;
isCapture = false;
}
});
}

项目

更新日志

2024-05-31

  1. 更新基础内容。

评论