闲来无事,把ViewStub
源码看了一遍,之前只是知道大概原理,从来没有把源码仔细看过,代码很少一会就能看完,只有300行,而且有一半是注释,读完有些收获,做个笔记。
环境
- Android Studio 1.5 Preview 2
- Android SDK 23
解析
先来看看ViewStub
的成员变量。
private int mInflatedId; // 保存xml指定的inflatedId
private int mLayoutResource; // 保存xml指定的layout
private WeakReference<View> mInflatedViewRef; // 用弱引用持有inflate之后的原始View
private LayoutInflater mInflater; // 这个不多说
private OnInflateListener mInflateListener; // inflate完成的监听器
然后是构造方法。
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ViewStub, defStyleAttr, defStyleRes);
// 获取xml中指定inflatedId属性,赋值给mInflatedId
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
// 获取xml中指定layout属性,赋值给layout
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
// 获取xml中指定的id属性
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
// 设置为不可见
setVisibility(GONE);
// 标记为不需要绘制,提高性能
setWillNotDraw(true);
}
这里面有三个地方要说明一下。
-
mID
在ViewStub
里面并没有定义,mID
是View
里面定义的,而且mID
访问修饰符是default
,也就是同包下可以访问,而这个mID
就是我们经常使用的View
的id
,一般在xml中指定,也可以通过View
的setId
和getId
进行操作。然而ViewStub
并没有调用setId
方法设置,而是很嚣张的直接给mID
赋值,这是因为ViewStub
和View
在同一个包里,所以比较牛逼,如果我们自己写自定义View
可干不了这事,当然可以通过反射做。 -
setVisibility()
方法在ViewStub
中被重写,这个方法比较关键,后面说。 -
setWillNotDraw()
这个方法View
中默认是false
,而ViewGroup
中默认是true
,如果为true
那么View
的onDraw()
不会被调用可以提高性能。
我再来看看ViewStub
重写一些View
的绘制过程中的方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 不管xml里面设置的宽高,全部设置为0
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
}
我们看到onMeasure
里面把宽高都设置为了0,而draw
相关方法都是空实现,这也说明了ViewStub
性能好的原因,它自己相当于不存在。
OK,我们知道如果想让ViewStub
中的真实的View
显示出来有两种方式。
- 调用
setVisibility()
设置为VISIBLE
或者INVISIBLE
- 调用
inflate()
方法
我们先看第一种方法。
@Override
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
// 如果已经inflate过,那么mInflatedViewRef不为null
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
// 这里是因为弱引用里面的View被回收了,那说明其他地方已经没有对View的引用了
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
// 如果之前没有inflate过,并且需要显示,那调用inflate()方法
inflate();
}
}
}
我们发现setVisibility()
最终也是调用了inflate()
方法,以为第一次mInflatedViewRef
肯定是null
,那继续看inflate()
方法的现实。
public View inflate() {
// 获取ViewStub的parent,ViewStub只是个占位符,最用要把inflate出来的View加到parent里面
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
// mLayoutResource就是xml指定的layout属性
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
// 使用LayoutInflater把xml中指定的layout加载出来
final View view = factory.inflate(mLayoutResource, parent,
false);
if (mInflatedId != NO_ID) {
// 给原始的View设置id,就是xml中指定的inflatedId
view.setId(mInflatedId);
}
// 找到ViewStub在parent里面的位置
final int index = parent.indexOfChild(this);
// 然后把ViewStub从parent里面移除,ViewStub已经没有存在的意义了
parent.removeViewInLayout(this);
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 下面就是把原始View加到parent中
if (layoutParams != null) {
// 把ViewStub上指定的layout参数给原始View
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
// 创建弱引用持有View
mInflatedViewRef = new WeakReference<View>(view);
// 触发infalte完成的监听器,listener可以通过set方法设置
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
// 返回原始的View
return view;
} else {
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
代码逻辑比较简单,这里我们注意一下ViewStub
被移除的过程,调用的是parent.removeViewInLayout(View)
方法,而我们平时可能会用removeView(View)
比较多,那么这两个方法有什么区别的,直接看源码就能明白。
public void removeView(View view) {
if (removeViewInternal(view)) {
requestLayout();
invalidate(true);
}
}
public void removeViewInLayout(View view) {
removeViewInternal(view);
}
很明显,removeView(View)
会引发重新layout
的过程,也就是如果View
已经显示在屏幕上了,如果我们想要移除,那就调用removeView(View)
从而刷新页面,如果View
没有显示在屏幕上,调用removeViewInLayout(View)
性能会好,避免了不必要的重绘,而ViewStub
中就是一个很好的例子。
到此ViewStub
的源码就分析完了。