美文网首页Android开发Android
ViewStub你真的了解吗

ViewStub你真的了解吗

作者: 杨充211 | 来源:发表于2019-11-21 12:52 被阅读0次

    目录介绍

    • 01.什么是ViewStub
    • 02.ViewStub构造方法
    • 03.inflate()方法解析
    • 04.WeakReference使用
    • 05.ViewStub为何无大小
    • 06.ViewStub为何不绘制
    • 07.可以多次inflate()吗
    • 08.ViewStub不支持merge
    • 09.ViewStub使用场景
    • 10.ViewStub总结分析

    好消息

    • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计N篇[近100万字,陆续搬到网上],转载请注明出处,谢谢!
    • 链接地址:https://github.com/yangchong211/YCBlogs
    • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

    01.什么是ViewStub

    • ViewStub 是一个看不见的,没有大小,不占布局位置的 View,可以用来懒加载布局。
    • 当 ViewStub 变得可见或 inflate() 的时候,布局就会被加载(替换 ViewStub)。因此,ViewStub 一直存在于视图层次结构中直到调用了 setVisibility(int)inflate()
    • 在 ViewStub 加载完成后就会被移除,它所占用的空间就会被新的布局替换。

    02.ViewStub构造方法

    • 先来看看构造方法:
      public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context);
      
          final TypedArray a = context.obtainStyledAttributes(attrs,
                  R.styleable.ViewStub, defStyleAttr, defStyleRes);
          // 要被加载的布局 Id
          mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
          // 要被加载的布局
          mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
          // ViewStub 的 Id
          mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
          a.recycle();
      
          // 初始状态为 GONE
          setVisibility(GONE);
          // 设置为不会绘制
          setWillNotDraw(true);
      }
      
    • 接下来就看看关键的方法,然后看看初始化状态setVisibility方法。
      // 复写了 setVisibility(int) 方法
      @Override
      @android.view.RemotableViewMethod
      public void setVisibility(int visibility) {
          // private WeakReference<View> mInflatedViewRef;
          // mInflatedViewRef 是对布局的弱引用
          if (mInflatedViewRef != null) {
              // 如果不为 null,就拿到懒加载的 View
              View view = mInflatedViewRef.get();
              if (view != null) {
                  // 然后就直接对 View 进行 setVisibility 操作
                  view.setVisibility(visibility);
              } else {
                  // 如果为 null,就抛出异常
                  throw new IllegalStateException("setVisibility called on un-referenced view");
              }
          } else {
              super.setVisibility(visibility);
              // 之前说过,setVisibility(int) 也可以进行加载布局
              if (visibility == VISIBLE || visibility == INVISIBLE) {
                  // 因为在这里调用了 inflate()
                  inflate();
              }
          }
      }
      

    03.inflate()方法解析

    • 核心来了,平时用的时候,会经常调用到该方法。inflate() 是关键的加载实现,代码如下所示:
      public View inflate() {
          // 获取父视图
          final ViewParent viewParent = getParent();
          
          if (viewParent != null && viewParent instanceof ViewGroup) {
              // 如果没有指定布局,就会抛出异常
              if (mLayoutResource != 0) {
                  // viewParent 需为 ViewGroup
                  final ViewGroup parent = (ViewGroup) viewParent;
                  final LayoutInflater factory;
                  if (mInflater != null) {
                      factory = mInflater;
                  } else {
                      // 如果没有指定 LayoutInflater
                      factory = LayoutInflater.from(mContext);
                  }
                  // 获取布局
                  final View view = factory.inflate(mLayoutResource, parent,
                          false);
                  // 为 view 设置 Id
                  if (mInflatedId != NO_ID) {
                      view.setId(mInflatedId);
                  }
                  // 计算出 ViewStub 在 parent 中的位置
                  final int index = parent.indexOfChild(this);
                  // 把 ViewStub 从 parent 中移除
                  parent.removeViewInLayout(this);
                  
                  // 接下来就是把 view 加到 parent 的 index 位置中
                  final ViewGroup.LayoutParams layoutParams = getLayoutParams();
                  if (layoutParams != null) {
                      // 如果 ViewStub 的 layoutParams 不为空
                      // 就设置给 view
                      parent.addView(view, index, layoutParams);
                  } else {
                      parent.addView(view, index);
                  }
                  
                  // mInflatedViewRef 就是在这里对 view 进行了弱引用
                  mInflatedViewRef = new WeakReference<View>(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");
          }
      }
      
    • Inflate使用特点
      • ViewStub只能被Inflate一次,inflate之后ViewStub对象就会被置为空。即某个被ViewStub指定的布局被Inflate后,就不能够再通过ViewStub来控制它了。
      • ViewStub只能用来Inflate一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。

    04.WeakReference使用

    • 使用了弱引用管理对象的创建,代码如下所示
      • 在这里使用了get方法
      @Override
      @android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
      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 {
              
          }
      }
      
      • 在这里创建了弱引用对象
      public View inflate() {
          final ViewParent viewParent = getParent();
          if (viewParent != null && viewParent instanceof ViewGroup) {
              if (mLayoutResource != 0) {
                  mInflatedViewRef = new WeakReference<>(view);
                  return view;
              } else {
                  throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
              }
          } 
      }
      

    05.ViewStub为何无大小

    • 首先先看一段源码,如下所示:
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          setMeasuredDimension(0, 0);
      }
      
      @Override
      public void draw(Canvas canvas) {
      }
      
      @Override
      protected void dispatchDraw(Canvas canvas) {
      }
      
    • 有没有觉得很与众不同
      • draw和dispatchDraw虽然重写了,但是看代码却都是什么也不做!并且onMeasure还什么也不做,直接setMeasuredDimension(0,0);来把view区域设置位0,原来一个ViewStub虽然是一个view,却是一个没有任何显示内容,也不显示任何内容的特殊view,并且对layout在加载时候不可见的。

    06.ViewStub为何不绘制

    • 具体看一下setWillNotDraw(true)方法,代码如下:
      public void setWillNotDraw(boolean willNotDraw) {
          setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
      }
      
    • View中,对于WILL_NOT_DRAW是这样定义的:
      /**
       * This view won't draw. {@link #onDraw(android.graphics.Canvas)} won't be
       * called and further optimizations will be performed. It is okay to have
       * this flag set and a background. Use with DRAW_MASK when calling setFlags.
       * {@hide}
       */
      static final int WILL_NOT_DRAW = 0x00000080;
      
    • 设置WILL_NOT_DRAW之后,onDraw()不会被调用,通过略过绘制的过程,优化了性能。在ViewGroup中,初始化时设置了WILL_NOT_DRAW,代码如下:
      public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
       
          initViewGroup();
          initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
      }
       
      private void initViewGroup() {
          // ViewGroup doesn't draw by default
          if (!debugDraw()) {
              setFlags(WILL_NOT_DRAW, DRAW_MASK);
          }
          mGroupFlags |= FLAG_CLIP_CHILDREN;
          mGroupFlags |= FLAG_CLIP_TO_PADDING;
          mGroupFlags |= FLAG_ANIMATION_DONE;
          mGroupFlags |= FLAG_ANIMATION_CACHE;
          mGroupFlags |= FLAG_ALWAYS_DRAWN_WITH_CACHE;
       
          if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) {
              mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
          }
       
          setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
       
          mChildren = new View[ARRAY_INITIAL_CAPACITY];
          mChildrenCount = 0;
       
          mPersistentDrawingCache = PERSISTENT_SCROLLING_CACHE;
      }
      
    • 所以,在写自定义布局时,如果需要调用onDraw()进行绘制,则需要在初始化时候,调用setWillNotDraw(false)。若是想要更进一步阅读View中WILL_NOT_DRAW的相关源码,可以去看下PFLAG_SKIP_DRAW相关的代码。

    07.可以多次inflate()吗

    • ViewStub对象只可以Inflate一次,之后ViewStub对象会被置为空。同时需要注意的问题是,inflate一个ViewStub对象之后,就不能再inflate它了,否则会报错:ViewStub must have a non-null ViewGroup viewParent。。
    • 其实看一下源码就很好理解:
      public View inflate() {
          //获取viewStub的父容器对象
          final ViewParent viewParent = getParent();
      
          if (viewParent != null && viewParent instanceof ViewGroup) {
              if (mLayoutResource != 0) {
                  final ViewGroup parent = (ViewGroup) viewParent;
                  //这里是加载布局,并且给它设置id
                  //布局的加载是通过LayoutInflater解析出来的
                  final View view = inflateViewNoAdd(parent);
                  //这行代码很重要,下面会将到
                  replaceSelfWithView(view, parent);
      
                  //使用弱引用
                  mInflatedViewRef = new WeakReference<>(view);
                  if (mInflateListener != null) {
                      mInflateListener.onInflate(this, view);
                  }
                  return view;
              } else {
                  //如果已经加载出来,再次inflate就会抛出异常呢
                  throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
              }
          } else {
              throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
          }
      }
      
    • 其实也可以用一张图来理解它,如下所示,摘自网络
      • image
    • 也就是说,一旦调用inflate上面的方法后ViewStub就会变成null了,因此使用该对象特别需要注意空指针问题。

    08.ViewStub不支持merge

    • 不能引入包含merge标签的布局到ViewStub中。否则会报错:android.view.InflateException: Binary XML file line #1: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true

    09.ViewStub使用场景

    • 一般的app中大多有这么一个功能,当加载的数据为空时显示一个数据为空的视图、在数据加载失败时显示加载失败对应的UI,当没有网络的时候加载没有网络的UI,并支持点击重试会比白屏的用户体验更好一些。俗称,页面状态切换管理……一般来说,加载中、加载失败、空数据等状态的UI风格,在App内的所有页面中需要保持一致,也就是需要做到全局统一,也支持局部定制。
    • ViewStub的优势在于在上面的场景中,并不一定需要把所有的内容都展示出来,可以隐藏一些View视图,待用户需要展示的时候再加载到当前的Layout中,这个时候就可以用到ViewStub这个控件了,这样可以减少资源的消耗,使最初的加载速度变快。
    • 那么就有了之前开发使用的状态管理器开源库,就是采用了ViewStub这个控件,让View状态的切换和Activity彻底分离开。用builder模式来自由的添加需要的状态View,可以设置有数据,数据为空,加载数据错误,网络错误,加载中等多种状态,并且支持自定义状态的布局。可以说完全不影响性能……

    10.ViewStub总结分析

    • 分析源码的原理,不管认识到哪一步,最终的目标还是在运用上,即把看源码获得的知识用到实际开发中,那么关于ViewStub的使用技巧,具体可以看我的状态管理器案例,链接地址:https://github.com/yangchong211/YCStateLayout
    • 欢迎你的star,这也是开源和写博客的源源动力,哈哈

    ViewStub状态管理库:https://github.com/yangchong211/YCStateLayout

    开源博客大汇总:https://github.com/yangchong211/YCBlogs

    相关文章

      网友评论

        本文标题:ViewStub你真的了解吗

        本文链接:https://www.haomeiwen.com/subject/jdzzictx.html