简介
基于 VisualElement 的编辑器 UI 框架,目的是简化编辑器开发时的 UI 构建。利用特性标签进行 UI 的初始化。
对于变量,我们只需要打上对应的标签,就能够在 UI 面板上显示对应的 UI。
对于方法,我们只需要打上 Button 标签,就能够生成一个实现该方法的按钮。
同时,本框架还支持对 UI 的样式和布局进行有限的修改,同样也是通过特性实现。
UI 界面的渲染顺序按照编辑器脚本对象的声明顺序排序,布局的结构也要按照顺序声明,可以看作是以代码的形式绘制 UI。
本框架支持自定义拓展功能,具体的使用说明和拓展规则见:
原理
初始化特性
我们通过基类初始化获取 Type,然后通过 Type 获取 members 并且按照声明顺序排序,之后通过遍历,在其循环体内根据对应的特性类别进行 UI 的渲染函数初始化。
初始化分为三种情况:
- 普通 UI。
- 列表。
- 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
|
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); }
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 的名字就可以。
初始化样式
样式的初始化分为两种:
- UI 元素样式初始化。
- Box 样式初始化。
Box 样式初始化相对简单,只需要根据对应的特性设置 style 即可。
UI 元素样式不仅需要设置 style,还因为设置 style 后会影响原来的伪类的样式,因此需要重新设置监听并修改不同情况下的 UI 元素样式。
Button UI 因为本身劫持了所有左键事件,因此需要额外的处理,详情见源码
初始化拖拽
初始化拖拽我们对每个列表元素监听三个事件:
- 鼠标按下。
- 鼠标移动。
- 鼠标抬起。
鼠标按下的时候我们记录当前鼠标的位置、当前的元素(拖拽元素)和当前元素的 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
|
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; 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)) { 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; } }); }
|
项目
更新日志