更新时间:2022-01-05
Android 30
文章目录
ViewStub是进行占位的。
ViewStub 是一个宽高都为 0 的 View,并且默认不可见,也不会进行绘制操作,仅仅占位。
用于在运行时按需懒加载资源,在代码中调用 inflate() 或者 setVisibility 为 View.VISIBLE 时,才可见。
当 viewstub 被 inflate 到 parent 时,viewstub 就被 remove 掉了,即当前 view hierarchy 中不再存在 viewstub,而是使用对应的 layout 视图替代。
源码分析
1)构造方法
在xml文件中,引用 ViewStub 会走这个构造方法
会获取viewStub属性layout引用的布局文件
同时设置 ViewStub 为 Gone
也会设置不参与绘制
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);
// 获取 InflatedId
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
// 获取设置layout的布局文件
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
// 获取引用 ViewStub 设置的 id
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
// 资源回收
a.recycle();
// 设置当前 ViewStub 隐藏
setVisibility(GONE);
// 设置自己不参与绘制
setWillNotDraw(true);
}
2)onMeasure 和 draw
onMeasure 设置宽高为 0
draw 为空,未做处理
也就是不会进行绘制了
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置宽高为 0
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
3)setVisibility
首次执行时,mInflatedViewRef 为 空,执行else ,然后 visibility 为 Gone,也不会执行 inflate。
mInflatedViewRef 会在 inflate 时进行赋值。
当调用setVisibility设置 View.VISIBLE时,会执行inflate方法,同时设置mInflatedViewRef ;
当再次调用setVisibility设置 View.VISIBLE时,mInflatedViewRef不为空,view也不为空,就会调用view的setVisibility
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
// 首次调用时,mInflatedViewRef 为空的,会走else
// 当调用 inflate 时,才会给 mInflatedViewRef 赋值
if (mInflatedViewRef != null) {
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
throw new IllegalStateException(
"setVisibility called on un-referenced view");
}
} else {
// 构造方法调用setVisibility设置Gone,会执行else,但不会执行inflate
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();
}
}
}
4)inflate
会进行判断 viewStub是否有父布局,并且为 viewGroup,否则就抛出异常;
将 viewStub 属性layout引用的资源文件,替换 viewStub。
当第一次调用inflate时,并不会抛出异常,当再次调用inflate时,会异常,因为上次执行inflate方法时,已经将viewStub移除了,获取getparent为空,抛出异常!
public View inflate() {
// 获取父控件
final ViewParent viewParent = getParent();
// 判断父控件 是否为空 是否为ViewGroup
if (viewParent != null && viewParent instanceof ViewGroup) {
// mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
// mLayoutResource 是在前面构造方法中赋值的,为layout属性设置的 资源文件
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
// 将 layout属性设置的 资源文件,渲染到 parent
// 并将 ViewStub 引用的资源文件 作为 View 返回
// 同时给该View设置了id为mInflatedId ,mInflatedId是在构造函数中赋值的
//mInflatedId=a.getResourceId(R.styleable.ViewStub_inflatedId,NO_ID);
final View view = inflateViewNoAdd(parent);
// 将资源文件 替代 父控件中的viewStub的位置
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");
}
// 当父控件为空 或者 不是ViewGroup时,就会抛出异常
// 当执行inflate中replaceSelfWithView后,就会将 viewStub给替换了
// 父控件中也就没有viewStub了
// 再次执行inflate,获取到的viewParent就为空,就会抛出此异常
} else {
throw new IllegalStateException(
"ViewStub must have a non-null ViewGroup viewParent");
}
}
5)replaceSelfWithView
将父控件中的 viewStub进行删除,同时,将 资源文件转化成的View,替换 ViewStub
private void replaceSelfWithView(View view, ViewGroup parent) {
// 获取 viewStub 在父控件中的位置
final int index = parent.indexOfChild(this);
// 从父控件中移除 viewStub
parent.removeViewInLayout(this);
// 获取 viewStub 的 LayoutParams,不是layout资源文件的
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 把 viewStub 引用的 layout 资源文件,添加到父布局中
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
代码演示
1)my_viewstub_layout.xml
通过在 activity_main.xml 中使用 ViewStub,在layout属性中引用资源文件my_viewstub_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/test_viewstub_tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test viewStub textView1"
/>
</LinearLayout>
2)activity_main.xml
使用 ViewStub,引用资源文件 my_viewstub_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<ViewStub
android:id="@+id/test_viewstub"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout="@layout/my_viewstub_layout"
/>
</LinearLayout>
3)直接运行,看层级结构 及 效果
自然也不会有展示内容,如下效果:
4)调用 inflate 或者 setVisibility,查看层级结构及效果
在MainActivity中调用inflate
public class MainActivity extends AppCompatActivity {
ViewStub viewStub1;
View inflate;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 根据 id 获取 ViewStub
viewStub1 = findViewById(R.id.test_viewstub);
// inflate只能调用一次,否则会异常
if (inflate == null) {
inflate = viewStub1.inflate();
}
// 设置为可见
// viewStub1.setVisibility(View.VISIBLE);
}
}
从中可以看出,ViewStub替换为了 引用的资源文件的层级:
效果如下:
5)给 viewStub 引用的资源文件的textView设置color
public class MainActivity extends AppCompatActivity {
ViewStub viewStub1;
View inflate;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 根据 id 获取 ViewStub
viewStub1 = findViewById(R.id.test_viewstub);
// inflate只能调用一次,否则会异常
if (inflate == null) {
inflate = viewStub1.inflate();
}
TextView test_viewstub_tv1 = inflate.findViewById(
R.id.test_viewstub_tv1);
test_viewstub_tv1.setTextColor(Color.BLUE);
}
}
其它
当调用第二次 viewStub1.inflate() 时,就会抛出异常?
从源码中可以看到,当第一次执行inflate的时候,会调用 replaceSelfWithView(view, parent) 方法,它会将 viewStub从父控件中移除,使用 viewStub的属性layout引用的资源文件进行替代,也就是 父控件中已经没有 viewStub了,再次调用inflate,getParent()就会为空,然后抛出异常。
参考文档:
Android布局层次结构查看工具-Layout Inspector介绍:https://blog.csdn.net/cadi2011/article/details/85212762
好记性不如烂笔头!