🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] 写项目的过程中发现,在需要根据适当条件进行相应 UI 展示时,代码中充斥了 setVisibility 相关的代码,相当混乱。我们可以使用 ViewStub 来简化相应的逻辑,并且 ViewStub 大小为 0,运行时才进行懒加载,所以性能上也有一定优势。 # ViewStub是什么 1、ViewStub 是一个看不见的,没有大小,不占布局位置的 View,可以用来懒加载布局。 2、当 ViewStub 变得可见或 inflate() 的时候,布局就会被加载(替换 ViewStub)。因此,ViewStub 一直存在于视图层次结构中直到调用了 setVisibility() 或 inflate()。 3、在 ViewStub 加载完成后就会被移除,它所占用的空间就会被新的布局替换。 # 简单使用 ViewStub 的使用很简单,在布局文件中像引入其他控件一样引入 ViewStub: ```xml <ViewStub android:id="@+id/stub" android:inflatedId="@+id/subTree" android:layout="@layout/mySubTree" android:layout_width="120dip" android:layout_height="40dip" /> ``` 需要展示 ViewStub 所引用的视图时,通过 id 获取到 ViewStub: ```java ViewStub stub = (ViewStub) findViewById(R.id.stub); View inflated = stub.inflate(); // stub.setVisibility(View.VISIBLE); ``` 除了调用 inflate 方法,还可以调用 setVisible 方法来展示 ViewStub 所引用的 View。下面,我们来看下 ViewStub 的源码。 为统一理解,下面使用 `延迟加载 View` 来指代 ViewStub 所引用的 View。 # 源码分析 ViewStub 这个类的代码加上注释也才三百多行,算是很简单的源码类了。先看下成员变量: ```java /** * 延迟加载 View 的 id */ private int mInflatedId; /** * 延迟加载 View 的布局资源 */ private int mLayoutResource; /** * 延迟加载 View 对象的弱引用 */ private WeakReference<View> mInflatedViewRef; /** * 布局加载器 */ private LayoutInflater mInflater; /** * 延迟加载 View 加载成功回调接口 */ private OnInflateListener mInflateListener; ``` 可以通过 ViewStub 的 setOnInflateListener(OnInflateListener inflateListener) 方法设定监听器,在视图加载成功后执行相应的操作,其中加载成功回调接口如下: ```java public static interface OnInflateListener { /** * @param stub 把加载延迟加载 View 的那个 ViewStub * @param inflated 加载出来的那个延迟加载 View */ void onInflate(ViewStub stub, View inflated); } ``` ## inflate 方法 通常调用 inflate 方法来延迟加载视图,来看看其源码: ```java public View inflate() { final ViewParent viewParent = getParent(); if (viewParent != null && viewParent instanceof ViewGroup) { if (mLayoutResource != 0) { final ViewGroup parent = (ViewGroup) viewParent; // 延迟加载 View inflate 出来 final View view = inflateViewNoAdd(parent); // 把 ViewStub 自身从父布局视图树中删除,延迟加载 View 加入父布局视图树中 replaceSelfWithView(view, parent); mInflatedViewRef = new WeakReference<>(view); if (mInflateListener != null) { mInflateListener.onInflate(this, 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 的父布局并强转为 ViewGroup,然后根据父布局的约束将延迟加载 View inflate 出来,接着把 ViewStub 自身从父布局视图树中删除,延迟加载 View 加入父布局视图树中。 延迟加载 View 加载成功后,通过弱引用对象保存该视图对象,并回调加载成功回调接口。至此,inflate 方法的工作也就完成了。 其中 inflateViewNoAdd 和 replaceSelfWithView 方法的源码如下,都是很基础的代码: ```java private View inflateViewNoAdd(ViewGroup parent) { final LayoutInflater factory; if (mInflater != null) { factory = mInflater; } else { factory = LayoutInflater.from(mContext); } final View view = factory.inflate(mLayoutResource, parent, false); if (mInflatedId != NO_ID) { view.setId(mInflatedId); } return view; } private void replaceSelfWithView(View view, ViewGroup parent) { final int index = parent.indexOfChild(this); parent.removeViewInLayout(this); final ViewGroup.LayoutParams layoutParams = getLayoutParams(); if (layoutParams != null) { parent.addView(view, index, layoutParams); } else { parent.addView(view, index); } } ``` ## setVisibility 方法 通过 ViewStub 的 inflate 方法,我们可以顺利的把想要延迟加载的 View 加载出来了,然而实际的业务逻辑并不会这么简单,有时可能加载出来后需要再次隐藏,隐藏后还要再次加载。或者当 inflate 方法多次调用时,就会报错了: ```plain java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent ``` 通过刚才的源码可以看到,在调用 inflate 方法时,viewParent 为 null 了,为什么呢?原来我们在第一次调用 inflate 方法时,执行到 replaceSelfWithView 方法时,就把 ViewStub 从父布局中移除,然后把延迟加载 View 加入到父布局中了。所以现在再次调用 ViewStub 的 getParent 方法当然为 null 了。 那么延迟加载 View 是否可以多次显示、隐藏呢,当然是可以的,接下来我们看看 ViewStub 的 setVisibility 方法。 ```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) { inflate(); } } } ``` 逻辑很简单:首先判断延迟加载 View 的弱引用对象是否为空,不为空直接获取该 View,并设置相应的可见性;弱引用对象为空的话,如果可见性设置为 VISIBLE 或 INVISIBLE 时,直接调用 inflate 方法进行加载。 需要注意两点: * 代码中不调用 inflate 方法,直接调用 setVisibility(View.INVISIBLE) 时,会将延迟加载 View 加载显示出来的 * ViewStub 在调用一次 inflate 方法后,延迟加载 View 的弱引用对象就不为空了,除非被垃圾回收器扫描到进行回收了。 # 绘制流程方法 ```java public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { //... setVisibility(GONE); setWillNotDraw(true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(0, 0); } @Override public void draw(Canvas canvas) { } @Override protected void dispatchDraw(Canvas canvas) { } ``` 可以看到,ViewStub在构造方法中调用了setWillNotDraw(true),会在后续过程中进行性能优化,略过绘制过程。在onMeasure方法中直接设置测量宽高为0,重写了draw方法、dispatchDraw方法,方法内容为空。所以ViewStub 是一个看不见的,没有大小,不占布局位置的 View。 # ViewStub 与 ListView 结合使用的问题 在 ListView 的 item 中使用 ViewStub 根据条件动态加载视图,遇到视图混乱问题,ListView 代码片段如下: ```java public View getView(int position, View convertView, ViewGroup parent) { /* if (convertView == null) { ... } else { ... } ... */ holder.mViewStub.setOnInflateListener(new ViewStub.OnInflateListener() { @Override public void onInflate(ViewStub stub, View inflated) { inflated.findViewById(R.id.tv_unapply) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // unapply(mEntity.getJobId); } }); inflated.findViewById(R.id.tv_contact_company) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // contactCompany(); } }); } }); holder.mApplyViewStub.setVisibility(View.VISIBLE); } ``` 上面源码分析时可知,ViewStub 的 setVisibility 方法在 mInflatedViewRef 不为空时,直接对延迟加载 View 进行可见性设定,并没有执行 inflate 方法,所以我们上面设置的 onInflateListener 就没有回调到了。 所以我们在 ViewHolder 中将 ViewStub 进行缓存后,getView 方法中再次取出时就不会调用 inflate 方法了,所以会出现 onInflateListener 方法回调异常。 # 总结 ViewStub 源码的几个关键成员变量和方法以及介绍完了。可以看到,ViewStub 的延迟加载特性,在提升视图性能的同时,还可以使业务逻辑更清晰,大量减少 View 的 setVisibility 相关代码。 使用时,只需要一次性加载的可以直接调用 inflate 方法进行加载。需要多次调用修改可见性的,可以调用 ViewStub 的 setVisibility 方法,同时可以通过设置监听器在加载完成后执行相应的操作。