Android中ViewStub的使用与分析

文章目录

  • 1.ViewStub的简单使用
    • 1.1 简单说明
    • 1.2 简单示例
  • 2.结合源码分析问题
    • 2.1 第二次调用inflate()加载会抛出空指针异常
      • 2.2 第二次通过调用setVisibility()加载也会抛出空指针异常
      • 2.3 监听事件的设置

1.ViewStub的简单使用

1.1 简单说明

  • ViewStub实质上是一个宽高都为0的不可见 View。
  • 通过延迟加载布局的方式优化布局,提升渲染性能。

1.2 简单示例

  • activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/show_stub_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="showStubView"
        android:textAllCaps="false"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ViewStub
            android:id="@+id/view_stub"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout="@layout/lazy" />
    FrameLayout>


androidx.constraintlayout.widget.ConstraintLayout>
  • 需要延迟加载的布局:lazy.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Lazy Inflation"
        android:textAllCaps="false"
        android:textColor="#0000ff"
        android:textSize="50dp" />

FrameLayout>
  • 延迟加载代码:
    • 通过调用inflate()加载。
    • 通过设置visibility为 View.VISIBLE加载。
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        show_stub_button.setOnClickListener {
       //   view_stub.visibility = View.VISIBLE       //方式一
            view_stub.inflate()						  //方式二
        }
    }
}

2.结合源码分析问题

2.1 第二次调用inflate()加载会抛出空指针异常

  • 原因概述:当ViewStub完成延迟加载完后会被移除,因此再次调用inflate()方法时ViewStub已经不存在,所以会抛出空指针异常。

原码分析

  • infalte()方法
public View inflate() {
    //获取到父控件
    final ViewParent viewParent = getParent();
    //父控件为空或者不是ViewGroup会抛出异常
    if (viewParent != null && viewParent instanceof ViewGroup) {
        //layout资源(lazy.xml)不能为空
        if (mLayoutResource != 0) {
            //获取父View
            final ViewGroup parent = (ViewGroup) viewParent;
            //将layout资源(lazy.xml)填充到父View中。(下面会展开看这个方法)
            final View view = inflateViewNoAdd(parent);
            //用layout资源(lazy)替代掉自己(StubView)。(下面会展开看这个方法)
            replaceSelfWithView(view, parent);
            mInflatedViewRef = new WeakReference<>(view);//记录layout资源(lazy),不用关注
            // 如果设置了监听事件,回调监听
            if (mInflateListener != null) {
                mInflateListener.onInflate(this, view);
            }
            // 返回layout资源(lazy)
            return view;
        } else {
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
}
  • inflateViewNoAdd(ViewGroup parent)方法
private View inflateViewNoAdd(ViewGroup parent) {
    final LayoutInflater factory;
    if (mInflater != null) {
        factory = mInflater;
    } else {
        factory = LayoutInflater.from(mContext);
    }
    //将layout资源(lazy.xml)填充到父View中,并返回layout资源(lazy)
    final View view = factory.inflate(mLayoutResource, parent, false);
    //如果ViewStub设置了InflatedId,那么将layout资源(lazy)的ID设置为ViewStub的InflatedId
    //!!!注意这里是InflatedId不是Id
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
    return view;
}
  • replaceSelfWithView(View view, ViewGroup parent)方法
private void replaceSelfWithView(View view, ViewGroup parent) {
    // 根据对象找到viewStub的index
    final int index = parent.indexOfChild(this);
    // 根据index找到在父view中移除viewStub
    parent.removeViewInLayout(this);
    // 获取本身的LayoutParams
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    // 如果LayoutParams不为空,viewStub将自身的LayoutParams给layout资源(lazy)后,并将layout资源(lazy)添加到父view。
    if (layoutParams != null) {
        parent.addView(view, index, layoutParams);
    } else {
        // 如果LayoutParams为空,直接将layout资源(lazy)添加到父view
        parent.addView(view, index);
    }
}

// 一个细节:这里将layout资源(lazy)添加到父view时,使用的index是原来viewStub的index。充分的利用了资源!!
  • ViewStub.setVisibility(int visibility) 方法:
public void setVisibility(int visibility) {
    // 在inflate()方法中,将mInflatedViewRef指向了layout资源(lazy)
    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        // layout资源(lazy)为空抛出异常,不为空,将其设置为visible。
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        //mInflatedViewRef为空,说明inflate()没有调用过。
        //如果设置visibility为VISIBLE或者INVISIBLE则调用inflate()。Tip:也就是说如果设置为GONE,并不会调用inflate()。
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}

2.2 第二次通过调用setVisibility()加载也会抛出空指针异常

  • 许多blog中提到,通过多次调用setVisibility()来执行延迟加载,不会抛出异常,除第一次操作外,后续操作相当于对layout资源(lazy)进行设置。但是实际操作中仍然会抛出空指针异常!

  • 原因分析

  • 1.混淆了InflatedId和Id概念

    //inflateViewNoAdd(ViewGroup parent)方法节选
    
    //如果ViewStub设置了InflatedId,那么将layout资源(lazy)的ID设置为ViewStub的InflatedId
    //!!!注意这里是InflatedId不是Id
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
    
    • 这里给layout资源(lazy)设置的Id,是ViewStub的InflatedId,不是ViewStub的ID
    • 举例说明InflatedId的使用:
    <FrameLayout
        android:id="@+id/view_stub_frame"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <ViewStub
            android:id="@+id/view_stub"
            android:inflatedId="@id/view_stub_frame"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout="@layout/lazy" />
    </FrameLayout>
    
  • 2.操作的时机

    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        // layout资源(lazy)为空抛出异常,不为空,将其设置为visible。
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } 
    
    • mInflatedViewRef会指向layout资源(lazy)。在ViewStub存在时调用,后续调用setVisibility()不会抛出异常。
    • 但是要注意的是mInflatedViewRef是一个弱引用,ViewStub在执行完延迟加载后,ViewStub对象将会被GC回收。当ViewStub被回收后,再次去调用setVisibility()方法,就会抛出空指针异常!
  • 总结

    • 因此不管是用过setVisibility()方法,还是通过inflate()方法进行延迟加载,多次调用都是不安全的!因此,延迟加载前,进行判空是必要的!

    • 安全的调用方法

      view_stub?.visibility = View.VISIBLE     //方式一
      view_stub?.inflate()                     //方式二
      

2.3 监听事件的设置

  • 代码:

    show_stub_button.setOnClickListener {
        view_stub?.visibility = View.VISIBLE     //方式一
        view_stub?.inflate()                     //方式二
    }
    view_stub.setOnInflateListener { stub, inflated -> XXXXXXXXX}
    
    • 当ViewStub执行延迟加载后会回调上述函数,stub是ViewStub本身,inflated是延迟加载的实例(即layout资源(lazy))。
  • Tip:也可以通过该回调,设置标志位,来判断是否执行过延迟加载。

你可能感兴趣的:(Android学习笔记)