移动端动态渲染技术实例解析

动态渲染库简介

移动端开发中,对于信息流展示页面,通常对动态性有较高的要求,本地预置模板+接口下发动态组合的方式虽然从某种程度上做到了动态化,但新增样式时依然不可避免需要发版.

本例中通过自定义页面描述语法,并将其下发至客户端,由客户端实时生成布局并渲染,来实现UI级别的动态化.

整体架构

将按照Flex规范定义的页面描述文件,转化成对应的原生视图,同时根据其配置信息,从外层数据中查找对应内容进行展示.

核心流程详解

DSL解析

接口下发的DSL数据示例

其中样式,事件,动画的解析,由一个入口XML解析器来分发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    @Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (mInnernalHandler == null) {
switch (localName) {
case "Action":
mInnernalHandler = new ActionXmlHandler();
break;
case "Animation"://动画还未启用,暂时注释.
// mInnernalHandler = new AnimationXmlHandler();
break;
default:
mInnernalHandler = new DynamicViewXmlHandler();
break;
}
if (mInnernalHandler != null) {
mInnernalHandler.startElement(uri, localName, qName, attributes);
}
} else {
mInnernalHandler.startElement(uri, localName, qName, attributes);
}
}

入口解析器解析完每一段xml数据,都会返回DslMappingData数据.前端根据数据实际类型进行保存.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DslMappingData mappingData = XMLParsingMethods.readMixedXmlBySAX(layoutBean.dslXml);
if (mappingData != null) {
if (mappingData instanceof XmlActionEntity) {
mActionPool.addAction((XmlActionEntity) mappingData);
} else if (mappingData instanceof XmlAnimationEntity) {
mAnimationPool.addAnimation((XmlAnimationEntity) mappingData);
} else if (mappingData instanceof DynamicViewStyle) {
DynamicViewStyle viewStyle = (DynamicViewStyle) mappingData;
viewStyle.layoutID = layoutBean.layoutID;
viewStyle.ver = layoutBean.ver;
if (TextUtils.isEmpty(viewStyle.ver)) {
viewStyle.ver = String.valueOf(System.currentTimeMillis());
}
DynamicViewStyle currentStyle = mLayoutBeanHashMap.get(layoutBean.layoutID);
if (currentStyle == null || currentStyle.ver == null) {
mLayoutBeanHashMap.put(layoutBean.layoutID, viewStyle);
} else {
if (!currentStyle.ver.equals(viewStyle.ver)) {
mLayoutBeanHashMap.put(layoutBean.layoutID, viewStyle);
}
}
}
}

视图创建

根据DynamicViewStyle中定义的type,生成对应原生控件,并使用对应装饰器对其进行配置.

DefaultViewGenerator是动态渲染库内部默认实现,提供了对应View的实例化过程:

1. 根据对应type,生成原生控件和其对应装饰器.

生成原生控件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private DynamicViewDecorator makeSingleView(Activity activity, DynamicViewStyle style) {
DynamicViewDecorator viewHolder = null;
if (style.type != null) {
switch (style.type) {
case "View":
viewHolder = new YogaLayoutDecorator(new CustomYogaLayout(activity));
break;
case "CardView":
viewHolder = new CardViewDecorator(new MgCardView(activity));
break;
case "ListView":
viewHolder = new ListViewDecorator(new DynamicListView(activity));
break;
}
if (viewHolder == null) {
View view = mExtendViewCreater.makeView(activity, style.type);
if (view != null) {
viewHolder = new ExtendViewDecorator(view);
}
}
}
return viewHolder;
}
根据控件类型,使用不同的装饰器进行属性配置

此处装饰器!=装饰者模式,仅是用于对不同类型的控件,进行特有的动态属性设置.

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
/**
* 作者:徐斌 <br>
* 日期:2020 08/03 10:09 <br>
* 说明:动态渲染控件配置接口,定义了目前动态渲染支持的一些属性.
*/
public interface DslDecorator {
/**
* 解析配置的Layout属性
* @param bean DynamicViewStyle中的layout
* @param parent 父容器.
*/
void parseLayout(StyleLayout bean, @Nullable ViewGroup parent);

/**
* 解析配置的Attr属性
* @param styleAttr DynamicViewStyle中的attr
* @param acitonObject DynamicViewStyle中的action
* @param actionPool 外部的Action池子.定义了各种事件.
*/
void parseAttr(Map<String, String> styleAttr, ActionEntity acitonObject, ActionPool actionPool);

/**
* 解析配置的动画属性
* @param animationEntity DynamicViewStyle中的animation部分
* @param animationPool 外部的Animation池子.定义了各种动画.
*/
void parseAnimation(AnimationEntity animationEntity, AnimationPool animationPool);
}

2. 解析DynamicViewStyle中的layout配置,并应用(以通用的DynamicViewDecorator为例).

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
public void parseLayout(StyleLayout bean, @Nullable ViewGroup parent) {
YogaNode yogaNode = null;
if (parent instanceof YogaLayout) {
yogaNode = ((YogaLayout) parent).getYogaNodeForView(itemView);
}
if (yogaNode != null) {
if (bean.alignSelf != null) {
switch (bean.alignSelf) {
case "flex-end":
yogaNode.setAlignSelf(YogaAlign.FLEX_END);
break;
case "center":
yogaNode.setAlignSelf(YogaAlign.CENTER);
break;
case "space-between":
yogaNode.setAlignSelf(YogaAlign.SPACE_BETWEEN);
break;
case "space-around":
yogaNode.setAlignSelf(YogaAlign.SPACE_AROUND);
break;
default:
yogaNode.setAlignSelf(YogaAlign.FLEX_START);
break;
}
} else {
yogaNode.setAlignSelf(YogaAlign.AUTO);
}
}
}

