Android viewstub

更新时间: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

好记性不如烂笔头!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值