原文链接: 1、Android | Tangram动态页面之路(一)需求背景 2、Android | Tangram动态页面之路(二)介绍 3、Android | Tangram动态页面之路(三)使用 4、Android | Tangram动态页面之路(四)vlayout原理 5、Android | Tangram动态页面之路(五)Tangram原理 6、Android | Tangram动态页面之路(六)数据分离 7、Android | Tangram动态页面之路(七)硬核的Virtualview
本系列文章主要介绍天猫团队开源的Tangram 框架的使用心得和原理,由于Tangram
底层基于vlayout ,所以也会简单讲解,该系列将按以下大纲进行介绍:
(一)需求背景
首先,笔者在工作中(生产环境)使用的并非Tangram,而是公司内部的框架(未开源),不过原理都大同小异,本系列文章也不会精细到每一行源码,不求齐全,只求用尽可能小的篇幅讲明白。
页面定投 大家都知道,电商行业喜欢造节,从双11双12,到现在的321、618、921等,几乎一年四季都会有营销活动,这些活动能带动GMV持续造血。一场大促,通常会分预热期和正式期。预热期用来造势,着重透出主会场、活动等内容;正式期则在接近尾声时,着重透出倒计时内容增强紧迫感(再不剁手就没机会了),如下图:
可以看出,从预热期到正式期,着重透出的内容不同,结构也不同。也就是说,需要足够灵活的页面模板,满足不同时间,不同人群(如多人多面)展示不同结构的页面
。当然这点h5也能做到,但是h5体验要差于native,一般用于临时活动页或高度灵活的页面。像商城首页,商品详情等相对稳定的页面,对灵活性的要求并非无限制的,只要各模块足够抽象、细粒度,native也是可以满足日常和大促需求的。
数据聚合 如前边的商城首页,数据来源也是多渠道的,比如头部的banner、各类入口、类目模块,数据来源于一个个不同的资源位,由不同的人进行配置,而尾部的商品流数据则来源于推荐引擎,如下图,
客户端不可能去发起多个请求拉取不同资源位的数据,所以就有了聚合层
接口,客户端通过一个聚合接口
,获取聚合数据,
商品流不属于资源位数据,所以独立成一个接口。至此,我们会发现,页面模板不仅要描述页面结构,还要描述各模块所需的数据来源
。大概如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "template" : [ { "component" : "banner" , "data" : "makeup:banner" } , { "component" : "category" , "data" : "makeup:category" } , { "component" : "goods" , "data" : "request:recommend" } ] }
RecyclerView的异构和扁平 要实现前边商城首页的复杂布局,通常的思路是定义各种itemType
,然后根据itemType
来解析不同的布局文件,这么做往往需要进行一层嵌套,因为RecyclerView
自带的LayoutManager
不支持如此异构的布局,效果如下图左,
而使用vlayout
,可以免去这层嵌套,如上图右,让子view成为RecyclerView
的直接子view,具体我们运行vlayout
的官方Demo,然后使用AS的Tools - Layout inspector
功能查看view树,可见图中的39和43两个小模块都是RecyclerView
的直接子view,有着平级关系,
这就是用RecyclerView
实现异构和扁平化的基本概念,vlayout
通过自定义LayoutManager
进行了实现。关于异构和扁平,更详细的分析可见参考文章。
参考文章
苹果核 - RecyclerView 里的自定义 LayoutManager 的一种设计与实现
(二)介绍 vlayout 因为Tangram
底层基于vlayout
,所以需要先了解下vlayout
。
首先,在view上的性能消耗通常有以下几种:
可以使用ConstraintLayout
或RelativeLayout
减少布局嵌套
列表使用RecyclerView
来复用布局
如果列表的样式不多,使用RecyclerView
的复用机制可以避免大量的xml解析;如果样式比较多比如商品图墙等,则有必要把xml解析提前到编译期,在编译期根据注解将xml转成对应的view类,直接使用view类创建viewHolder,当然这么做会势必会增大包体积,需要克制使用
然后,vlayout
主要解决前两点,做到复杂布局下扁平和细粒度复用。
常规的RecyclerView
使用:
1 2 3 4 5 recyclerView.setAdapter () recyclerView.setLayoutManager ()
其中LayoutManager
有3种,
1 2 3 4 LinearLayoutManager extends LayoutManager GridLayoutManager extends LinearLayoutManager StaggeredGridLayoutManager extends LayoutManager
在面对比较复杂的布局时,如1拖3样式,
通常只能在1拖3外边套上一层layout,然后使用LinearLayoutManager
实现。为了解决这个问题,
vlayout自定义了一个VirtualLayoutManager,它继承自 LinearLayoutManager;引入了 LayoutHelper 的概念,它负责具体的布局逻辑;VirtualLayoutManager管理了一系列LayoutHelper,将具体的布局能力交给LayoutHelper来完成,每一种LayoutHelper提供一种布局方式,框架内置提供了几种常用的布局类型,包括:网格布局、线性布局、瀑布流布局、悬浮布局、吸边布局等。这样实现了混合布局的能力,并且支持扩展外部,注册新的LayoutHelper,实现特殊的布局方式。
引用自苹果核 - Tangram 的基础 —— vlayout(Android)
大致意思是这样,
在vlayout
Demo中,使用代码是这样,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 List<DelegateAdapter.Adapter> adapters = new LinkedList <>(); SubAdapter subAdapter1 = new SubAdapter (new LinearLayoutHelper ());adapters.add(subAdapter1); SubAdapter subAdapter2 = new SubAdapter (new ColumnLayoutHelper ());adapters.add(subAdapter2); delegateAdapter.setAdapters(adapters); recyclerView.setAdapter(delegateAdapter);
可以看到,随着布局样式越来越多,代码量也会越来越多,于是,用json模板描述页面
的Tangram
诞生了。
Tangram 把vlayout
直接给到业务方使用,这样的接入成本是不能接受的,于是需要屏蔽掉vlayout
细节,让业务方用的更舒服。至于为何要引入json模板
,需求背景 里已经做过介绍。
Tangram
的意思是七巧板,旨在用七巧板的方式拼凑出各式各样的页面。他抽象了两个概念,Card
和Cell
,Card
用于描述布局方式,Cell
用于描述在这个布局方式下,用什么样的view去展示,比如Tangram
Demo里的data.json
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [ { "type" : "container-oneColumn" , "items" : [ { "imgUrl" : "https://gw.alicdn.com/tfs/TB1vqF.PpXXXXaRaXXXXXXXXXXX-110-72.png" , "arrowImgUrl" : "https://gw.alicdn.com/tfs/TB1vqF.PpXXXXaRaXXXXXXXXXXX-110-72.png" , "title" : "标题1" , "type" : "vvtest" } , { "imgUrl" : "https://gw.alicdn.com/tfs/TB1vqF.PpXXXXaRaXXXXXXXXXXX-110-72.png" , "arrowImgUrl" : "https://gw.alicdn.com/tfs/TB1vqF.PpXXXXaRaXXXXXXXXXXX-110-72.png" , "title" : "标题2" , "type" : "vvtest" } ] } ]
这些布局方式Card
,在Tangram
内部会进行注册,
1 2 3 4 5 6 7 8 void installDefaultRegistry (final DefaultResolverRegistry registry) { registry.registerCard (TYPE_CONTAINER_BANNER, BannerCard.class ); registry.registerCard (TYPE_SINGLE_COLUMN_COMPACT, SingleColumnCard.class ); }
布局方式确定好后,需要具体的View来展示,也就是Cell
,比如单图SingleImageView
,纯文本RatioTextView
等等,这些Cell
则需手动注册,如果是偏业务的Cell
,可以在业务层按需注册,如果是更抽象的通用Cell
,则应该下沉到基础库里全局注册,更抽象的Cell
意味着需要提供更为通用的配置属性,能提供给更多不同的业务方使用。Cell
的手动注册如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void onCreate (Bundle savedInstanceState ) { builder.registerCell (1 , TestView .class ); builder.registerCell (2 , SimpleImgView .class ); engine.bindView (recyclerView); JSON Array data = new JSON Array (new String (getAssertsFile (this , "data.json" ))); engine.setData (data); }
Card
和Cell
都注册好了,通过TangramEngine
将数据设置进去,跟进去可以看到,Tangram
把布局能力交给了vlayout
,
1 2 3 4 5 6 void setData (List<L> cards, boolean silence) { setLayoutHelpers (transformCards(cards, mData, mCards)); }
Tangram
把json模板中描述的一个个Card
解析成了所对应的vlayout
的布局方式LayoutHelper
,
值得注意的是,Tangram
Demo里的json模板都是包含了业务数据的,这么做可能是为了剔除掉网络请求的代码,方便开源学习,而在实际业务中不太可能把数据绑定在模板里,这样模板会很臃肿,我们要做的是,用模板描述页面结构和数据源
,而非数据本身,如,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "template" : [ { "card" : "container-banner" , "cell" : "imageView" , "data" : "makeup:banner" } , { "card" : "container-fiveColumn" , "cell" : "imageAndTextView" , "data" : "makeup:category" } , { "card" : "container-waterfall" , "cell" : "goodsView" , "data" : "request:recommend" } ] }
既然json模板可以由后端下发,也就意味着,我们可以让运营同学通过后台拖动模块搭建页面,选择资源位设置数据源,然后生成json模板下发,开发同学从此就可以解放双手,做更有趣(更有挑战)的事情了。
不过这都是后话了,对Tangram
和vlayout
的介绍就到这里了,下篇文章将对Tangram的使用进行更详细的讲解,点关注,不迷路~
参考文章
苹果核 - Tangram 的基础 —— vlayout(Android)
苹果核 - 页面动态化的基础 —— Tangram
(三)使用 Demo代码
基础使用 引入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 implementation 'com.alibaba.android:tangram:2.2.5@aar' implementation 'com.alibaba.android:vlayout:1.2.36@aar' implementation ('com.alibaba.android:virtualview:1.4.6@aar' ) { transitive true } implementation 'com.alibaba.android:ultraviewpager:1.0.7.8@aar' implementation 'io.reactivex.rxjava2:rxjava:2.1.12' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
初始化,主要是传递上下文进去,和设置图片加载能力,这里我们使用Glide
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MyApp extends Application { @Override public void onCreate() { super .onCreate(); TangramBuilder .init(this , new IInnerImageSetter () { @Override public <IMAGE extends ImageView > void doLoadImageUrl(@NonNull IMAGE view, @Nullable String url) { Glide .with (view.getContext()).load(url). error(R .mipmap.ic_launcher). into(view); } }, NetImageView .class ); } }
在activity中使用,
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 void onCreate (Bundle savedInstanceState ) { TangramBuilder .InnerBuilder builder = TangramBuilder .newInnerBuilder (this ); builder.registerCell (ImageTextView .class .getSimpleName (), ImageTextView .class ); builder.registerCell (SingleImageView .class .getSimpleName (), SingleImageView .class ); mEngine = builder.build (); mEngine.bindView (mBinding.rvList ); mBinding.rvList .addOnScrollListener (new RecyclerView .OnScrollListener () { @Override public void onScrolled (RecyclerView recyclerView, int dx, int dy ) { super .onScrolled (recyclerView, dx, dy); mEngine.onScrolled (); } }); String file = FileUtil .getAssertsFile (this , "main.json" ); try { mEngine.setData (new JSON Array (file)); } catch (JSON Exception e) { e.printStackTrace (); } }
前边的介绍篇提到过,Tangram
内置了一些布局方式Card
,基本满足需求了,所以我们只需定制自己的具体View也就是Cell
即可,上边手动注册了两个Cell
,ImageTextView
和SingleImageView
,我们先来看ImageTextView
,他是一个LinearLayout
,上边一个图标,下边一个文本,需要实现ITangramViewLifeCycle
接口,在相应的回调里执行自己的逻辑,
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 public class ImageTextView extends LinearLayout implements ITangramViewLifeCycle { private NetImageView mImgIcon; private TextView mTvTitle; public ImageTextView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); setOrientation(VERTICAL); setGravity(Gravity.CENTER); inflate(getContext(), R.layout.cell_image_text, this ); mImgIcon = findViewById(R.id.img_icon); mTvTitle = findViewById(R.id.tv_title); } @Override public void cellInited (BaseCell cell) { } @Override public void postBindView (BaseCell cell) { mImgIcon.load(cell.optStringParam("imgUrl" )); mTvTitle.setText(cell.optStringParam("title" )); } @Override public void postUnBindView (BaseCell cell) { } }
其布局文件cell_image_text.xml
如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <merge xmlns:android ="http://schemas.android.com/apk/res/android" xmlns:tools ="http://schemas.android.com/tools" android:layout_width ="wrap_content" android:layout_height ="wrap_content" > <com.holiday.tangram.view.NetImageView android:id ="@+id/img_icon" android:layout_width ="50dp" android:layout_height ="50dp" android:scaleType ="centerCrop" tools:background ="@color/colorAccent" /> <TextView android:id ="@+id/tv_title" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_marginTop ="4dp" android:ellipsize ="end" android:maxLines ="1" tools:text ="@string/app_name" /> </merge >
Cell
都准备好了,下面就是准备json模板
了,先来一个四列布局container-fourColumn
,
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 { "type" : "container-fourColumn" , "items" : [ { "imgUrl" : "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg" , "title" : "标题1" , "type" : "ImageTextView" } , { "imgUrl" : "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg" , "title" : "标题2" , "type" : "ImageTextView" } , { "imgUrl" : "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg" , "title" : "标题3" , "type" : "ImageTextView" } , { "imgUrl" : "https://tva1.sinaimg.cn/large/007S8ZIlgy1geqp9zhftrj303r03r3yl.jpg" , "title" : "标题4" , "type" : "ImageTextView" } ] }
运行如下,
下面再来看另一个自定义Cell
,SingleImageView
很简单,就是单图,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class SingleImageView extends NetImageView implements ITangramViewLifeCycle { public SingleImageView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); setScaleType(ScaleType.CENTER_CROP); } @Override public void cellInited (BaseCell cell) { } @Override public void postBindView (BaseCell cell) { load(cell.optStringParam("imgUrl" )); } @Override public void postUnBindView (BaseCell cell) { } }
然后来一个1拖n布局container-onePlusN
,
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 { "type" : "container-onePlusN" , "style" : { "aspectRatio" : "1.778" , "margin" : "[10,0,0,0]" } , "items" : [ { "imgUrl" : "https://wanandroid.com/blogimgs/942a5c62-ca87-4e7c-a93d-41ff59a15ba4.png" , "type" : "SingleImageView" } , { "imgUrl" : "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png" , "type" : "SingleImageView" } , { "imgUrl" : "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png" , "type" : "SingleImageView" } , { "imgUrl" : "https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png" , "type" : "SingleImageView" } ] }
图片取自玩安卓 的banner图,运行如下,
因为有4条数据,所以展示的效果就是1拖3。
然后再来个轮播图布局container-banner
,他的Cell
还是使用单图SingleImageView
,
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 { "type": "container-banner" , "style" : { "margin ": "[0,0,10,0]" , "pageWidth" : 200 , "pageHeight" : 100 , "indicatorMargin" : "5" , "infinite" : "true" , "indicatorImg2" : "https://img.alicdn.com/tps/TB1XRNFNXXXXXXKXXXXXXXXXXXX-32-4.png" , "indicatorImg1" : "https://img.alicdn.com/tps/TB16i4qNXXXXXbBXFXXXXXXXXXX-32-4.png" , "scrollMarginLeft" : "10" , "indicatorGap" : "2" , "indicatorHeight" : "1.5" , "itemRatio" : "2.654" , "scrollMarginRight" : "10" , "indicatorGravity" : "center" , "hGap" : "20" }, "items": [ { "imgUrl": "https://wanandroid.com/blogimgs/942a5c62-ca87-4e7c-a93d-41ff59a15ba4.png" , "type" : "SingleImageView" }, { "imgUrl": "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png" , "type" : "SingleImageView" }, { "imgUrl": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png" , "type" : "SingleImageView" }, { "imgUrl": "https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png" , "type" : "SingleImageView" } ] }
可以看出可供配置的参数还是很多的,文档可以看这里 ,运行如下,
整体效果如下,
内置support支持 Tangram
内置了一些support
支持,如处理点击SimpleClickSupport
,卡片数据加载CardLoadSupport
,曝光逻辑ExposureSupport
等,可以通过TangramEngine
的add
方法注册,如下,
1 2 3 mEngine.addSimpleClickSupport (new MyClickSupport ());
然后看MyClickSupport
,
1 2 3 4 5 6 7 8 9 10 11 12 public class MyClickSupport extends SimpleClickSupport { public MyClickSupport () { setOptimizedMode(true ); } @Override public void defaultClick(View targetView, BaseCell cell, int eventType) { super .defaultClick(targetView, cell, eventType); QrToast .show(cell.stringType); } }
我们可以在自己的Cell
里对view设置点击事件,也可以把点击事件交给support
全局处理,如果要使用support
处理点击事件,需要在Cell
里加这行代码,
1 2 3 4 5 6 @Override public void cellInited (BaseCell cell ) { setOnClickListener (cell); }
比如商城首页,大多数模块都是点击进行页面跳转,行为比较单一,不需要每个Cell
都去做点击事件,SimpleClickSupport
就能很好的支持,在defaultClick
里取出页面短链进行跳转即可,如,
1 2 3 4 5 6 7 8 9 10 11 12 13 public class MyClickSupport extends SimpleClickSupport { public MyClickSupport () { setOptimizedMode(true ); } @Override public void defaultClick(View targetView, BaseCell cell, int eventType) { super .defaultClick(targetView, cell, eventType); PageRouter .to(cell.optStringParam("link" )); } }
用起来很简单有木有!好啦,Tangram
的使用就介绍到这里了,后面会分析原理,实现模板和数据分离等等,敬请期待~
参考文章
GitHub-Tangram使用文档
Tangram官网
(四)vlayout原理 基于vlayout最新源码
在Tangram和vlayout介绍 这篇文章提到过,
vlayout自定义了一个VirtualLayoutManager,它继承自 LinearLayoutManager;引入了 LayoutHelper 的概念,它负责具体的布局逻辑;VirtualLayoutManager管理了一系列LayoutHelper,将具体的布局能力交给LayoutHelper来完成,每一种LayoutHelper提供一种布局方式,框架内置提供了几种常用的布局类型,包括:网格布局、线性布局、瀑布流布局、悬浮布局、吸边布局等。这样实现了混合布局的能力,并且支持扩展外部,注册新的LayoutHelper,实现特殊的布局方式。
引用自苹果核 - Tangram 的基础 —— vlayout(Android)
大致意思是这样,
在VLayoutActivity
中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void onCreate (Bundle savedInstanceState) { if (FLOAT_LAYOUT) { FloatLayoutHelper layoutHelper = new FloatLayoutHelper (); layoutHelper.setAlignType (FixLayoutHelper.BOTTOM_RIGHT); layoutHelper.setDefaultLocation (100 , 400 ); LayoutParams layoutParams = new LayoutParams (150 , 150 ); adapters.add (new SubAdapter(this, layoutHelper, 1 , layoutParams)); } }
来到子适配器SubAdapter
,
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 class SubAdapter extends DelegateAdapter.Adapter <MainViewHolder > { private LayoutHelper mLayoutHelper; public SubAdapter (Context context, LayoutHelper layoutHelper, int count, LayoutParams layoutParams ) { this .mContext = context; this .mLayoutHelper = layoutHelper; this .mCount = count; this .mLayoutParams = layoutParams; } @Override public LayoutHelper onCreateLayoutHelper () { return mLayoutHelper; } public MainViewHolder onCreateViewHolder (ViewGroup parent, int viewType ) { return new MainViewHolder(LayoutInflater.from (mContext).inflate(R.layout.item, parent, false )); } protected void onBindViewHolderWithOffset (MainViewHolder holder, int position, int offsetTotal ) { ((TextView) holder.itemView.findViewById(R.id.title)).setText(Integer.toString(offsetTotal)); } }
在delegateAdapter.setAdapters(adapters)
时,取出适配器指定的布局方式,进行透传,
1 2 3 4 5 6 7 8 9 10 public void setAdapters (List<Adapter> adapters) { List<LayoutHelper> helpers = new LinkedList <>(); for (Adapter adapter : adapters) { LayoutHelper helper = adapter.onCreateLayoutHelper(); helpers.add(helper); } super .setLayoutHelpers(helpers); }
来到VirtualLayoutManager
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void setLayoutHelpers (@Nullable List<LayoutHelper> helpers) { if (helpers != null) { int start = 0 ; Iterator<LayoutHelper> it1 = helpers.iterator (); while (it1.hasNext()) { LayoutHelper helper = it1.next (); if (helper.getItemCount() > 0 ) { helper.setRange (start, start + helper.getItemCount() - 1 ); } else { helper.setRange (-1 , -1 ); } start += helper.getItemCount (); } } this.mHelperFinder .setLayouts (helpers); requestLayout (); }
LayoutHelper
被赋值好后,进行布局,这里暂不深究View
的测量布局绘制流程,来到VirtualLayoutManager.onLayoutChildren
,
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 void onLayoutChildren (RecyclerView.Recycler recycler, RecyclerView.State state) { runPreLayout (recycler, state); super.onLayoutChildren (recycler, state); } void onLayoutChildren (RecyclerView.Recycler recycler, RecyclerView.State state) { fill (recycler, mLayoutState, state, false); } int fill (RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { layoutChunk (recycler, state, layoutState, layoutChunkResultCache); } void layoutChunk (RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, com.alibaba.android.vlayout.layout.LayoutChunkResult result) { final int position = layoutState.mCurrentPosition ; LayoutHelper layoutHelper = mHelperFinder == null ? null : mHelperFinder.getLayoutHelper (position); layoutHelper.doLayout (recycler, state, mTempLayoutStateWrapper, result, this); } void doLayout (RecyclerView.Recycler recycler, RecyclerView.State state, LayoutStateWrapper layoutState, LayoutChunkResult result, LayoutManagerHelper helper) { layoutViews (recycler, state, layoutState, result, helper); }
具体的测量和布局的实现layoutViews
,我们举两个比较典型的布局方式分析,ColumnLayoutHelper
和FloatLayoutHelper
。
举例ColumnLayoutHelper列布局 设置比重,第一列和第四列占比33,中间两列不指定比重,则平分剩余空间,
1 2 layoutHelper.setWeights (new float [] {33 f, Float .NaN , Float .NaN , 33 f});
效果如下,
来看layoutViews
方法,
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 void layoutViews (RecyclerView.Recycler recycler, RecyclerView.State state, VirtualLayoutManager.LayoutStateWrapper layoutState, LayoutChunkResult result, LayoutManagerHelper helper) { final int count = getAllChildren(mViews, recycler, layoutState, result, helper); for (int i = 0 ; i < count; i++) { View view = mViews[i]; VirtualLayoutManager.LayoutParams params = (VirtualLayoutManager.LayoutParams) view.getLayoutParams(); int heightSpec = helper.getChildMeasureSpec( helper.getContentHeight() - helper.getPaddingTop() - helper.getPaddingBottom(), uniformHeight > 0 ? uniformHeight : params.height, true ); if (mWeights != null && i < mWeights.length && !Float.isNaN(mWeights[i]) && mWeights[i] >= 0 ) { int resizeWidth = (int ) (mWeights[i] * 1.0f / 100 * availableWidth + 0.5f ); if (!Float.isNaN(params.mAspectRatio)) { int specialHeight = (int ) (resizeWidth / params.mAspectRatio + 0.5f ); heightSpec = View.MeasureSpec .makeMeasureSpec(specialHeight, View.MeasureSpec.EXACTLY); } helper.measureChildWithMargins(view, View.MeasureSpec.makeMeasureSpec(resizeWidth, View.MeasureSpec.EXACTLY), heightSpec); usedWidth += resizeWidth; minHeight = Math.min(minHeight, view.getMeasuredHeight()); } else { mEqViews[eqSize++] = view; } } }
3.将剩余宽度平分给没有设置百分比的child,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 for (int i = 0 ; i < eqSize; i++) { View view = mEqViews[i]; VirtualLayoutManager.LayoutParams params = (VirtualLayoutManager.LayoutParams) view.getLayoutParams(); int heightSpec; int resizeWidth = (int ) ((availableWidth - usedWidth) * 1.0f / eqSize + 0.5f ); if (!Float.isNaN(params.mAspectRatio)) { int specialHeight = (int ) (resizeWidth / params.mAspectRatio + 0.5f ); heightSpec = View.MeasureSpec .makeMeasureSpec(specialHeight, View.MeasureSpec.EXACTLY); } else { heightSpec = helper.getChildMeasureSpec( helper.getContentHeight() - helper.getPaddingTop() - helper.getPaddingBottom(), uniformHeight > 0 ? uniformHeight : params.height, true ); } helper.measureChildWithMargins(view, View.MeasureSpec.makeMeasureSpec(resizeWidth, View.MeasureSpec.EXACTLY), heightSpec); minHeight = Math.min(minHeight, view.getMeasuredHeight()); }
4.为所有child统一高度,为最小高度
1 2 3 4 5 6 7 8 9 for (int i = 0 ; i < count; i++) { View view = mViews[i]; if (view.getMeasuredHeight() != minHeight) { helper.measureChildWithMargins(view, View.MeasureSpec.makeMeasureSpec(view.getMeasuredWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(minHeight, View.MeasureSpec.EXACTLY)); } }
5.测量完成,进行布局,最终交给RecyclerView.LayoutManager
进行处理,即layoutDecorated
,
1 2 3 4 5 6 7 8 9 for (int i = 0 ; i < count; i++) { View view = mViews[i]; int top = mTempArea.top, bottom = mTempArea.bottom; int right = left + orientationHelper.getDecoratedMeasurementInOther(view); layoutChildWithMargin(view, left, top, right, bottom, helper); left = right; }
举例FloatLayoutHelper浮动可拖拽布局 FloatLayoutHelper
的布局代码就不看了,大概就是根据位置和偏移量计算具体位置,我们重点关注下他的触摸事件实现,
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 View.OnTouchListener touchDragListener = new View.OnTouchListener() { boolean onTouch (View v, MotionEvent event ) { int action = event .getAction(); switch (action) { case MotionEvent.ACTION_DOWN: isDrag = false ; (v.getParent()).requestDisallowInterceptTouchEvent(true ); lastPosX = (int ) event .getX(); lastPosY = (int ) event .getY(); break ; case MotionEvent.ACTION_MOVE: if (Math.abs(event .getX() - lastPosX) > mTouchSlop || Math.abs(event .getY() - lastPosY) > mTouchSlop) { isDrag = true ; } if (isDrag) { v.setTranslationX(curTranslateX); v.setTranslationY(curTranslateY); } break ; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: doPullOverAnimation(v); (v.getParent()).requestDisallowInterceptTouchEvent(false ); break ; } } }
效果如下,
RecyclerView复用和Cantor函数 RecyclerView
最终使用的是管理子适配器集合的DelegateAdapter
,通常情况下,我们是没法保证各个子适配器间的viewType
能不冲突的,所以这里只分析hasConsistItemType=false
的情况,具体原因见FAQ (组件复用的问题),
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 @Override public int getItemViewType (int position) { Pair<AdapterDataObserver, Adapter> p = findAdapterByPosition(position); int subItemType = p.second.getItemViewType(position - p.first.mStartPosition); int index = p.first.mIndex; return (int ) Cantor.getCantor(subItemType, index); } @Override public RecyclerView.ViewHolder onCreateViewHolder (ViewGroup parent, int viewType) { Cantor.reverseCantor(viewType, cantorReverse); int index = (int )cantorReverse[1 ]; int subItemType = (int )cantorReverse[0 ]; Adapter adapter = findAdapterByIndex(index); return adapter.onCreateViewHolder(parent, subItemType); }
这边有点晦涩,画了张图,需要细品~
这样,自然就可以利用RecyclerView
自带的复用机制帮我们管理view的复用了,
关于cantor函数:
设idx1,type1;idx2,type2,
当 idx1 != idx2 或 type1 != type2,
viewType1 = cantor(idx1,type1)
viewType2 = cantor(idx2,type2) 时
满足 viewType1 != viewType2
同时支持逆运算:
viewType1 => idx1,type1
viewType2 => idx2,type2
感兴趣的话可以看vlayout中使用数学的小场景 。
参考文章
苹果核 - Tangram 的基础 —— vlayout(Android)
苹果核 - Pairing Function —— vlayout 中使用数学的小场景
博客园-基于场景解析RecyclerView的回收复用机制原理
(五)Tangram原理 基于Tangram最新源码分析
笔者Demo代码
在Tangram和vlayout介绍 这篇文章提到过,Tangram
通过解析json模板得到布局方式Card
和具体视图Cell
,然后将Card
转换成对应的vlayout
的LayoutHelper
来进行测量和布局,如下,
官网的架构图 如下,
Card转成LayoutHelper 跟进TangramActivity
的engine.setData(data)
,
1 2 3 4 5 6 7 8 9 10 11 void setData(T data ) { List<C> cards = mDataParser.parseGroup(data , this ); this .setData(cards); } void setData(List<C> data ) { this .mGroupBasicAdapter.setData(data ); }
来到GroupBasicAdapter
,
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 void setData (List<L> cards, boolean silence) { setLayoutHelpers (transformCards(cards, mData, mCards)); if (!silence) notifyDataSetChanged (); } List<LayoutHelper> transformCards (List<L> cards, List<C> data, List<Pair<Range<Integer>, L>> rangeCards) { int lastPos = data.size (); List<LayoutHelper> helpers = new ArrayList<>(cards.size()); for (int i = 0 , size = cards.size(); i < size; i ++) { L card = cards.get (i); final String ctype = getCardStringType (card); List<C> items = getItems (card); if (items == null) { continue; } data.addAll (items); int offset = lastPos; lastPos += items.size (); rangeCards.add (Pair.create(Range.create(offset, lastPos), card)); LayoutBinder<L> binder = mCardBinderResolver.create (ctype); LayoutHelper helper = binder.getHelper (ctype, card); if (helper != null) { helper.setItemCount (items.size()); helpers.add (helper); } } return helpers; }
Card
被转换成LayoutHelper
,
转换完成后,调用了notifyDataSetChanged
,是如何显示到RecyclerView
上的呢?
RecyclerView展示 跟进TangramActivity
的engine.bindView(recyclerView)
,
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 void bindView(@NonNull final RecyclerView view) { this .mContentView = view; this .mContentView.setLayoutManager(mLayoutManager); mLayoutManager.setPerformanceMonitor(mPerformanceMonitor); if (mGroupBasicAdapter == null ) { this .mGroupBasicAdapter = mAdapterBuilder.newAdapter(mContext, mLayoutManager, this ); mGroupBasicAdapter.setPerformanceMonitor(mPerformanceMonitor); mGroupBasicAdapter.setErrorSupport(getService(InternalErrorSupport.class )); } if (mContentView.getRecycledViewPool() != null ) { mContentView.setRecycledViewPool(new InnerRecycledViewPool(mContentView.getRecycledViewPool())); } register(GroupBasicAdapter.class , mGroupBasicAdapter); register(RecyclerView.RecycledViewPool.class , mContentView.getRecycledViewPool()); this .mContentView.setAdapter(mGroupBasicAdapter); }
可见RecyclerView
设置的适配器是GroupBasicAdapter
,看下我们比较关心的几个方法,
1 2 3 4 5 6 7 8 9 int getItemViewType (int position) { C data = mData.get (position); return getItemType (data); }
官方Demo早期用了int
来声明Cell
,这样容易混乱,不利于在json模板里表意,现在改成了String
来声明(为此还做了些兼容代码),建议直接使用String
来注册,可参考Tangram的使用 ,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "id" : "banner1" , "type" : "container-oneColumn" , "style" : { "aspectRatio" : 3.223 }, "items" : [ { "bizId" :"item1" , "type" : 110 , // 不要再使用int 声明cell,建议使用唯一字符串如SingleImageView "msg" : "info1" }, { "bizId" :"item2" , "type" : 110 , // 不要再使用int 声明cell,建议使用唯一字符串如SingleImageView "msg" : "info2" } ] }
然后看下onCreateViewHolder
和onBindViewHolder
,
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 BinderViewHolder <C, ? extends View > onCreateViewHolder (ViewGroup parent, int viewType ) { String cellType = getCellTypeFromItemType (viewType); ControlBinder <C, ? extends View > binder = mCompBinderResolver.create (cellType); BinderViewHolder binderViewHolder = createViewHolder (binder, mContext, parent); return binderViewHolder; } void onBindViewHolder (BinderViewHolder<C, ? extends View> holder, int position ) { C data = mData.get (position); holder.bind (data); } void postBindView (BaseCell cell ) { textView.setText ("xxx" ); }
至此,整个流程就跑通了。
参考文章
(六)数据分离 笔者Demo代码 ,内容见demo2
包。
前面的文章提到过,在实际业务中不太可能把数据绑定在模板里,这样模板会很臃肿,我们要做的是,用模板描述页面结构和数据源
,而非数据本身,因此需要将数据剥离出来。
运行效果:
数据mock自玩安卓 (看着有点乱,后续有时间搭个小服务,向业务贴近),
重点看页面结构即可,远程模板调整了Card
顺序、4列布局改成5列、改了文本颜色和瀑布流item背景色。
然后来看该页面实现ShoppingHomeAct
,
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 public class ShoppingHomeAct extends TangramActivity { ActivityShoppingHomeBinding mBinding; @Override protected void onCreate (Bundle savedInstanceState ) { mBinding = DataBindingUtil .setContentView (this , R.layout .activity_shopping_home ); super .onCreate (savedInstanceState); } @Override protected String createBizDomain ( ) { return "shopping_home" ; } @Override protected RecyclerView createRecyclerView ( ) { return mBinding.rvList ; } @Override protected boolean needRefreshAndLoadMore ( ) { return true ; } }
代码很少,只需继承具备Tangram
能力的TangramActivity
,返回其需要的对象即可。
动态合并数据 参考官方Demo,首先想到的方法是,动态来合并数据,也就是按如下思路,
进行数据准备,
模板地址:net_shopping_home.json
聚合数据接口:tangram/shopping/home
瀑布流数据接口:玩安卓 - article/list/0/json
模板如下(有删减),描述了页面结构和数据源,
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 { "requestMakeup" : "http://rest.apizza.net/mock/3f233eed2d9be716a5f48fccb9c719f2/tangram/shopping/home" , "requestList" : "https://www.wanandroid.com/article/list/%s/json" , "templateName" : "net_shopping_home" , "template" : [ { "type" : "container-fiveColumn" , "load" : "makeup:category" , "itemType" : "ImageTextView" , "style" : { "textColor" : "#6699ff" , "padding" : [ 9 , 9 , 0 , 9 ] } } , { "type" : "container-waterfall" , "itemType" : "GoodsItemView" , "load" : "xxx" , "style" : { "column" : 2 , "hGap" : "4" , "vGap" : "4" , "margin" : [ 9 , 9 , 0 , 9 ] , "itemBgColor" : "#1F1F1F" , "textColor" : "#ffffff" } } ] }
聚合数据如下(有删减),
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 { "errorCode" : 0 , "errorMsg" : "" , "data" : { "banner" : [ { "imgUrl" : "https://wanandroid.com/blogimgs/942a5c62-ca87-4e7c-a93d-41ff59a15ba4.png" , "link" : "https://www.wanandroid.com/navi" } , { "imgUrl" : "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png" , "link" : "https://www.wanandroid.com/blog/show/2" } , { "imgUrl" : "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png" , "link" : "https://flutter.cn/" } ] , "bottomTitle" : [ { "title" : "猜你喜欢" } ] } }
一切准备就绪,开始实现TangramActivity
,关注主要实现即可。
把聚合数据合并进模板对象的template字段,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void mergeMakeupDataToTemplate (JSON Object data, JSON Array template) throws JSON Exception { for (int i = 0 ; i < template.length (); i++) { JSON Object card = template.getJSONObject (i); if (card.has ("load" ) && card.getString ("load" ).startsWith ("makeup:" )) { String load = card.getString ("load" ); JSON Array cells = data.getJSONArray (load.substring (load.indexOf (":" ) + 1 )); for (int cellIdx = 0 ; cellIdx < cells.length (); cellIdx++) { cells.getJSONObject (cellIdx).put ("type" , card.getString ("itemType" )); } card.put ("items" , cells); } } }
解析瀑布流数据,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void parseListData (List<ArticleBean.DataBean.Article> list, @NonNull Card card ) { JSON Array cells = new JSON Array (); try { for (int i = 0 ; i < list.size (); i++) { JSON Object obj = new JSON Object (MyApp .gson .toJson (list.get (i))); obj.put ("type" , card.optStringParam ("itemType" )); obj.put ("imgUrl" , DataUtil .getImgByIdx (i + mListDataPage * list.size ())); cells.put (obj); } } catch (JSON Exception e) { e.printStackTrace (); } List <BaseCell > cs = mEngine.parseComponent (cells); card.addCells (cs); card.notifyDataChange (); finishLoad (); }
完整代码可见TangramActivity.java 。
待解决问题
局部刷新问题,暂时无解。之前有小伙伴提到过tangram不支持局部刷新,然后实践了一下,处理起来确实挺棘手,如加载瀑布流数据后,card.notifyDataChange
的本质还是notifyDataSetChanged
。
优化成非继承TangramActivity
实现。让业务Activity继承实现始终不够灵活,尝试包装一下核心引擎TangramEngine
。
(七)硬核的Virtualview GitHub地址:
GitHub - Virtualview-Android
GitHub - virtualview_tools
需求背景 一文介绍了模块化搭建页面的由来,那有没有想过这样一种场景,有天产品灵光一闪,想要不发版把上图下文
换成上文下图
,又或者想要在每个图片右上角加个双11大促角标来营造氛围,由于客户端只预埋了上图下文的样式(以下简称cell
),即ImageTextView
,所以只能延期到下一班车,
很显然,即便我们根据当下的业务抽象了一些常用的Cell
,比如上图下文
、纯文本
、单图
等,而且还支持了一些通用的属性配置如文本大小颜色等,也无法满足多变的业务需求,也即cell
不够用了,我们要有线上生产cell
并下发的能力。所以,VirtualView
诞生了。
VirtualView
的核心思想是,编写xml
样式文件,编译压缩成二进制文件,下发到客户端,客户端解析,转成native view
,或者用canvas
绘制。引用官方的一张图片,
因此,当UI有细节变动时,只需要修改xml
,然后编译好下发给客户端替换即可。不过,我们的生产环境用的是另外一套基于flexbox-layout 的方案而非VirtualView
,本文是站在学习的角度进行调研。
框架名字积木
和七巧板
,可见,相似的业务场景,衍生出了相似的技术方案。
VirtualView
很赞的两点是,他的二进制压缩
和实时预览
,接下来进行详细分析。
二进制压缩
通过 XML 编写的业务组件,如果直接加载解析,会有几个问题:一是原始文件相对较大,因为 XML 里会有冗余信息,如空格、换行、还有重复出现的字符串等,文件体积比较大;二是解析 XML 会有一定开销,相对于二进制数据直接解析,XML 解析会比较重,例如节点遍历、属性访问等都显得有些臃肿。通过提前将 XML 模板处理成二进制格式,可以将繁重的解析工作从客户端运行时中剥离出来,而通过将一些重复的资源做合并处理并建立索引,可以减少冗余信息,减少模板文件大小,通常情况下,处理成二进制格式的模板比原始模板可减少 50% - 60% 的大小。
引用自苹果核 - VirtualView Android实现详解(一)—— 文件格式与模板编译
先来看一个简单的xml
样式文件,直接把他下发到客户端存在两个问题,一是冗余字符引起的带宽浪费,二是客户端解析耗时和内存,在用户手机内存吃紧时,面对一个样式繁多的RecyclerView
时,即便存在复用机制也可能因解析引起oom(来自电商的痛),往往需要在编译期就把xml
转成view类,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <VHLayout xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation ="../../vv.xsd" orientation ="V" layoutWidth ="match_parent" layoutHeight ="200" background ="#11000000" > <NText text ="${text}" textSize ="30" textColor ="@{${items[0].info.textColor} ? ${items[1].subItems[0].info.textColor} : ${items[2].subItems[0].info.textColor}}" background ="#fffff0" layoutWidth ="match_parent" layoutHeight ="200" gravity ="h_center|v_center" /> </VHLayout >
连Android自带的XmlPullParser
解析都足够重了,那我们能不能避开这个思路呢?来看看VirtualView
的思路,首先看到virtualview_tools工程 ,在virtualview_tools/compiler-tools/RealtimePreview/config.properties
文件中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 把内置支持的view 映射成int ,1000 以内 VIEW_ID_FrameLayout=1 VIEW_ID_NImage=9 VIEW_ID_VImage=10 // 1000 以上给外部自定义的view VIEW_ID_TotalContainer=1010 // 定义枚举映射,即xml 里写的row 、row -reverse 也会被转成int flexDirection=Enum<row :0 ,row -reverse :1 ,column :2 ,column -reverse :3 > orientation=Enum<H:1 ,V:0 > // 定义一些属性值的类型 borderWidth=Float itemWidth=Number
在进行类型的简化后,约定一种数据格式,每一块分别展示什么信息,如下,
比如,开头有版本区
,后面有组件区
、组件长度区
、字符串区
、字符串长度区
、表达式区
、表达式长度区
…这有点像JVM
校验解析字节码的过程。一些资源的映射处理,如下,
颜色:转换成4字节整型颜色值,格式 AARRGGBB;
枚举:按照预定义的整数转换,比如 gravity 的类型,orientation 的类型;
字符串:以 hashCode 值作为它的序列化后整数,并在字符串资源区建立以 hashCode 为索引的列表,在解析的时候从中获取原始的字符串值;
逻辑表达式:与字符串的处理类似;
数字:直接转换成 4 字节的整型或者浮点型,并支持带单位的类型;
引用自苹果核 - VirtualView Android实现详解(一)—— 文件格式与模板编译
字符串用hashCode值为索引的列表方案,可以节省重复字符串的空间,表达式是用来绑定动态数据如${text}
。
得到二进制数据,
把二进制数据下发到客户端,在Virtualview-Android工程 中,可以看到一个BinaryLoader
类,
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 public int loadFromBuffer (byte [] buf, boolean override) { CodeReader reader = new CodeReader (); reader.setCode(buf); reader.seekBy(Common.TAG.length()); int majorVersion = reader.readShort(); int minorVersion = reader.readShort(); int patchVersion = reader.readShort(); reader.setPatchVersion(patchVersion); int uiStartPos = reader.readInt(); reader.seekBy(4 ); int strStartPos = reader.readInt(); reader.seekBy(4 ); int exprCodeStartPos = reader.readInt(); reader.seekBy(4 ); }
这样,把xml
样式文件压缩成二进制文件,既节省了带宽,又免去了客户端比较重的XmlPullParser
解析,真是快乐Double~
原生控件和虚拟控件 VirtualView
翻译成中文就是虚拟视图,因为他里边有个虚拟控件的概念。可以看到它里边有些控件有两份,分别是V和N开头的,如VImage
和NImage
、VText
和NText
,
V开头指的是Virtual View
虚拟视图,即不需要实际的ImageView
或TextView
,而是在一个Container
(如ViewGroup)内,直接拿他的画布canvas
进行内容绘制,如drawText
或drawBitmap
等操作;
N开头指的是Native View
即原生视图,需要实际的ImageView
或TextView
来承载。
看下截图更直观,
Virtual View
:
Native View
:
虚拟视图跟原生视图相比会更轻量,当然具体还得结合业务使用,目前支持两种视图的混用,这样就需要去避免一个问题,虚拟视图画在宿主上作为”背景“,原生视图放在宿主上有可能会遮挡虚拟视图。
实时预览 安装fswatch
监听文件修改,
安装qrencode
生成二维码(可选),
在virtualview_tools项目 中virtualview_tools/compiler-tools/RealtimePreview
目录下,执行./run.sh
启动服务器,手机和电脑连同一网络,手机运行Virtualview-Android项目 (记得把HttpUtil
类中的ip地址改成电脑的ip),进入模板实时预览
,可以加载服务器下发的HelloWorld
,点进去就可以看样式了,
接着修改文件保存,fswatch
监听到修改,触发服务器重新编译HelloWorld
,
合并结果data.json
如下,
1 2 3 4 5 6 7 8 9 { "templates" : [ "QUxJVlYAAQAAOMQAAAAvAAAAkAAAAL8AAAD1AAABuAAAAAAAAAG8AAAAAAABAAAAAAABAApIZWxsb1dvcmxkAH4AAAIEqjL10AAAAABc1fDxAAAAyLCYVS4RAAAAd3CsvP////8AAAACfREwBNF35jvOOvRwYx6r5gAAAAAHBcQtOs4AAAAUXNXw8QAAAMiwmFUu////8BC4ck4AAAAkd3CsvP////8AAAACADZFLUjWynnAmy42tiGl5gAAAQEAAAAGfREwBAAdeHNpOm5vTmFtZXNwYWNlU2NoZW1hTG9jYXRpb262IaXmAG9AeyR7aXRlbXNbMF0uaW5mby50ZXh0Q29sb3J9ID8gJHtpdGVtc1sxXS5zdWJJdGVtc1swXS5pbmZvLnRleHRDb2xvcn0gOiAke2l0ZW1zWzJdLnN1Ykl0ZW1zWzBdLmluZm8udGV4dENvbG9yfX1jHqvmAClodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZc469HAACXhtbG5zOnhzadF35jsADC4uLy4uL3Z2LnhzZEjWynkAByR7dGV4dH0AAAAA" ] , "data" : { "text" : "Hello World!" } }
可见实时预览时,服务端把二进制数据进行了Base64编码(真实的业务场景也可以参考),客户端点击Refresh
按钮重新加载http://127.0.0.1:7788/helloworld/data.json
,在PreviewActivity
中,
1 2 3 4 5 6 7 8 9 PreviewData previewData = new Gson ().fromJson (string, PreviewData.class); loadTemplates (previewData.templates);sViewManager.loadBinBufferSync (Base64.decode(temp, Base64.DEFAULT));
在VirtualView
的加持下,Tangram
的动态能力得到进一步提升,实现了线上生产cell
并下发替换。
一些案例 Tangram
:
官方show-case 、
内部Lego
:
参考文章
苹果核 - 天猫客户端组件动态化的方案——VirtualView 上手体验
苹果核 - VirtualView 工具大更新啦
苹果核 - 提升开发体验,预览 VirtualView
苹果核 - VirtualView Android 实现详解(三)—— 添加一个自定义控件
文档 - Virtualview