[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 方法,同时可以通过设置监听器在加载完成后执行相应的操作。
- 导读
- Java知识
- Java基本程序设计结构
- 【基础知识】Java基础
- 【源码分析】Okio
- 【源码分析】深入理解i++和++i
- 【专题分析】JVM与GC
- 【面试清单】Java基本程序设计结构
- 对象与类
- 【基础知识】对象与类
- 【专题分析】Java类加载过程
- 【面试清单】对象与类
- 泛型
- 【基础知识】泛型
- 【面试清单】泛型
- 集合
- 【基础知识】集合
- 【源码分析】SparseArray
- 【面试清单】集合
- 多线程
- 【基础知识】多线程
- 【源码分析】ThreadPoolExecutor源码分析
- 【专题分析】volatile关键字
- 【面试清单】多线程
- Java新特性
- 【专题分析】Lambda表达式
- 【专题分析】注解
- 【面试清单】Java新特性
- Effective Java笔记
- Android知识
- Activity
- 【基础知识】Activity
- 【专题分析】运行时权限
- 【专题分析】使用Intent打开三方应用
- 【源码分析】Activity的工作过程
- 【面试清单】Activity
- 架构组件
- 【专题分析】MVC、MVP与MVVM
- 【专题分析】数据绑定
- 【面试清单】架构组件
- 界面
- 【专题分析】自定义View
- 【专题分析】ImageView的ScaleType属性
- 【专题分析】ConstraintLayout 使用
- 【专题分析】搞懂点九图
- 【专题分析】Adapter
- 【源码分析】LayoutInflater
- 【源码分析】ViewStub
- 【源码分析】View三大流程
- 【源码分析】触摸事件分发机制
- 【源码分析】按键事件分发机制
- 【源码分析】Android窗口机制
- 【面试清单】界面
- 动画和过渡
- 【基础知识】动画和过渡
- 【面试清单】动画和过渡
- 图片和图形
- 【专题分析】图片加载
- 【面试清单】图片和图形
- 后台任务
- 应用数据和文件
- 基于网络的内容
- 多线程与多进程
- 【基础知识】多线程与多进程
- 【源码分析】Handler
- 【源码分析】AsyncTask
- 【专题分析】Service
- 【源码分析】Parcelable
- 【专题分析】Binder
- 【源码分析】Messenger
- 【面试清单】多线程与多进程
- 应用优化
- 【专题分析】布局优化
- 【专题分析】绘制优化
- 【专题分析】内存优化
- 【专题分析】启动优化
- 【专题分析】电池优化
- 【专题分析】包大小优化
- 【面试清单】应用优化
- Android新特性
- 【专题分析】状态栏、ActionBar和导航栏
- 【专题分析】应用图标、通知栏适配
- 【专题分析】Android新版本重要变更
- 【专题分析】唯一标识符的最佳做法
- 开源库源码分析
- 【源码分析】BaseRecyclerViewAdapterHelper
- 【源码分析】ButterKnife
- 【源码分析】Dagger2
- 【源码分析】EventBus3(一)
- 【源码分析】EventBus3(二)
- 【源码分析】Glide
- 【源码分析】OkHttp
- 【源码分析】Retrofit
- 其他知识
- Flutter
- 原生开发与跨平台开发
- 整体归纳
- 状态及状态管理
- 零碎知识点
- 添加Flutter到现有应用
- Git知识
- Git命令
- .gitignore文件
- 设计模式
- 创建型模式
- 结构型模式
- 行为型模式
- RxJava
- 基础
- Linux知识
- 环境变量
- Linux命令
- ADB命令
- 算法
- 常见数据结构及实现
- 数组
- 排序算法
- 链表
- 二叉树
- 栈和队列
- 算法时间复杂度
- 常见算法思想
- 其他技术
- 正则表达式
- 编码格式
- HTTP与HTTPS
- 【面试清单】其他知识
- 开发归纳
- Android零碎问题
- 其他零碎问题
- 开发思路