直播公司,活动做的比较多,端内业务主要是做几个特殊的定制化弹窗,但是每次都需要发版本,公司内部统计,让一个用户升级的成本其实很高的。一直寻求动态化的解决方案,无非自己端内写组件,或者寻求外部方案。但是自己研发的成本很高。之前查过luaViewSDK,但是该项目很久没有维护。后来找到了阿里的VirtualView项目,先做个调研。
VirtualView简介
下面简称VV,是阿里Tangram中的动态组件框架,它开创了一种虚拟化开发基础控件的技术,使用方只要按照指定协议实现一个基础控件的尺寸计算、绘制逻辑、布局逻辑,即能实现在宿主容器的 canvas 里实现直接绘制 UI 内容的,让最终渲染出来的视图结构呈现扁平化,提升组件渲染性能。同时为了解决虚拟化 View 带来的原生 View 的能力损失的问题,它支持加载和渲染原生基础控件,两者组合产生合力,既能最大限度发挥性能提升,又能满足特殊场景下的业务需求。 VirtualView 内置实现了一系列基础控件,可以让使用方直接上手尝试;而搭建业务组件的方式采用 XML 模板来编写,这使得业务组件动态更新成为了可能。XML 模板里还支持写数据绑定的表达式,在样式动态化、数据动态化的场景下能非常方便地实现业务需求。(以上为官方文档)。
项目地址:
https://github.com/alibaba/Virtualview-Android
VV项目缺点:
无对应编辑器,官方提供的编译器 如对自定义控件动态编译 使用很麻烦
项目优点:
可以解决现在动态下发的问题。
实时预览工具使用详解
开源地址:
https://github.com/alibaba/Virtualview-Android
缺点:
端内自定义组件无法预览,需要运行才可以展示。
需要依赖的环境:
(安装方法度娘或者谷哥)
python 2
java
fswatch
qrencode
使用方法:
下载项目 导入AS后,找到工具类HttpUtil,并编译运行,确保手机和编辑器处于同一网络环境中。
public static String getHostIp() { //此处返回本机ip地址 return "192.168.4.109"; }
刷新原理简介:
app启动后会主动链接IP:7788端口,点击刷新会去主动拉取对应端口数据,并对数据进行解析,加载到页面上。
刷新部分代码如下
private void refreshByUrl(final String url) {
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
try {
Response response = client.newCall(request).execute();
if (response != null && response.isSuccessful()) {
if (response.body() != null) {
String string = response.body().string();
final PreviewData previewData = new Gson().fromJson(string, PreviewData.class);
if (previewData != null) {
loadTemplates(previewData.templates);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
JsonObject json = previewData.data;
if (json != null) {
try {
mJsonData = JSON.parseObject(json.toString());
preview(mTemplateName, mJsonData);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (JsonSyntaxException e) {
e.printStackTrace();
}
}
}).start();
}
预览和编译组件工具使用
项目地址:
https://github.com/alibaba/virtualview_tools/
项目下主要包含两个包目录TemplateWorkSpace和RealtimePreview
RealtimePreview
templates
data.json 真实预览数据,用于测试下发数据后的样式。
模板.json 如 Earth.json 预览的数据
模板.xml Earth.xml 模板的xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../vv.xsd"
orientation="V" layoutWidth="match_parent"
layoutHeight="wrap_content" background="#11000000">
模板_QR.png 例如 Earth_QR.png, 作用扫描预览效果
run.sh
执行用的脚本
使用方法:在Terminal下, cd 进入RealtimePreview 包下, sh run.sh 启动
注意事项:
- data.json 名称不可更改 ,否则在预览界面会解析报错
- 模板.json 和模板.xml 需要名称对应否则会加载不到。
- xml中的 下面标签不可删除,否则会报错
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../vv.xsd"
- 运行过程中注意log,可能或有异常,会有对应提示。
TemplateWorkSpace
template
编译的模板文件列表
注意事项
- 在编译的时候 下面标签要删除
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../vv.xsd"
- xml首字母需要小写
build
二进制文件的输出目录
- out文件夹
例如 模板对应.out文件
- sigin文件夹
输出模板对应的MD5
- java 文件夹
输出对应文件.bin文件 为byte数组,加载代码和代码如下
int result = vafContext.getViewManager().loadBinBufferSync(decode);
public class EARTH{
public static final byte[] BIN = new byte[] {
65, 76, 73, 86, 86, 0, 1, 0, 0, 0, 1, 0, 0, 0, 47, 0, 0, 0, -60, 0, 0, 0, -13, 0, 0, 0, 29, 0, 0, 1, 20, 0, 0, 0, 0, 0, 0, 1, 24, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 5, 69, 97, 114, 116, 104, 0, -73, 0, 0, 2, 3, -86, 50, -11, -48, 0, 0, 0, 0, 92, -43, -16, -15, -1, -1, -1, -2, 119, 112, -84, -68, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 92, -43, -16, -15, 0, 0, 1, 109, 119, 112, -84, -68, 0, 0, 1, -14, 0, 0, 0, 0, 0, 0, 0, 0, 9, 2, 92, -43, -16, -15, -1, -1, -1, -1, 119, 112, -84, -68, 0, 0, 1, -14, 0, 0, 0, 1, 0, 1, -67, -28, 68, 97, -58, -52, 0, 0, 1, 0, 0, 1, 3, 92, -43, -16, -15, -1, -1, -1, -1, -80, -104, 85, 46, -1, 0, 0, 0, 119, 112, -84, -68, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 7, 4, -60, 45, 58, -50, 0, 0, 0, 24, 92, -43, -16, -15, 0, 0, 0, -56, 119, 112, -84, -68, -1, -1, -1, -1, -64, -101, 46, 54, -1, 25, -126, 92, 0, 0, 0, 1, 0, 54, 69, 45, -33, 33, 89, 28, 0, 0, 1, 1, 1, 1, 0, 0, 0, 2, 68, 97, -58, -52, 0, 6, 36, 123, 98, 97, 103, 125, -33, 33, 89, 28, 0, 11, 36, 123, 115, 104, 111, 119, 116, 101, 120, 116, 125, 0, 0, 0, 0,
};
}
templatelist.properties
这里主要定义模板的编译完成后的名称
样式为:xmlFileName=outFileName,Version[,platform]
如 earth=Earth,1
注意事项:
标识 template 目录下需要编译的 xml 文件名建议不带 .xml 后缀,目前做了兼容(我个人测试带也没事)
outFileName 输出到 build 目录下的 .out 文件名
Version 表示 xml 编译后的版本号
platform 同时兼容 iOS 和 android 时不写,可填的值为 android 和iphone
config.properties
配置组件 ID、xml 属性对应的 value 类型
VIEW_ID_名称=id
注意事项
自定义组件属性 属性名=类型(不写默认为String)
这里的id,端内注册的时候要对应上,比如这里写注册id为2002,端内注册的时候也要注册为2002
viewManager.getViewFactory().registerBuilder(2002, new MhtBaseTextView.Builder());
compiler.jar
java 代码编译后 jar 文件,执行 xml 的编译逻辑(端内用不到)
buildTemplate.sh
编译组件使用到的sh文件
使用方法
- cd 到TemplateWorkSpace/template 文件目录
-
sh buildTemplate.sh
端内使用
依赖
//这里使用官方最新版本
compile ('com.alibaba.android:virtualview:1.0.5@aar') {
transitive = true
}
初始化
VafContext vafContext = new VafContext(WboApplication.this);
ViewManager viewManager = vafContext.getViewManager();
viewManager.init(WboApplication.this);
屏幕适配
//这里代码的意思是设置基本单元 如不设置会自动将屏幕视为750
com.libra.Utils.setUedScreenWidth(VLDensityUtils.getScreenWidth());
自定义组件
利用提供的原始控件组合(纯vv原生控件)
优点:可直接下发,无需端内预先注册,动态能力max
如需要动态配置对应的数据,可以使用${xxx},json中下发即可
{
"open2": "https://xossimg.2cq.com/system/img/banner/be2cf7f26787b33d443110e8aa03dd14.png",
"open1": "https://xossimg.2cq.com/system/img/banner/be2cf7f26787b33d443110e8aa03dd14.png",
"showContent": "321321321312312"
}
利用原生组件封装成特定模板
如下面xml
使用上面的编译工具,将该模板编译为一个特定组件,这里注意在xml中删除上面提到的两个标签,不然会报出异常,需要在templatelist中编写对应id。规则看上文。
编译运行,会输出对应的.out 和.bin文件
端内加载:这里动态下发 建议使用.bin文件,因为bin文件中就是byte数组,在json传输中,使用base64转为字符串,在收到后再转回byte数组
public class EARTH{
public static final byte[] BIN = new byte[] {
65, 76, 73, 86, 86, 0, 1, 0, 0, 0, 1, 0, 0, 0, 47, 0, 0, 0, -60, 0, 0, 0, -13, 0, 0, 0, 29, 0, 0, 1, 20, 0, 0, 0, 0, 0, 0, 1, 24, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 5, 69, 97, 114, 116, 104, 0, -73, 0, 0, 2, 3, -86, 50, -11, -48, 0, 0, 0, 0, 92, -43, -16, -15, -1, -1, -1, -2, 119, 112, -84, -68, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 92, -43, -16, -15, 0, 0, 1, 109, 119, 112, -84, -68, 0, 0, 1, -14, 0, 0, 0, 0, 0, 0, 0, 0, 9, 2, 92, -43, -16, -15, -1, -1, -1, -1, 119, 112, -84, -68, 0, 0, 1, -14, 0, 0, 0, 1, 0, 1, -67, -28, 68, 97, -58, -52, 0, 0, 1, 0, 0, 1, 3, 92, -43, -16, -15, -1, -1, -1, -1, -80, -104, 85, 46, -1, 0, 0, 0, 119, 112, -84, -68, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 7, 4, -60, 45, 58, -50, 0, 0, 0, 24, 92, -43, -16, -15, 0, 0, 0, -56, 119, 112, -84, -68, -1, -1, -1, -1, -64, -101, 46, 54, -1, 25, -126, 92, 0, 0, 0, 1, 0, 54, 69, 45, -33, 33, 89, 28, 0, 0, 1, 1, 1, 1, 0, 0, 0, 2, 68, 97, -58, -52, 0, 6, 36, 123, 98, 97, 103, 125, -33, 33, 89, 28, 0, 11, 36, 123, 115, 104, 111, 119, 116, 101, 120, 116, 125, 0, 0, 0, 0,
};
}
//获取下发的bin文件的字符转
String bin = VLJsonParseUtils.getString(message, "bin");
//转回原始的byte数组
byte[] decode = Base64.decode(bin, Base64.DEFAULT);
//加载该数组
int result = vafContext.getViewManager().loadBinBufferSync(decode);
try {
//msgType 在生成的时候定义为什么,就要加载什么,如上文我写成了earth 那这里要为earth
View vvView = WboApplication.getVafContext().getContainerService().getContainer(msgType, false);
JSONObject jsonObject = new JSONObject(message);
IContainer container1 = (IContainer) vvView;
//设置数据
container1.getVirtualView().setVData(jsonObject);
VvRoomDialog vvRoomDialog = new VvRoomDialog(room.getContext());
vvRoomDialog.addShowView(vvView);
vvRoomDialog.show();
} catch (Exception e) {
}
如该为固定模板,可以端内预加载加载,直接下发,无需在使用getVafContext().getContainerService().getContainer(名称, false)加载
viewManager.getViewFactory().registerBuilder(2002, new MhtBaseTextView.Builder());
端内自定义模板
适用于使用原生组件拼装困难,需要需要提前在端内注册,下面为端内代码
实体view(实现功能的view ,需要实现IView)
public class MhtTestViewImp extends TextView implements IView {
public MhtTestViewImp(Context context) {
super(context);
}
public MhtTestViewImp(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MhtTestViewImp(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.setTextSize(40);
}
@Override
public void measureComponent(int widthMeasureSpec, int heightMeasureSpec) {
this.measure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void comLayout(int l, int t, int r, int b) {
this.layout(l, t, r, b);
}
@Override
public void onComMeasure(int widthMeasureSpec, int heightMeasureSpec) {
this.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void onComLayout(boolean changed, int l, int t, int r, int b) {
this.onLayout(changed, l, t, r, b);
}
@Override
public int getComMeasuredWidth() {
return this.getMeasuredWidth();
}
@Override
public int getComMeasuredHeight() {
return this.getMeasuredHeight();
}
}
包装view 需要继承ViewBase
public class MhtBaseTextView extends ViewBase {
private MhtTestViewImp testViewImp;
private static final String TAG = "MhtBaseTextView";
private final int textContentId;
public MhtBaseTextView(VafContext context, ViewCache viewCache) {
super(context, viewCache);
testViewImp = new MhtTestViewImp(context.forViewConstruction());
StringSupport mStringSupport = context.getStringLoader();
//这里代码意思是找到系统提供的StringSupport,去查找自定义的属性text的key
textContentId = mStringSupport.getStringId("text", false);
}
@Override
public void onComMeasure(int widthMeasureSpec, int heightMeasureSpec) {
testViewImp.onComMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public int getComMeasuredWidth() {
return testViewImp.getComMeasuredWidth();
}
@Override
public int getComMeasuredHeight() {
return testViewImp.getComMeasuredHeight();
}
@Override
public void onComLayout(boolean changed, int l, int t, int r, int b) {
//这里一定要写不然控件出不来
testViewImp.comLayout(l, t, r, b);
}
@Override
public void comLayout(int l, int t, int r, int b) {
super.comLayout(l, t, r, b);
testViewImp.comLayout(l, t, r, b);
}
@Override
public void onParseValueFinished() {
super.onParseValueFinished();
//所有属性加载结束 这里可以进行处理
//如 if(xxx){xxx.setxxx} 一般属性不为表达式的,这里要进行处理,如果为表达式则不用
}
@Override
protected boolean setAttribute(int key, String stringValue) {
boolean result = true;
//系统调用设置属性 如果key和预定的复合进行一些操作
if (key == textContentId) {
//这里的判断是当前值是否是一个表达式如${xx}
if (Utils.isEL(stringValue)) {
//将值存放到缓存中
mViewCache.put(this, key, stringValue, ViewCache.Item.TYPE_STRING);
} else {
//如果不是则可以直接取出 String content=stringValue;
}
} else {
result = super.setAttribute(key, stringValue);
}
//返回true证明消费掉这个值,反之回继续下传
return result;
}
@Override
public View getNativeView() {
//这个方法虽然对外没用过,但是也要写上
return testViewImp;
}
@Override
public void reset() {
super.reset();
}
//对外提供的构造器
public static class Builder implements ViewBase.IBuilder {
@Override
public ViewBase build(VafContext context, ViewCache viewCache) {
return new MhtBaseTextView(context, viewCache);
}
}
@Override
protected boolean setAttribute(int key, Object value) {
return super.setAttribute(key, value);
}
}
编写基础模板 ,这里使用动态预览会报错,报MhtBaseTextView不存在
使用编译工具
template下放置xml
//对应点击事件EventManager.xxx
vafContext.getEventManager().register(EventManager.TYPE_Click, new IEventProcessor() {
@Override
public boolean process(EventData data) {
//这里可以截取到对应的action进行事件
String action = data.mVB.getAction();
return true;
}
});
在config.properties中配置id,这里的id用于在端内注册使用
VIEW_ID_名称=id
VIEW_ID_MhtBaseTextView=2002
![image.png](https://upload-images.jianshu.io/upload_images/2657154-62e9971e0a3e6c2d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
我们在控件中使用了一个text的自定义属性,需要在config.properties中进行配置,如果是String类型,则可以不用配置,默认为String类型。
![image.png](https://upload-images.jianshu.io/upload_images/2657154-ebf17f571a39b7a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
一般我们也会为该模板生成一个名字 ,方便动态下发
![image.png](https://upload-images.jianshu.io/upload_images/2657154-8a390c21dbd60cde.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
端内需要注册:这样下发的时候才能找到对应组件
viewManager.getViewFactory().registerBuilder(1001, new MhtBaseTextView.Builder());
vv常用功能使用
- 基本控件
地址:http://tangram.pingguohe.net/docs/virtualview/about-virtualview
- 点击事件
flag: flag_software:关闭view的硬件加速,flag_exposure:需要触发曝光事件,flag_clickable:需要响应点击事件,flag_longclickable:需要响应长按事件,flag_touchable:需要响应触摸事件
//对应点击事件EventManager.xxx
vafContext.getEventManager().register(EventManager.TYPE_Click, new IEventProcessor() {
@Override
public boolean process(EventData data) {
//这里可以截取到对应的action进行事件
String action = data.mVB.getAction();
return true;
}
});
- 字体样式(特殊字体样式)
Ntext 控件 supportHTMLStyle 属性 true支持,false 默认为普通文本,IOS不支持(需要端内实现)
- 图片加载
vafContext.setImageLoaderAdapter(new ImageLoader.IImageLoaderAdapter() {
@Override
public void bindImage(String uri, ImageBase imageBase, int reqWidth, int reqHeight) {
BitmapRequestBuilder requestBuilder =
Glide.with(WboApplication.this).load(uri).asBitmap();
ImageTarget imageTarget = new ImageTarget(imageBase);
requestBuilder.into(imageTarget);
}
@Override
public void getBitmap(String uri, int reqWidth, int reqHeight, ImageLoader.Listener lis) {
BitmapRequestBuilder requestBuilder =
Glide.with(WboApplication.this
).load(uri).asBitmap();
ImageTarget imageTarget = new ImageTarget(lis);
requestBuilder.into(imageTarget);
}
});
- 获取view和设置数据
//根据对应组件类型取出对应view
View vvView = WboApplication.getVafContext().getContainerService().getContainer(msgType, false);
//下发下来的json 里面包含对应的数据
JSONObject jsonObject = new JSONObject(message);
//转为IContainer
IContainer container1 = (IContainer) vvView;
//设置数据
container1.getVirtualView().setVData(jsonObject);
//将view加载到我们指定布局或者控件
VvRoomDialog vvRoomDialog = new VvRoomDialog(room.getContext());
vvRoomDialog.addShowView(vvView);
vvRoomDialog.show();