深入分析 ViewStub 原理

前言

最近在面试的时候被面试官问到ViewStub内部是如何使用占位的时候,我是一脸懵逼,说实话我之前对UI上的一些控件内部的代码看的非常少,所以一时答不上来,这个东西其实并不说有多难,而是我们平时在开发的过程中只是简单的去调用了一下但是并没有深入的去了解其内部的原理。其实面试的过程就是我们一个查漏补缺的过程中,让我们的各个知识面都更加的能提高,同时做到知其然更知其所以然。

介绍

平时我们在开发Android的界面的时候,如果遇到不需要显示的控件或者是布局的时候挺通常我们都会将其设置为View.Gone或者是View.INVISIBLE来达到我们想要的目的的,这样做的优点就是逻辑简单而且控制起来比较灵活,但是其缺点就是耗资源,其实在内部Xml解析的时候同样也会将其解析并且实例化、设置属性的,同样还是会消耗系统资源的。这个时候ViewStub就闪亮登场了,为了更好的方便理解,首先我们看看其大概用法,同时分析源代码以后再来总结其结论

用法

首先我们在布局文件里面使用两个ViewStub,然后设置其 id 和layout 布局文件

<?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">

    <Button
        android:id="@+id/display"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="显示/隐藏"
        android:textColor="@color/colorAccent" />

    <ViewStub
        android:id="@+id/style_1"
        android:layout_width="200dip"
        android:layout_height="200dip"
        android:layout="@layout/layout_content_first" />

    <ViewStub
        android:id="@+id/style_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout="@layout/layout_content_second" />

</LinearLayout>
public class MainActivity extends AppCompatActivity {

    private ViewStub mViewStub1;
    private ViewStub mViewStub2;

    private View mViewStubContentView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //首先根据 id 获取 ViewStub
        mViewStub1 = findViewById(R.id.style_1);
        mViewStub2 = findViewById(R.id.style_2);
        //同时在我们需要的时候,初始化 ViewStub 包裹的布局,其实ViewStub的延迟加载就是这么个原理的
        mViewStubContentView = mViewStub1.inflate();
        //同时获取 ViewStub的 LayoutParams 参数
        ViewGroup.LayoutParams params = mViewStub1.getLayoutParams();
        Log.i("LOH", params.width + "...height..." + params.height);
        //我们使用display按钮来控制 ViewStub加载出来以后的view的显示与隐藏
        findViewById(R.id.display).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mViewStubContentView.getVisibility() == View.VISIBLE) {
                    mViewStubContentView.setVisibility(View.GONE);
                }else {
                    mViewStubContentView.setVisibility(View.VISIBLE);
                }
            }
        });
    }
}

上面就是一个非常简单的 ViewStub 的使用案例,如果我们需要显示ViewStub 中布局文件的话,可以调用inflate 方法或者是也可以调用 ViewStub.setVisible(View.VISIBLE)就能将布局显示出来。

代码分析

  • 构造方法分析
   public final class ViewStub extends View {
   
   .......
   //一般ViewStub 在xml中引用的话,都是走这个构造方法的。
   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);
       //首先获取 inflateId 编号
       mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
       //然后获取自定义属性 inflatedId 也就是 布局文件(xml 文件)
       mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
       //获取 ViewStub 定义的 id 编号
       mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
       //记住这里需要回收(因为编译器会提示说这里内存泄漏)
       a.recycle();
       //这是是核心关键,首先设置 当前的View隐藏,同时设置自己不参与绘制,接着我们在 onDraw方法
       setVisibility(GONE);
       setWillNotDraw(true);
   }
   
   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       setMeasuredDimension(0, 0);
   }

   @Override
   public void draw(Canvas canvas) {
   }
}

通过上面的代码我们可以很清楚的知道了ViewStub不参与在View的绘制中,首先是设置了View.GONE,接着调用了View方法里面的 setWillNotDraw 不参与界面绘制,而且自己的 draw方法也未任何的实现。最后将自己的宽度和高度都设置为了0。

  • 通过setVisible 方法显示界面
public void setVisibility(int visibility) {
        //首先第一次调用的 mInflatedViewRef 是为空的,所以就进入else 分支
        if (mInflatedViewRef != null) {
            View view = mInflatedViewRef.get();
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else {
            //首先这里会直接调用 view的 setVisibility方法
            super.setVisibility(visibility);
            //接着判断我们传进来的 visibility的值,如果还是 GONE的话则不做处理,最后还是调用inflate
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate();
            }
        }
    }

通过上面的代码分析我们可以得出 setVisibility 最后调用的还是 inflate,所以这个方法才是关键的

public View inflate() {
    final ViewParent viewParent = getParent();
    //首先判断 ViewStub是否存在父控件,同时父控件是否 ViewGroup
    if (viewParent != null && viewParent instanceof ViewGroup) {
       //同时必须设置 ViewStub的layout 信息,不能单独设置 View。
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            //将 ViewStub 的layout布局文件转化为 View,但是不添加到 parent中
            final View view = inflateViewNoAdd(parent);
            //最后在viewParen中找到ViewStub的位置,同时将inflate出来的View替换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");
        }
    } else {
      //ViewStub 不能单独使用,比如是 ViewGroup的一个子View。
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
  }

最后将实例化出来的view替换ViewStub在viewParent中的位置

private void replaceSelfWithView(View view, ViewGroup parent) {
     //首先获取ViewStub 在 parent中的位置
     final int index = parent.indexOfChild(this);
     //同时将其从父控件中移除
     parent.removeViewInLayout(this);
     //注意这里获取的是 ViewStub的 LayoutParams,也就是 layout出来的参数是没效果的。只能设置ViewStub的参数才行。
     final ViewGroup.LayoutParams layoutParams = getLayoutParams();
     if (layoutParams != null) {
         parent.addView(view, index, layoutParams);
     } else {
         parent.addView(view, index);
     }
 }

分析

这个类设计的其实非常的简单,其代码也就仅仅只有几百行。通过我们上面对代码的分析可以将其总结为如下

  1. ViewStub 通过设置GONE 以及设置宽和高都为0,以及调用函数setWillNotDraw(true)来达到自己不绘制,不渲染在界面的效果,其实仅仅就是作为一个占着坑的意思。
  2. ViewStub 只能调用一次 setVisibility 方法,而 setVisibility 最后调用的还是 inflate 方法。在 replaceSelfWithView 中 indexOfChild(this)代码中,如果ViewStub被移除了以后,index则是 -1那么 addView的时候则会抛出异常的。
  3. ViewStub layoutParams 加入到载入的android:layotu视图上。而其根节点 layoutParams 设置无效

总结

通过上面的源代码我们可以分析得出ViewStub的懒加载的原理,首先通过 ViewStub占住位置而且又不绘制和显示在界面(原因看分析),接着我们在需要的时候调用ViewStub的setVisible方法和inflate方法来将页面显示,其原来就是将 layout 文件渲染成 view然后添加到其父控件(ViewGroup中),主要是替换之前ViewStub之前占用的位置来达到显示界面。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值