Android中ViewStub原理解析

本文主要从如下几点来学习ViewStub

  • ViewStub是啥
  • ViewStub的属性解析
  • ViewStub的代码实操
  • ViewStub的原理解析
  • ViewStub实际中一般常用的情景
  • ViewStub的两个小问题

ViewStub是啥

在介绍ViewStub是啥之前,我们了解下为什么要用ViewStub

在我们日常开发中,有些布局或者控件一开始并不需要显示,是根据业务场景来控制显示状态的,我们通常的做法就是在xml文件设置不可见,然后通过setVisibility()方法来更新它的可见性,但是这样做会对程序的性能有一定的影响,我们知道加载布局的时候有两个瓶颈 一个是将xml文件加载到内存中是IO操作,通过反射或者到View的对象反射操作是耗时操作,这两者都是耗时的操作。

基于上面的业务情况,出现了ViewStub的标签,它是按需加载View,能够很容易实现布局的懒加载来提升程序的性能。

  • ViewStub 继承于 View

  • 看下源码声明

    /*
     * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate
     * layout resources at runtime.
     * When a ViewStub is made visible, or when {@link #inflate()}  is invoked, the layout resource 
     * is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views.
     * Therefore, the ViewStub exists in the view hierarchy until {@link #setVisibility(int)} or
     * {@link #inflate()} is invoked.
     */
    
    
    //ViewStub是一个不可见的,宽高为0的View,可用于在程序运行的时候延迟  加载布局资源的(用于实现布局资源的“懒加载”)
    
    //当使ViewStub可见或者调用inflate方法,可以使布局资源被加载!
    
    //ViewStub存在于视图的层级中直到setVisibility()方法或者inflate()方法被执行后,ViewStub相关的资源就会被加载并在控件层级结构中代替ViewStub,同时ViewStub会从控件中移除。
    
    

ViewStub的属性解析

android:id
  • ViewStub在布局文件中ID,用于在代码中访问
  • View共有的
android:layout
  • 在显示ViewStub时真正加载并且显示的布局文件
  • ViewStub特有的
android:inflatedId
  • 真正布局加载后的布局Id
  • ViewStub特有的

ViewStub的代码实操

Xml资源文件
  • 代码如下

    <ViewStub android:id="@+id/stub"       
      android:inflatedId="@+id/subTree"                                        
      android:layout="@layout/mySubTree              
      android:layout_width="120dp"
      android:layout_height="40dp"/>
    
Java代码
  • 代码如下

    //1,通过id找到ViewStub,得到ViewStub对象
    ViewStub myViewStub = (ViewStub)findViewById(R.id.stub);
    if(myViewStub!=null){
      //2,通过inflate方法加载真正的布局View
      View myInflatedView = myViewStub.inflate();
      //3,找到相应的控件
     TextView myTextView =  myInflatedView.findViewById(R.id.my_text_view);
    }
    

ViewStub的原理解析

ViewStub的构造方法
  • 代码如下

    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);
    
            //获取在xml文件中定义的inflatedId属性
            mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
            //获取到xml文件中定义的layout属性
            mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
            //获取xml文件中定义的id属性
            mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
            a.recycle();
    
            //设置ViewStub直接不显示
            //也可以看出来,你在xml文件中如何控制它的显示属性,都是不显示的
            setVisibility(GONE);
            // 设置ViewStub不尽兴绘制
            setWillNotDraw(true);
        }
    
ViewStub的onMeasure和onDraw方法
  • 代码如下

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //设置宽和高都为0 也就是控件的大小为0
            setMeasuredDimension(0, 0);
        } 
    
    @Override
        public void draw(Canvas canvas) {
            //不进行任何绘制
        }
    
        @Override
        protected void dispatchDraw(Canvas canvas) {
        }
    
