控件以 ZIndex 为 order 确认纵向层级,同一层级的控件按照显示范围依次绘制。存在遮挡区域的不同控件,通过设定不同的 Z 轴 index 进行层级划分,默认为 0,越往上数值越高。
View 控件集合:
控件 | 含义 |
UKLabel | 文本控件 |
UKButton | 按钮控件 |
UKImageView | 图片控件 |
UKMapView | 地图控件 |
UKTableView | 列表控件 |
UKCheckBox | 单选框控件 |
UKLoadingView | loading控件 |
UKViewGroup | View组合控件 |
并且定义了 View 的通用属性:
属性 | 含义 |
id | 文本控件 |
rect | 显示区域 |
backgroundcolor | 背景色值 |
UKMapView | 地图控件 |
zIndex | 纵向index |
invisible | 是否隐藏 |
这里有一个有意思的点。通用属性里有一行:invisible、bool 值。含义是:是否隐藏。
这里没有用 visible:是否显示。简单解释一下,中间通过 JCE 数据格式进行数据传输,默认不填数据,bool 值默认是 false。那么假设这里设置的是 visible,而用户没有设置该属性的话,值就默认是 false,不显示了。这可不是用户想要的,用户还是希望默认是显示的。于是便有了这样的定义。
下面是一个文本控件的JCE格式示例:
struct UKLabel {
// View通用属性
0 require UKInt id; //唯一标示
1 optional UKInt zIndex;//z轴索引
2 require UKRect rect;//显示区域,坐标,宽高
3 optional UKBool invisible;//是否隐藏
4 optional UKColor backgroundColor;//背景色
// 文本属性.
5 optional UKString text; //文本
6 optional UKColor textColor; //文本颜色
7 optional UKColor highlightedTextColor;//高亮时的颜色
8 optional UKFont font;//字体
9 optional UKTextAlignment textAlignment;//文本位置,居中,居左等
10 optional UKEllipsis ellipsis;//文本省略方式
};
3.3 数据传输
UI框架大致如上,而如何将这一整套框架运转起来呢——数据驱动。
设想一下荣耀页面整体的运转流程:
王者用户点击荣耀战区,会进入荣耀地图页面。那么这时候,需要进入该场景,也就需要创建一个 Scene。然后需要加载一个页面,就是一个Page。之后在 Page 上添加地图 View、添加按钮、添加图片、添加文字等元素。经过这些元素的添加,整个页面就显示出来了。 |
然后,接受用户的事件,譬如说一个按钮的点击,点击事件获取到以后,就需要进行下一步的处理,譬如修改某个文本,设置某个图片的元素等等,也就是会继续向该框架发送下一个指令。
总结来说,要做两件事:Unity 向 Friday 发送指令,Friday 将用户事件回调给 Unity。这两件事情可以归纳为:方法调用和事件回调。
这里要解决两个问题:
1)如何通过数据完成方法调用和事件回调?
2)如何找到对应的调用对象?
3.3.1 方法调用举一个例子,设置文本控件的文字,正常的方法调用是这样的:
class UKLabel{
/**
设置文本
*/
public void setText(UKString text){
if(text != null){
setText(text.getval())
}
}
}
UKLabel label = new UKLabel();
label.setText("hello world");
那么如何去解决呢,方法如下:
如上图所示,方法名对应数据的变量名,参数对应数据的参数值,参数类型就对应的是数据的参数类型,是否被调用就对应变量值是否为空,这样就完成了一个普通方法的调用。
下一个问题:多个参数如何处理?既然参数类型对应的是变量类型,那么多个参数只要设计一个结构体进行存储即可。
按照这套规则,我们可以看到,同时可以有多个方法被调用。这大大增加了使用的灵活性,减少冗余数据的出现。而顺序则是按照既定或协商好的顺序执行。
方法可以调用了,接下来就是修改文本,但修改哪一个文本控件的文字呢?
这就需要找到指定的文本控件。如前面的 Label 的 JCE 数据所示,所有的 View 控件都是有一个 id 的,而且所有的 View id 要求必须唯一,而且 id 的规则是由外部调用者(王者)决定的。这就解决了方法调用对象的问题,通过 id 索引,找到对应的View控件,从而调用到该控件支持的方法,完成完整的方法调用。
因此,一个方法调用包括两部分:方法目标(Target)、方法体(Method)。
Target 中包含该对象的 id,方法体包含具体的方法数据。而这里还需要解决一个问题,因为拿到的数据虽然有对象,通过对象也能知道该对象的类型,并且拿到该对象类型支持的方法类型,也能把方法体解析出来。但为了方便,还是直接将方法类型封装在 target 里,便于快速解析,如:
由于所有数据都进行了 JCE 格式的压缩,数据以二进制的形式通过阿波罗团队在Unity 和 Friday 之间传递,对外暴露的接口在 android 侧是下面这个样子:
/**
* 对外调用接口
* @param target
* 消息目标,jce格式化后的数据
* @param method
* 数据参数,jce格式化后的数据
* */
public void call(byte[] target, byte[] method);
所有的方法调用都是通过该通道传输。
3.3.2 事件回调方法调用完成后,另一块就是看各种事件如何传递给 Unity 侧。如一个点击事件:一个TableView 的某一项被点击、CheckBox 某一项被选中、某个地图上的标注被点击等等。
如何构造回调事件,需要解决两个问题:
1)是谁发生了点击或状态变化 2)发生的变化是什么 |
关于1):因为每个对象都有了唯一的标识,所以向外输出时,可以将该id对外发布。而为了外部解析的便捷,也将回调的对象类型和数据类型一起回调给 Unity。
示例如下:
struct UKCallbackTarget {
0 require UKInt targetID;//回调时间的id
1 require UKTargetType targetType;//回调的对象类型 如:Button,TableView
2 require UKCallbackType callbackType;//回调数据类型,如点击或者状态变化
};
关于2):对应不同的点击事件,定义了不同的回调类型,并且将所需的数据封装起来一起回传。如 TableView 的点击回调数据类型,需要回调 Unity 哪一条被点击:
struct UKTableViewCallbackData_Clicked {
0 require UKInt idx; //被点击的item的index
};
其他的回调也都是类似,同方法调用,回调对外提供的接口为:
/**
* 回调,目前支持点击回调,或者事件回调
* @param target
* 回调事件对象
* @param @data
* 回调事件数据
* */
public void callback(byte[] target, byte[] data);
而阿波罗团队只需要将方法调用和事件回调中的两份数据传递给王者团队,即可完成通道作用。
3.4 小结通过UI的框架和方法的调用以及回调系统的设计和研发,整体的设计架构也就基本搭建完成了,剩下的就是不同UI控件的具体实现和接口输出了。这一部分是在第一周研发的前期完成,包括文本、图片、TableView、按钮等控件等,通过这些已经可以基本模拟出王者第一个页面的显示。第一周的研发工作也基本告一段落,下一步就是”开赴成都,与王者团队会师“!
04
遇到的问题和解决方案
第一周时,团队准备了详细的设计方案和使用文档,以为可以轻轻松松去联调了。结果还是遇到了很多问题。
4.1 三端坐标系统一Untiy 有自己的一套坐标系,拿到的坐标系在 Android 侧既不是 dp 也不是像素,在 ios也是一样。当时自己和同事的第一反应是找一下 Unity 的坐标系原理,确认其和端上的转换关系,只有这样才能把控件绘制到王者游戏中想要的位置。
我们在不同的设备上测试了一下,没有找到什么规律,也查找了 Unity 坐标相关的文档,短时间内没有找到解决问题的思路。Andorid 和 ios 建立的都是以像素为单位的坐标系,如果寄希望于上层 Unity 以终端的设备为单位的坐标系去设置所有控件的宽高、位置等属性,对于 Unity 是很大的负担。
但无论坐标系是怎么样的,都是一个基于平面的坐标系,而屏幕宽高比是一致的。如王者在 Vivo XPlay5 获取的屏幕宽高(横屏)是:
size: {
width: {
val: 1280
}
height: {
val: 720
}
}
而终端通过以下代码获取屏幕宽高:
WindowManager wm = this.getWindowManager();
ScreenUtils.width = wm.getDefaultDisplay().getWidth();
ScreenUtils.height = wm.getDefaultDisplay().getHeight();
结果:width:2560;height:1440;手机屏幕密度是 3.5
由于王者所有的UI元素都是基于范围为(1280*720)的坐标系建立的,而手机端的显示都是基于(2560*1440)的坐标系建立的,但比例是一样的,只需要将所有的坐标做一个比例映射就可以解决。
4.2 Android 点击事件处理4.2.1 原生 View 无法获取焦点在加载 Android 原生 View 后会出现一个问题,从UI层级上看,原生页面在上,Unity 页面在下,但上层却没有收到点击事件。经过和阿波罗团队的沟通,得出了解决问题的思路和方案:
我们知道,Android 程序都是运行在 dalvik/art 虚拟机上的,而 Unity 程序是运行在(mono/il2cpp)上。当一个Unity应用想要用到 Andorid 的方法时,毫无疑问,这个应用就需要两套虚拟机同时运行,即两个虚拟机运行在同一个进程中。 那么,Unity 与 Android 之间的交互,其实就是两个 VM 之间的相互调用,如下图: 如上图所示,Unity 通过 UnityEngine 提供的 API 调用 Android 的方法;Android 借助 com.unity.player 包提供的 API 调用 Unity 的方法。 |
点击事件是先由 Unity 侧先收到,如果需要传递到 Android 侧,可以设置:统一转发机制允许将事件传播到 DalvikVM。需在AndroidManifest.xml 文件中的 activity 子节点下增加如下两行代码。
<meta-data android:name="android.app.lib_name" android:value="unity" />
<meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" />
通过此方式,将点击事件传递到 Android 侧。点击传递如下: