VirtualView实现动态下发

直播公司,活动做的比较多,端内业务主要是做几个特殊的定制化弹窗,但是每次都需要发版本,公司内部统计,让一个用户升级的成本其实很高的。一直寻求动态化的解决方案,无非自己端内写组件,或者寻求外部方案。但是自己研发的成本很高。之前查过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"; }

image.png

刷新原理简介:

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 启动


image.png
注意事项:
  • 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文件


image.png
  • sigin文件夹

输出模板对应的MD5


image.png
  • 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


image.png
config.properties

配置组件 ID、xml 属性对应的 value 类型

VIEW_ID_名称=id

注意事项

自定义组件属性 属性名=类型(不写默认为String)

这里的id,端内注册的时候要对应上,比如这里写注册id为2002,端内注册的时候也要注册为2002


image.png
viewManager.getViewFactory().registerBuilder(2002, new MhtBaseTextView.Builder());
compiler.jar

java 代码编译后 jar 文件,执行 xml 的编译逻辑(端内用不到)

buildTemplate.sh

编译组件使用到的sh文件

使用方法

  • cd 到TemplateWorkSpace/template 文件目录
  • sh buildTemplate.sh


    image.png

端内使用

依赖
//这里使用官方最新版本
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。规则看上文。


image.png

image.png

编译运行,会输出对应的.out 和.bin文件


image.png

image.png

端内加载:这里动态下发 建议使用.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();

你可能感兴趣的:(VirtualView实现动态下发)