3. 解析DynamicViewStyle中的attr配置,并应用(以通用的DynamicViewDecorator为例).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public DynamicValueBinder<T> onParseAttr(@Nullable Map<String, String> styleAttr, @Nullable ActionEntity acitonObject, ActionPool actionPool) {
ValueStore valueStore;
DynamicValueBinder<T> binder = createDynamicValueBinder();
if (styleAttr != null) {
String alphaStr = styleAttr.get("alpha");
if (alphaStr != null) {
float alphaNum = DynamicUtil.stringToFloat(alphaStr);
if (alphaNum != 0) {
itemView.setAlpha(alphaNum);
}
}
String visibleStr = styleAttr.get("visible");
if (!TextUtils.isEmpty(visibleStr)) {
valueStore = new ValueStore(visibleStr);
binder.addDataPath("visible", valueStore);
}
}
return binder;
}

4. 为需要动态设置渲染内容的View配置id,用于外部查找.

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
//第四步,设置attr属性,影响该控件显示的内容.
// 如果是普通控件,按照是否有动态数据和是否有事件来决定生成id并加入列表
// 否则直接给生成一个id,然后加入列表
if (viewHolder instanceof ExtendViewDecorator) {
int id = countHelper.getIdAndInCrease();
itemView.setId(id);
if (bindableViewIds != null) {//bindableViewIds是对应某个layout样式的,这里做个判空,防止重复添加.
bindableViewIds.add(id);
}
} else {
viewHolder.parseAttr(style.attr, style.action, mActionPool);
//如果该控件有需要动态设置的属性.设置id,并把id放入公共的id列表中.
if (viewHolder.needDynamicBind) {
int id = 0;
if (itemView instanceof PlayerView) {
id = R.id.dsl_tag_playerview;
} else if (itemView instanceof BackgroundView) {
id = R.id.dsl_tag_backgroundview;
} else {
id = countHelper.getIdAndInCrease();
}
itemView.setId(id);
if (bindableViewIds != null) {//bindableViewIds是对应某个layout样式的,这里做个判空,防止重复添加.
bindableViewIds.add(id);
}
}
}

5. 为列表型View设置item样式.

1
2
3
if (style.item != null && itemView instanceof DslListInterface) {
((DslListInterface) itemView).setDynamicStyle(style.item, this);
}

6. 重复1~5,对其child进行实例化,并构建层次结构.

1
2
3
4
5
6
//第七步,如果这是个容器.遍历一下.添加child.
if (itemView instanceof ViewGroup && style.children != null) {
for (DynamicViewStyle childBean : style.children) {
makeCurrentView(activity, (ViewGroup) itemView, childBean, bindableViewIds, countHelper, connectionHelper);
}
}

7. 完成View-View间的绑定关系(目前用于Banner-Indicator).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* view创建完了.来做个绑定吧..可能内部存在啥联动之类的.比如banner和指示器
*/
public void creatConections() {
if (mConnectionAbleArrayList.size() > 1) {
for (ConnectionAble connectionAble : mConnectionAbleArrayList) {
for (ConnectionAble connectionAble2 : mConnectionAbleArrayList) {
if (connectionAble != connectionAble2) {
connectionAble.onConection(connectionAble2);
}
}
}
}
mConnectionAbleArrayList.clear();
}
对于动态渲染库不支持的type,会在第一步中调用设置的mExtendViewCreater来进行控件生成过程.
1
2
3
4
5
6
if (viewHolder == null) {
View view = mExtendViewCreater.generateView(activity, style);
if (view != null) {
viewHolder = new ExtendViewDecorator(view);
}
}

数据绑定

理论上所有配置在attr中的属性,都支持通过相对路径从数据实体中取值,并设置.

例如attr中配置text:~title,则意味着该控件将外部数据实体看做JsonObject,取其title字段当做文本内容.

在样式描述体中定义控件的某个属性和其取值对应路径,待外层拿到数据实体后,进行实时解析并设置给控件的过程,即动态绑定.

动态绑定定义在ValueBinder接口中.

1
2
3
4
5
6
7
8
9
public interface DataBinder<T extends View> {
/**
* 给定view和数据源,尝试用内部的datePathMap进行解析,然后设置给view
* @param view 当前view
* @param data 数据源
* @param listIndex 在列表中的index
*/
void rendWithData(T view, JsonObject data, CommBridge commBridge,int listIndex);
}

其主要有两类实现:

  • DynamicValueBinder:动态渲染库内部实现,具备保存属性和相对路径映射的能力.
  • 外部业务层实现类:对应原生控件,根据预置的属性和解析策略,自行实现渲染方法.

DynamicValueBinder结构图

逻辑计算

属性对应的值以^开头时,意味着不仅需要动态取值,还要进行逻辑运算.

例如visible:^feedBack and fdTags.count!=0代表当外层数据feedBack值不为空,且fdTags数据对应的列表长度不为0时,visible值为1.

动态渲染库中集成了简单的逻辑处理流程,目前支持and,or,==,!=,三目运算.

采用类似二叉树概念,将逻辑语句拆解,转化成多个可计算的最小单元,从末端开始计算,最终得到整体计算结果.

事件传递

当前事件主要有3种

  • 曝光事件.RenderAction.在渲染时产生.对应rendWithData调用时.
  • 点击事件.ClickAction.点击时产生.
  • 列表滑动事件.ChildScrollAction.列表控件的item滑动时产生.

事件通过rendWithData(T view, JsonObject data, CommBridge commBridge,int listIndex)中的CommBridge对外传递.

总结

UI级别的动态化由于完全采用平台api实现.在放弃部分灵活性的同时,保证其性能和兼容性,是当前移动端不可或缺的技术之一,其设计思路对于设计真正的跨平台框架也有所帮助.