ViewStub源码剖析
之前在ViewStub懒加载一文中记录了如何使用ViewStub懒加载布局,当时没有继续对ViewStub深入研究。现在回头看看ViewStub源码,只有三百多行,属实需要细细品一下。
nukc在看了ViewStub源码之后突发灵感,写了StateView库,吸收了ViewStub的一些特性,内存占用少。
从ViewStub的构造函数开始看,ViewStub继承自View,所以至少有这四个标准构造函数:
public ViewStub(Context context) {
this(context, 0);
}
public ViewStub(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
final TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.ViewStub, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.ViewStub, attrs, a, defStyleAttr,defStyleRes);
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
setVisibility(GONE);
setWillNotDraw(true);
}
除了以上的四个构造函数,ViewStub中还增加了一个构造函数ViewStub(Context context, int layoutResource):其中参数用于指定ViewStub加载的布局资源layout。
/**
* Creates a new ViewStub with the specified layout resource.
*
* @param context The application's environment.
* @param layoutResource The reference to a layout resource that will be inflated.
*/
public ViewStub(Context context, @LayoutRes int layoutResource) {
this(context, null);
mLayoutResource = layoutResource;
}
上面的几个构造方法,最后都会调用下面带4个参数的构造方法ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)。在该构造方法里做了几件重要的事情:
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewStub, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.ViewStub, attrs, a, defStyleAttr, defStyleRes);
//加载的布局id,默认NO_ID
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
//加载的布局资源id
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
//当前ViewStub控件的id
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle(); //回收TypedArray
//设置当前ViewStub可见性为GONE
setVisibility(GONE);
//不绘制当前ViewStub
setWillNotDraw(true);
}
首先,初始化mInflatedId、mLayoutResource、mID,分别从ViewStub的三个属性获取:inflatedId、layout、id。如何定义View的属性另见自定义View属性一文。ViewStub的几个属性定义如下:
<!-- A {@link android.view.ViewStub} lets you lazily include other XML layouts inside your application at runtime. -->
<declare-styleable name="ViewStub">
<!-- Supply an identifier name for this view. -->
<attr name="id" />
<!-- Supply an identifier for the layout resource to inflate when the ViewStub
becomes visible or when forced to do so. The layout resource must be a
valid reference to a layout. -->
<attr name="layout" format="reference" />
<!-- Overrides the id of the inflated View with this value. -->
<attr name="inflatedId" format="reference" />
</declare-styleable>
这三个属性就是在布局中使用ViewStub时所用到的:①inflatedId就是加载进来的布局id,如果需要获取加载的布局,就要用这个inflatedId,原来的id会被替代(如果有)。②而layout就是要加载进来的布局。③id就是当前ViewStub的id。
<ViewStub
android:id="@+id/view_stub"
android:inflatedId="@+id/inflate_layout"
android:layout="@layout/include_layout" />
其次,在构造方法的最后执行setVisibility(GONE)设置当前ViewStub可见性为GONE,和调用setWillNotDraw(true)方法设置不绘制当前ViewStub。并且还重写了下面这三个方法:onMeasure()、draw()、dispatchDraw(),最大程度降低ViewStub在布局绘制阶段对性能的影响。只有在需要的时候才去真正的加载、绘制布局。
//当前ViewStub大小始终为0
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
}
当需要去加载布局展示的时候,只需调用ViewStub的inflate()方法。该方法在ViewStub中的定义如下:
/**
* Inflates the layout resource identified by {@link #getLayoutResource()}
* and replaces this StubbedView in its parent by the inflated layout resource.
*
* @return The inflated layout resource.
*
*/
public View inflate() {
//拿到当前ViewStub所在的父容器
final ViewParent viewParent = getParent();
//
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
//ViewParent通常为ViewGroup
final ViewGroup parent = (ViewGroup) viewParent;
//从方法名就可以看出,仅加载布局,这一步先不将加载的布局添加到父容器中
final View view = inflateViewNoAdd(parent);
//而是在这里,用加载出来的布局 替换 当前ViewStub
replaceSelfWithView(view, parent);
//通过弱引用持有加载出来的布局
mInflatedViewRef = new WeakReference<>(view);
//回调OnInflateListener监听器,如果有的话
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
//返回懒加载出来的布局对象
return view;
} else {
//使用ViewStub必须指定要懒加载的布局资源id,也就是layout属性
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
//如果ViewStub已经加载了布局,那么通过getParent()获取到的viewParent就为null
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
在inflate()方法中通过inflateViewNoAdd(ViewGroup parent)先去加载布局:
private View inflateViewNoAdd(ViewGroup parent) {
//使用LayoutInflater加载布局
final LayoutInflater factory;
//可以用setLayoutInflater(LayoutInflater inflater)设置mInflater
if (mInflater != null) {
factory = mInflater;
} else {
//或者使用默认的LayoutInflater
factory = LayoutInflater.from(mContext);
}
//加载布局,注意attachToRoot参数为false
final View view = factory.inflate(mLayoutResource, parent, false);
//注意这里,如果给ViewStub提供了inflatedId属性值,就会将其设置给加载出来的布局
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
return view;
}
加载到布局后,紧接调用replaceSelfWithView(View view, ViewGroup parent)方法,ViewStub用加载到的布局替换自身:
private void replaceSelfWithView(View view, ViewGroup parent) {
//ViewStub在父容器中的索引位置
final int index = parent.indexOfChild(this);
//从父容器中移除自身
parent.removeViewInLayout(this);
//细节的一步,将自身的LayoutParams设置给加载到的布局
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
//如果没有,则直接添加到自己的父容器里,ViewGroup会为其设置默认的LayoutParams
parent.addView(view, index);
}
}
ViewStub的inflate()方法只能调用一次,调用多次会抛出异常。ViewStub重写了View的setVisibility(int visibility)方法,通过该方法设置可见性间接的去懒加载布局。要注意弱引用关联的对象如果被回收,再调用此方法会抛出异常。关于弱引用另见《Java四种引用类型》一文。
public void setVisibility(int visibility) {
//弱引用如果已经初始化
if (mInflatedViewRef != null) {
//从弱引用中拿懒加载的布局对象
View view = mInflatedViewRef.get();
if (view != null) {
//设置其可见性
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
//否则设置可见性
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
//当设置的可见性值为VISIBLE或INVISIBLE的时候,调用inflate()方法去加载布局。
inflate();
}
}
}
这里补充一下,ViewStub中定义了一个接口OnInflateListener。前面inflate()方法中也看到,当ViewStub成功加载布局的时候会回调该接口的onInflate(ViewStub stub, View inflated)方法(业务可以用来回调监听布局的加载):
/**
* Listener used to receive a notification after a ViewStub has successfully
* inflated its layout resource.
*
* @see android.view.ViewStub#setOnInflateListener(android.view.ViewStub.OnInflateListener)
*/
public static interface OnInflateListener {
/**
* Invoked after a ViewStub successfully inflated its layout resource.
* This method is invoked after the inflated view was added to the
* hierarchy but before the layout pass.
*
* @param stub The ViewStub that initiated the inflation.
* @param inflated The inflated View.
*/
void onInflate(ViewStub stub, View inflated);
}
ViewStub提供setOnInflateListener(OnInflateListener inflateListener)方法用于设置一个布局加载监听器给mInflateListener。
/**
* Specifies the inflate listener to be notified after this ViewStub successfully
* inflated its layout resource.
*
* @param inflateListener The OnInflateListener to notify of successful inflation.
*
* @see android.view.ViewStub.OnInflateListener
*/
public void setOnInflateListener(OnInflateListener inflateListener) {
mInflateListener = inflateListener;
}
相关资料:
github/LittleFriendsGroup/AndroidSdkSourceAnalysis/ViewStub 源码解析
https://github.com/nukc/StateView
自定义View属性