ViewStub的inflate方法
  • 代码如下

    public View inflate() {
            //获取ViewStub在布局文件中的父布局
            final ViewParent viewParent = getParent();
    
            if (viewParent != null && viewParent instanceof ViewGroup) {
                //mLayoutResource 就是属性 layout指定的真正要加载的布局
                if (mLayoutResource != 0) {
                    final ViewGroup parent = (ViewGroup) viewParent;
                    //把真正要显示的View布局文件渲染成View对象并且给返回
                    final View view = inflateViewNoAdd(parent);
                    //将ViewStub从布局文件结构中移除,并且把渲染好的View添加到ViewStub所处的位置。
                    replaceSelfWithView(view, parent);
    
                    mInflatedViewRef = new WeakReference<>(view);
                    if (mInflateListener != null) {
                        //保存当前View对象的弱引用,方便其他地方使用
                        mInflateListener.onInflate(this, view);
                    }
    
                    //返回创建的View对象
                    return view;
                } else {
                    //当我们没有为ViewStub指定layut属性时,会走这个case,抛出异常
                    throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
                }
            } else {
                //第一个调用ViewStub的inflate方法后,会把ViewStub从布局文件结构中移除,就是没有了ViewGroup了
                // 当第二次调用ViewStub的inflate方法后,会走这个case,抛出异常。
                throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
            }
        }
    
    private View inflateViewNoAdd(ViewGroup parent) {
            // 获取到布局的填充器
            final LayoutInflater factory;
            if (mInflater != null) {
                factory = mInflater;
            } else {
                factory = LayoutInflater.from(mContext);
            }
            // 把真正要显示的布局文件渲染成View对象
            final View view = factory.inflate(mLayoutResource, parent, false);
    
            //  mInflatedId 对应 android:inflatedId 如果指定了就为渲染好的View给设置进去
            if (mInflatedId != NO_ID) {
                view.setId(mInflatedId);
            }
            return view;
        }
    
    private void replaceSelfWithView(View view, ViewGroup parent) {
            // 获取ViewStub在父布局中所处在的位置
            final int index = parent.indexOfChild(this);
            // 将ViewStub从父布局中移除
            parent.removeViewInLayout(this);
            // 获取ViewStub的布局参数
            final ViewGroup.LayoutParams layoutParams = getLayoutParams();
            // 当设置了布局参数(例如 android:width="50dp",height="50dp")
            if (layoutParams != null) {
                // 将渲染好的View连同ViewStub的布局参数添加到ViewStub所处的位置
                parent.addView(view, index, layoutParams);
            } else {
                //将渲染好的View添加到ViewStub所处的位置
                parent.addView(view, index);
            }
        }
    
  • 在我看来inflate方法主要的就是inflateViewNoAdd和replaceSelfWithView方法

  • inflateViewNoAdd:获取到布局渲染器将真正需要展示的布局文件渲染成View并且给返回。

  • replaceSelfWithView:将ViewStub从布局文件结构中移除,同时把渲染好的View添加到ViewStub之前所处的位置

  • 之后把渲染好的View的弱引用给存储起来。方便在setVisibility()方法中使用。

ViewStub的setVisibility()方法
  • 代码如下

    public void setVisibility(int visibility) {
            // 纵观全局,mInflatedViewRef只有在inflate方法中初始化了,
            // 当真正的布局文件被加载之后
            if (mInflatedViewRef != null) {
                // 获取到当前的View
                View view = mInflatedViewRef.get();
                if (view != null) {
                    //操纵当前View的可见行
                    view.setVisibility(visibility);
                } else {
                    throw new IllegalStateException("setVisibility called on un-referenced view");
                }
            } else {
                //没有调用inflate的话,会设置可见性
                super.setVisibility(visibility);
                //当 当前设置可见性为 VISIBLE或者INVISIBLE的时候,会调用inflate方法。
                if (visibility == VISIBLE || visibility == INVISIBLE) {
                    inflate();
                }
            }
        }
    

ViewStub实际中一般常用情景

  • 比如我们在无数据或者网络错粗的时候,需要单独显示一个布局,那么这个布局就可以用ViewStub。

ViewStub的几个问题

两次调用ViewStub的inflate方法会怎么样?
  • 我们知道第一次调用inflate方法的时候,会将ViewStub从布局文件结构中移除。
  • 当第二次调用的时候,ViewStub已经没有父控件了,当做错误检查的时候,会抛出异常。
在xml文件中为ViewStub设置了 android:visibility="visible" 属性,ViewStub中真正要显示的View会显示嘛?
  • 不会,我们在ViewStub的构造方法中,有看到它默认调用了setVisibility(GONE)
  • 所以你在xml文件中如何操作可见行,都是没法显示的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容