egret-docs-master/extension/EUI/outline/NewFeatures/README.md

114 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

这篇文章主要介绍EUI库的新特性。在EUI库中我们对这部分做了全面的优化清除冗余计算精简嵌套层次减少类继承深度并让包体最终降到了原先版本的40%。EUI目前的版本gzip后包体仅有36k大小对加载量已构不成压力。
以下是GUI体系时比较集中的几个痛点
1.异步问题。失效验证机制带来的延迟计算优化导致组件宽高无法立即得到立即访问会返回0。
2.不统一的显示列表。两套显示列表使用不便开发者需要额外判断addElement()和addChild()用在什么地方。
3.皮肤分离机制过于严格。皮肤分离机制在定义可复用的外观上更有优势。但对于只使用一次的外观定义不够方便。
新版本的EUI库在提高新手易用性上做了大刀阔斧的重构以上痛点在EUI中也都得到了彻底解决在以下的新特性解读中将会逐一覆盖到。
## 组件立即返回宽高
我们在分析这个问题的过程中,得出了一条避免造成初学门槛的重要结论:子类不应该改变父类行为。
先简单介绍失效验证机制就是属性发生改变时不立即应用改变而是先标记下来延迟一定时间片后再统一计算这样可以有效避免重复的计算量从而提升运行时性能。尤其在使用自适应流式布局时失效验证能起到强力的性能保障作用。而组件的宽高是需要动态测量的所以也需要用失效验证机制延迟计算因此实例化组件后立即访问宽高将会返回0.
跟开发者深入沟通后我们发现真实的需求都是希望能把GUI对象当做普通显示对象来用,统一显示列表. 抽象出来质上几乎本都是相同的问题:子类改变了父类的行为
基于这个结论我们重构了所有子类改变父类行为的设计。确保每个EUI组件都能直接当成普通显示对象来用。现在访问EUI组件宽高时也会跟原生显示对象的表现一致立即能得到包含子项的宽高值。
## 统一的显示列表
之前版本的GUI库引入了addElement()系列方法用于替代addChild()系列方法在GUI范围内调用addChild()方法将会报一个错。这么做的主要原因是自适应流式布局是层层向上测量层层向下布局的。如果GUI显示列表中间混入了一个普通显示对象将会造成自动布局体系断层而在那一层失效。所以想要正常启用自适应流式布局就应该让GUI组件添加在一起中间没有断层。但是这其实应该算一种最佳实践不应该在框架层级去强制限制开发者还是会有混合添加普通显示对象的需求的。之前是提供了UIAsset组件作为普通显示对象的包装器加到GUI显示列表但这还不够方便。
基于以上得出的结论两套显示列表并屏蔽addChild()系列方法的设计显然是违反了「子类不能改变父类行为」原则的。现在EUI里只有addChild()系列方法已经不存在addElement()方法。任意EUI组件和普通显示对象都可以互相混合添加。这里我们需要讨论的是两种添加情况的处理方式
1.普通显示对象添加到EUI组件里。效果跟设置EUI组件的includeInLayout属性为false类似。布局类在计算布局时会主动忽略它对它不测量也不布局这样本身也比较符合预期。那么也就不在需要UIAsset包装器了任何一个EUI组件只要有addChild()方法,都可以直接添加普通显示对象。
2.EUI组件添加到普通显示对象里。这个在之前的的GUI库里就是允许的。在这种情况下EUI组件被当做一个普通显示对象来用你对它设置的leftright等布局属性都无效因为布局属性是需要父级容器对它布局的。而你的父级不是EUI组件。这个就是所谓的断层处。但是只会影响这一层在组件内部再添加其他EUI组件是可以正常布局的。
所以结论是开发者一开始可以不使用自动布局功能把所有EUI组件仅当做普通显示对象来操作。若需要开始用自动布局可以再采用最佳实践的方式将EUI组件都组织到一起。
## EXML支持内部类
首先简单介绍皮肤分离的机制:皮肤分离机制就是将原本一个组件拆分成两个。一个逻辑组件只管代码控制,一个皮肤组件只负责外观。运行时将皮肤组件附加到逻辑组件上,变成一个完整组件。皮肤组件并不是显示对象,实际上更类似一个持有外观信息的数据对象。这样做的好处比较多,例如:方便代码解耦,方便复用外观,方便可视化编辑,等等。
而传统的UI方式通常是只有一个组件在组件上直接修改预设的外观属性。这个带来的问题是你必须在UI组件上声明非常多的 外观属性,例如文本颜色,背景色等,而且不管声明再多,通常也都还是会不够用,导致可自定义的部分比较有限。而皮肤分离的模式,逻辑组件上基本不声明任何外观属性。完全交给另一个皮肤组件去决定外观。这样可以完全自定义,扩展性上比较灵活。
之前的GUI库里必须将皮肤声明为一个独立的EXML文件再引用它。对于需要复用的皮肤这种方式比较理想。而对于只用一次的外观则会比较不便显得文件也特别多。现在EUI里已经支持EXML的内部类定义方式可以直接嵌套写在节点内。通常有两种节点支持作为内部类Skin和ItemRenderer如下
~~~ typescript
<?xml version='1.0' encoding='utf-8'?>
<e:Skin class="skins.DemoSkin" xmlns:e="http://ns.egret.com/eui" minHeight="230" minWidth="470">
<e:Button label="按钮">
<e:Skin states="up,over,down,disable">
<e:Image source="image/button_up.png" includeIn="up" width="100%" height="100%" />
<e:Image source="image/button_down.png" includeIn="down" width="100%" height="100%" />
<e:Label id="LabelDisplay" left="20" right="20" top="10" text="{data.label}" />
</e:Skin>
</e:Button>
<e:List id="list" cacheAsBitmap="false">
<e:itemRendererSkinName>
<e:Skin states="up,down">
<e:Image source="image/button_up.png" includeIn="up" width="100%" height="100%" />
<e:Image source="image/button_down.png" includeIn="down" width="100%" height="100%" />
<e:Label id="LabelDisplay" left="20" right="20" top="10" text="{data.label}" />
</e:Skin>
</e:itemRendererSkinName>
</e:List>
</e:Skin>
~~~
如上面的例子这个Button的外观只有它自己使用那么可以从节点内部可以直接开始描述一个Skin而不需要另外声明一个ButtonSkin的exml文件。另外一个比较常用的是ItemRendererItemRenderer通常都是直接跟List关联的很少有复用的情况现在也可以直接嵌入写在List节点内部。
在代码上看内部类的作用起来可能只是少创建了一个文件而已。但是在工具层面这将会是完全不同的操作体验。之前要定义外观前我们总是得先创建一个皮肤文件编辑完后回来引用这个皮肤文件。现在的流程可以简化为拖拽一个按钮到界面上双击直接进入编辑外观。如果你不需要复用它那么就结束了。当你需要复用这个按钮皮肤时再一键将内部皮肤转换为独立EXML文件变成可复用的。
## 运行时解析EXML
XML的文件结构描述显示列表有着天然的优势。在之前的GUI库里EXML文件是在命令行编译阶段被编译为了JS文件然后作为标准代码加载运行的。我们反复优化了好多次编译结果始终还是EXML文件本身才是最小的记录方式。在EUI中我们将EXML文件改为运行时解析而不再提前编译。这样做的几个好处
能够减少网络加载量。
减少中间转换过程降低调试难度。也可以直接在编辑器编辑后拷贝EXML内容到代码中粘贴解析。
由于之前编译器只为TS开发者设计的现在不再依赖命令行JS开发者也能直接使用EXML文件。
不过不用担心性能问题运行解析并不是每次实例化皮肤都解析一次而是只有第一次解析会将EXML编译为JS代码然后使用eval()方法转换为标准的类定义。之后都直接调用类定义快速创建。
另外之前EXML的模块名是根据所在文件夹路径生成的现在由于EXML文件变成了运行时解析有可能只有文本内容并没有路径信息因此包名也不再依赖文件路径。我们提供了另一种声明类名的方式在EXML根节点上设置class属性class属性的值会被解析并注册为全局类名。若不声明这个EXML文件解析的类定义会被解析器作为一个临时变量返回。声明方式如下图
~~~ typescript
<?xml version="1.0" encoding="utf-8" ?>
<e:Skin class="skins.ButtonSkin" states="up,down,disabled" minHeight="50" minWidth="100" xmlns:e="http://ns.egret.com/eui">
<e:Image width="100%" height="100%" scale9Grid="1,3,8,8" includeIn="up" source="button_up_png" />
<e:Image width="100%" height="100%" scale9Grid="1,3,8,8" includeIn="down" source="button_down_png" />
<e:Image width="100%" height="100%" scale9Grid="1,3,8,8" includeIn="disabled" source="button_disable_png" />
<e:Label id="labelDisplay" top="10" bottom="10" left="20" right="20" verticalCenter="0" horizontalCenter="0"/>
</e:Skin>
~~~
## EXML描述非皮肤对象
之前版本的GUI库里EXML的根节点被限制为Skin皮肤节点只能用于描述皮肤实际上还有较多显示列表初始化需求是直接使用Group等不可定义皮肤组件作为根容器初始化的。这样的情况就只能使用代码方式手动编写显示列表初始化代码而不能使用EXML这种更加简便的描述方式。现在这部分代码在EUI里也可以直接用EXML描述了。EXML的根节点不再必须是Skin可以为任意组件。这个特性全面提升了EXML的适用范围能够简化普通容器的显示列表创建过程。解析后的对象是一个继承自根节点的自定义类。定义了ID的节点会在自定义类上以ID名声明一个成员变量持有该节点的引用。
### 动态数据绑定
数据绑定一直都是呼声相当高的一个便捷功能。现在EUI里也已经对其提供了支持。注意前文「EXML支持内部类」一节中的配图可以发现ItemRendeerer内的Label节点已经使用了数据绑定功能
text="{data.label}"
它表示Label的text属性与ItemRenderer的data.label属性绑定。当列表的数据源改变时ItemRenderer里的Label会自动刷新显示的文本内容。而不用手动写刷新的逻辑代码。在EXML中开发者只需要简单地使用一对{expression}即可完成数据绑定大括号内的expression表示根节点组件上的属性或当前EXML内定义的ID实际上也是根节点上的属性。注意在这个ItemRenderer的例子中由于ItemRenderer是内部类根节点就是ItemRenderer自身。
数据绑定功能相当于给静态的XML语法加入了部分动态刷新的功能能够极大程度减少逻辑代码的编写量在配合列表的ItemRenderer视图刷新尤其方便。之前我们通常要写一个ItemRenderer的逻辑类覆盖dataChanged()方法访问data属性然后重新赋值刷新所有相关的视图组件。现在只需要简单地定义一个数据绑定标签无需任何繁琐的过程。
另外值得一提的是EUI里提供的数据绑定功能是基于setter的方式改变的时候才会触发一次并不是定时刷新检查的。得益于JavaScript的动态语言特性所有的Object对象都可以实现动态数据绑定并不限定于egret引擎内的对象。
### 自动布局兼容旋转缩放
这个特性也相当有用之前版本的GUI库里如果组件设置了旋转或缩放自动布局的时候依然是按照未变换之前的矩形来计算的会造成诸多不便。现在EUI里对这部分也实现了完美兼容不仅旋转缩放对EUI组件使用Matrix进行的任意变换都可以按照实际显示的矩形区域被正确测量和布局。