Android 动态解析生成布局文件的意思是:通过服务器给你发送一段Json 文件,然后根据其中的自己定义的属性,解析成原生的Android 的布局文件,并添加到 View 上作为展示。
该用途是可以实时在线更新多种不同的布局,而不是写死在apk中的不同布局文件,然后根据传进来的不同参数,显示不一样的布局。
这两种有本质的区别,在于一个是静态的(死布局),而另外一个是动态布局(比较灵活),因为如果要新增多种布局的话,又要去改apk的代码去新增,而如果是动态解析布局的话,那么就不用每次都更新apk啦,其次是如果是sdk需要这样的功能给第三方接入sdk的人使用时,是不太可能每次都去更新sdk的。
先看看效果图:
上面的那个卡片布局就是动态解析json 文件生成的。
下面是整个Demo的地址:
https://download.csdn.net/download/m0_37094131/10494440
其实采用了第三方的框架,然后在它的框架中加上一些自己需要的功能,比如drawable中的xml文件,比如上面的查询按钮中的圆角背景,就是框架中没有提供的功能,是自己改着源码添加的,然后话不多说,开始看代码吧:
{
"widget": "android.widget.RelativeLayout",
"properties": [{
"name": "background",
"type": "color",
"value": "#e5e5e5"
},
{
"name": "layout_width",
"type": "dimen",
"value": "match_parent"
},
"views": [{
"widget": "android.widget.RelativeLayout",
"properties": [{
"name": "layout_width",
"type": "dimen",
"value": "362dp"
},
{
"name": "layout_height",
"type": "dimen",
"value": "wrap_content"
},
...
{
"name": "background",
"type": "ref",
"value": "shape|corner:8|color:'#ffffff'"
},
...
这只是部分json布局文件,可以提前写好,也可以用网络请求从后台服务器中获取,由于是Demo,所以我就没有放在服务器上,而是放在本地文件中去获取。真实情况下,是需要请求网络获取的。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
JSONObject jsonObject;
try {
jsonObject = new JSONObject(readFile("TextIT.json", this));
} catch (JSONException je) {
je.printStackTrace();
jsonObject = null;
}
if (jsonObject != null) {
//根据json 解析得到的json对象,来创建View
View sampleView = DynamicView.createView(this, jsonObject, ViewHolder.class);
sampleView.setLayoutParams(new WindowManager.LayoutParams(
MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT));
setContentView(sampleView);
} else {
Log.e("cx", "Could not load valid json file");
}
}
重点关注这一行代码
//根据json 解析得到的json对象,来创建View
View sampleView = DynamicView.createView(this, jsonObject, ViewHolder.class);
那现在重点就是这个createView 方法啦:
public static View createView (Context context, JSONObject jsonObject, ViewGroup parent, Class holderClass) {
...
HashMap ids = new HashMap<>();
View container = createViewInternal(context, jsonObject, parent, ids);
if (container.getTag(INTERNAL_TAG_ID) != null)
//解析布局属性
DynamicHelper.applyLayoutProperties(container, (List) container.getTag(INTERNAL_TAG_ID), parent, ids);
...
container.setTag(INTERNAL_TAG_ID, null);
if (holderClass!= null) {
try {
Object holder = holderClass.getConstructor().newInstance();
//解析View
DynamicHelper.parseDynamicView(holder, container, ids);
container.setTag(holder);
} catch (Exception e) {
e.printStackTrace();
}
}
return container;
}
重点是这两行代码
//创建View
(1) View container = createViewInternal(context,
jsonObject, parent, ids);
//解析布局参数
(2) DynamicHelper.applyLayoutProperties(container,
(List) container.getTag(
INTERNAL_TAG_ID), parent, ids);
先看第一个:
private static View createViewInternal (Context context, JSONObject jsonObject, ViewGroup parent, HashMap ids) {
View view = null;
ArrayList properties;
try {
String widget = jsonObject.getString("widget");
if (!widget.contains(".")) {
widget = "android.widget." + widget;
}
//反射构建Android view 的布局对象
Class viewClass = Class.forName(widget);
view = (View) viewClass.getConstructor(Context.class).newInstance(new Object[] { context });
} catch (Exception e) {
e.printStackTrace();
}
...
try {
//创建布局参数
ViewGroup.LayoutParams params = DynamicHelper.createLayoutParams(parent);
view.setLayoutParams(params);
properties = new ArrayList<>();
JSONArray jArray = jsonObject.getJSONArray("properties");
...
view.setTag(INTERNAL_TAG_ID, properties);
//解析style
String id = DynamicHelper.applyStyleProperties(context,view, properties);
if (!TextUtils.isEmpty(id)) {
ids.put(id, mCurrentId);
view.setId( mCurrentId );
mCurrentId++;
}
if (view instanceof ViewGroup) {
...
JSONArray jViews = jsonObject.optJSONArray("views");
if (jViews != null) {
int count=jViews.length();
for (int i=0;i...
}
}
for(View v : views) {
//遍历View解析属性
DynamicHelper.applyLayoutProperties(v, (List) v.getTag(INTERNAL_TAG_ID), viewGroup, ids);
v.setTag(INTERNAL_TAG_ID, null);
}
}
} catch (JSONException e) {
e.printStackTrace();
}
return view;
}
通过反射来构造一个View 对象,而至于是什么View 取决于你在Json文件中获取到的属性,可以是LinearLayout也可以是ReleativeLayout等等。通过递归去遍历Json 文件中的属性,然后生成不同的嵌套的控件。
生成原生控件之后,就开始得适配属性值,以及控件位置了。
public static void applyLayoutProperties(View view, List properties, ViewGroup viewGroup, HashMap ids) {
if (viewGroup == null)
return;
ViewGroup.LayoutParams params = createLayoutParams(viewGroup);
for (DynamicProperty dynProp : properties) {
try {
switch (dynProp.name) {
case LAYOUT_HEIGHT: {
params.height = dynProp.getValueInt();
}
break;
case LAYOUT_WIDTH: {
params.width = dynProp.getValueInt();
}
break;
...
case LAYOUT_GRAVITY: {
switch (dynProp.type) {
case INTEGER: {
if (params instanceof LinearLayout.LayoutParams)
((LinearLayout.LayoutParams) params).gravity = dynProp.getValueInt();
}
break;
case STRING: {
if (params instanceof LinearLayout.LayoutParams)
((LinearLayout.LayoutParams) params).gravity = (Integer) dynProp.getValueInt(Gravity.class, dynProp.getValueString().toUpperCase());
}
break;
}
}
break;
case LAYOUT_WEIGHT: {
switch (dynProp.type) {
case FLOAT: {
if (params instanceof LinearLayout.LayoutParams)
((LinearLayout.LayoutParams) params).weight = dynProp.getValueFloat();
}
break;
}
}
break;
}
} catch (Exception e) {
}
}
view.setLayoutParams(params);
}
可以适配Android 所有原生位置的属性,Layout_Width,Layout_Height,Layout_toLeftof等等,属性太多,就不占太多篇幅了,因为demo中有完整的例子。
这是为每个控件生成Params.
public static String applyStyleProperties(Context context ,View view, List properties) {
mContext = context;
String id = "";
for (DynamicProperty dynProp : properties) {
switch (dynProp.name) {
case ID: {
id = dynProp.getValueString();
Log.e("cx" , "ID applyStyleProperties :" + id);
}
break;
case BACKGROUND: {
applyBackground(context ,view, dynProp);
}
break;
case TEXT: {
applyText(view, dynProp);
}
...
break;
case DRAWABLELEFT: {
applyCompoundDrawable(view, dynProp, 0);
}
break;
case SELECTED: {
applySelected(view, dynProp);
}
break;
case SCALEX: {
applyScaleX(view, dynProp);
}
break;
...
case VISIBILITY:{
applyVisibility(view, dynProp);
}
break;
}
}
return id;
}
这是的属性设置,也适配许多,由于篇幅问题,只能省略显示,具体的demo中全部都有!
看到这里估计很多人就懂了,解析布局,其实类似Android 的代码LayoutInflate.from(context).inflate(R.layout.main,container,false),和里面的源码思路差不多,读者感兴趣的话,可以自己去了解一下Android 加载布局的源码,会很有意思,不断的递归递归。
总结一下,就是解析Json 文件中的不同属性,生成不同的View,包括嵌套View,以及同级View都行。然后在解析View中设置好位置,以及id,还有background,以及你自己想要定制什么属性都可以,你也可以扩展一些你想要实现的属性,包括自定义View等。另外多说一句,其实写TextIT.json 文件可以按照你写好的xml布局文件的格式,去模仿改动就可以了。