前段时间hongyang大神发布了一个库,AndroidAutoLayout。该库的使用,是用户(该库的使用者,即,猿们)告诉app,设计图的宽和高为多少像素,然后在UI布局里直接使用px作为单位,该库会自动将填写的px值转换为屏幕的百分比值,以此来完成适配。
该库的使用方式有两种,一种是直接将Activity extends AutoLayoutActivity,另一种是在布局文件里使用该库提供的三个代替LinearLayout, FrameLayout, RelativeLayout的AutoLinearLayout, AutoFrameLayout, AutoRelativeLayout控件.
如果是使用直接继承AutoLayoutActivity的方法,这个相当省事。其实现是:在AutoLayoutActivity类内部重写了onCreateView(),这个方法会在Activity的onCreate()之后调用,onCreateView调用完毕才调用Activity生命周期函数onStart().
这是它在Activity源码中的说明,默认实现是返回null,它是LayoutInflater.Factory#onCreateView()的实现。
/**
* Standard implementation of
* {@link android.view.LayoutInflater.Factory#onCreateView} used when
* inflating with the LayoutInflater returned by {@link #getSystemService}.
* This implementation does nothing and is for
* pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} apps. Newer apps
* should use {@link #onCreateView(View, String, Context, AttributeSet)}.
*
* @see android.view.LayoutInflater#createView
* @see android.view.Window#getLayoutInflater
*/
@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
而Factory是LayoutInflater中的一个接口
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
在这个方法传进来的参数中的name是每一个View(放在布局文件中的View)的名字,比如LinearLayout, FrameLayout,TextView。
在Activity中onCreateView()会被调用多次,顺序是LinearLayout , ViewStub , FrameLayout(这个就是ContentFrameLayout),之后就是我们写在xml里的View了,所以在这里我们能拿到写在xml文件里的控件名字。
LayoutInflater创建View的方法是createView(),其流程是:从xml中解析出来信息后,判断是否带包名,如果没带包名就给name拼接上前缀(也就是系统的包名,例如TextView变成android.widget.TextView),带包名的(使用自定义View,扩展包View的时候要带包名)就不需要加前缀。拿到了这个View的包名+类名,类加载器加载class,取得构造方法,将View new出来。
而如果onCreateView返回的不是null,而是一个View,就不会走createView()。
作者就是在AutoLayoutActivity中重写了该方法,并判断如果是LinearLayout,就返回该库里的AutoLinearLayout。RelativeLayout,FrameLayout同上。如此便做到了偷梁换柱的效果,我们在xml里使用的是LinearLayout,但实际生成的AutoLinearLayout。
public View onCreateView(String name, Context context, AttributeSet attrs) {
Object view = null;
if(name.equals("FrameLayout")) {
view = new AutoFrameLayout(context, attrs);
}
if(name.equals("LinearLayout")) {
view = new AutoLinearLayout(context, attrs);
}
if(name.equals("RelativeLayout")) {
view = new AutoRelativeLayout(context, attrs);
}
return (View)(view != null?view:super.onCreateView(name, context, attrs));
}
而用户不是选择使用Activity extends AutoLayoutActivity这种方式,而是在xml文件里直接写上包名+AutoXXXLayout,额。。。这里好像没什么好说的。
接下来,我们来看该库的自动布局类,这里分析AutoFrameLayout,其余两个类似。
先说下背景,由于我们已经告诉app我们设计图的大小了(该库的使用条件之一,在Manifest.xml中添加meta,两个字段design_width,design_height),程序就能拿到设计图的宽高(单位为像素),而设备的屏幕宽高我们也是可以拿到的。当我们在布局文件里将View的属性值设为像素值时,自然就可以通过换算得到实际上在不同的屏幕上该占多少百分比的像素了。以宽为例:
(edit_width / design_width)* screen_width 这个结果就是实际的像素值。edit_width是用户填写的px值。这部分代码就不分析了。
既然已经把FrameLayout偷梁换柱成我们自己的View(AutoFrameLayout)了,那么就可以拿到布局文件时写的各个值(包括自定义属性,系统属性)。
作者在AutoFrameLayout中主要的代码是:
public AutoFrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new AutoFrameLayout.LayoutParams(this.getContext(), attrs);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(!this.isInEditMode()) {
this.mHelper.adjustChildren();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
重写generateLayoutParams(),让该ViewGroup中的每一个View的LayoutParams变成该类的静态内部类LayoutParams。
而在onMeasure()方法中借由AutoLayoutHelper来调整每一个子View的属性(该库支持9个属性)。
而在AutoFrameLayout.LayoutParams类中会把该库现有两个自定义属性(basewidth,baseheight),和9个支持的系统属性的值拿出来,然后将2个自定义属性和9个属性值放在AutoFrameLayout.LayoutParams的成员变量AutoLayoutInfo中,并提供getter。
public static class LayoutParams extends android.widget.FrameLayout.LayoutParams implements AutoLayoutParams {
private AutoLayoutInfo mAutoLayoutInfo;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
this.mAutoLayoutInfo = AutoLayoutHelper.getAutoLayoutInfo(c, attrs);
}
//...这处代码省略
public AutoLayoutInfo getAutoLayoutInfo() {
return this.mAutoLayoutInfo;
}
}
这里有一点需要提一下,在记录过程中,只有属性是以“px”结尾的属性才会被记录下来。也就是说,如果你在AutoFrameLayout中某个View某个属性不想使用这个百分比自动布局的功能,只要不把属性写成px的就行了。(我在用这个库的时候,有一个View的宽属性无法事前确定,只能wrap_content。还在想该库是不是可以提供一个自定义属性ignore可以让该View免于自动布局,看了代码才知道作者早就设计好了)
下面是adjustChildren()的代码:
public void adjustChildren() {
AutoLayoutConifg.getInstance().checkParams();
int i = 0;
for(int n = this.mHost.getChildCount(); i < n; ++i) {
View view = this.mHost.getChildAt(i);
LayoutParams params = view.getLayoutParams();
if(params instanceof AutoLayoutHelper.AutoLayoutParams) {
AutoLayoutInfo info = ((AutoLayoutHelper.AutoLayoutParams)params).getAutoLayoutInfo();
if(info != null) {
info.fillAttrs(view);
}
}
}
}
作者先做了一次检查,checkParams(),如果用户没有在Manifest.xml中提供design_width,design_height会报错。
之后就是拿到mHost(AutoXXXLayout)中的每一个View的LayoutParams,取出之前记录了自定义属性和属性值的AutoLayoutInfo,然后将里面的每一个属性作用到View上。这样就完成了View属性的更改。
该库提供两个自定义属性layout_auto_basewidth, layout_auto_baseheight。由于手机屏幕的碎片化(Android手机你懂的),比如想让一个ImageView的宽高一致时,写宽20px,高20px,经过百分比换算屏幕大小,很可能就不一致了。所以需要一个单位基准,比如宽20px,高20px,layout_auto_basewidth=height,那么高度就会以width为基准了,其换算公式是(edit_height/design_width) * screen_width。如此,这个ImageView的宽高就一样大小了,正方形。
AutoAttr这个类作者用来封装每一个属性值的信息,一般就是多少px,是哪个方向(宽,高)为基准。如果我们在xml里没有指定某属性的基准,就会使用该属性的默认基准。默认基准是,竖直方向上以高为基准,水平方向以宽为基准。即:marginTop以height为基准,marginLeft以width为基准,textSize以height为基准,其他的同上。
AutoAttr的apply()方法代码如下:
public void apply(View view) {
boolean log = view.getTag() != null && view.getTag().toString().equals("auto");
if(log) {
L.e(" pxVal = " + this.pxVal + " ," + this.getClass().getSimpleName());
}
int val;
if(this.useDefault()) {
val = this.defaultBaseWidth()?this.getPercentWidthSize():this.getPercentHeightSize();
if(log) {
L.e(" useDefault val= " + val);
}
} else if(this.baseWidth()) {
val = this.getPercentWidthSize();
if(log) {
L.e(" baseWidth val= " + val);
}
} else {
val = this.getPercentHeightSize();
if(log) {
L.e(" baseHeight val= " + val);
}
}
val = Math.max(val, 1);
this.execute(view, val);
}
以上是我对AndroidAutoLayout的理解,如果错了,还请指正,谢谢。
热门评论
不错