从源码角度分析ViewStub 疑问与原理

一、提出疑问

    ViewStub比较简单,之前文章都提及到
《Android 性能优化 三 布局优化ViewStub标签的使用》,但是在使用过程中有一个疑惑,到底是ViewStub上设置的参数有效还是在其包括的layout中设置参数有效?如果不明白描述的问题,可以看下以下布局伪代码。

res/layout/main.xml
<LinearLayout >
    <ViewStub
        android:id="@+id/viewstub"
        android:layout_width="100dip"
          android:layout_marginTop="100dip"
        android:layout_height="wrap_content"
        android:layout="@layout/sub_layout"
        />
   
</LinearLayout>

res/layout/sub_layout.xml
<TextView
     android:layout_width="50dip"
     android:layout_marginTop="50dip"
     android:layout_height="wrap_content"
     android:text="ViewStub中包含的TextVeiw"/>

    上面的代码中width最终效果是100dip还是50dip?marginTop是100dip还是50dip?带着这个问题一起看下Android 5.0源码看看ViewStub原理。

    为了便于把ViewStub与其infalte()加载出来的android:layout视图做个区分,下文中针对前者统一命名“ViewStub视图”,后者命名“被 加载视图”,仅为了描述统一并不一定是专业名称。

二、分析ViewStub源码

    让ViewStub有两种方式一种是调用ViewStub.inflate() 另外一种是设置ViewStub.setVisibility(View.VISIBLE); 其实第二种方式依然是调用的infalte方法,可以看如下ViewStub源码。

    @Override
    @android.view.RemotableViewMethod
    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 {
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate();
            }
        }
    }

ViewStub复写了setVisibility方法,并在其中调用infalte方法,下面来看此方法源码

public final class ViewStub extends View {
     ......

    public View inflate() {
        final ViewParent viewParent = getParent(); // 1 为什么可以直接获取父视图?

          // ViewStub的父视图必须是ViewGroup的子类
        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) { // ViewStub必须设置android:layout属性
                final ViewGroup parent = (ViewGroup) viewParent;
                final LayoutInflater factory;
                if (mInflater != null) {
                    factory = mInflater;
                } else {
                    factory = LayoutInflater.from(mContext);
                }
                // 2 inflate被加载视图 
                final View view = factory.inflate(mLayoutResource, parent,
                        false);

                if (mInflatedId != NO_ID) {
                    view.setId(mInflatedId);
                }

                // 从父视图中获取当前ViewStub在父视图中的位置
                final int index = parent.indexOfChild(this);
                // 当前ViewStub也是个View仅仅只是用来占位,所以先把占位的ViewStub视图删除
                parent.removeViewInLayout(this);

                // 3 此处获取的是ViewStub上面设置的参数
                final ViewGroup.LayoutParams layoutParams = getLayoutParams();
                if (layoutParams != null) {
                    parent.addView(view, index, layoutParams);
                } else {
                    parent.addView(view, index);
                }

                // 目的是在复写的setVisibility方法中使用
                // 因为ViewStub.setVisibility操作的是被加载视图并非当前ViewStub视图
                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");
        }
    }
     ......
}

下面说下源码中列出的几点。


 1. 为什么可以直接获取父视图?

ViewStub 继承自View其自身就是一个视图,其调用getParent()可以从父类View上入手、

public class View {    
    public final ViewParent getParent() {
        return mParent;
    }

    void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"
                    + " it already has a parent");
        }
    }
}

从View的源码中获取到,修改mParent参数的仅有assignParent方法且View中并未调用此方法,下面查看下其子类ViewGroup是否有调用。

public class ViewGroup {    
    public void addView(View child, int index, LayoutParams params) {
    
        ......
         
        addViewInner(child, index, params, false);
    }

    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {
              
        ......           
         
        // tell our children
        if (preventRequestLayout) {
            child.assignParent(this);
        } else {
            child.mParent = this;
        }
         
          ......
     }    
}

从上面源码可以看到在addView方法中会调用addViewInner,其中调用child.assignParent(this);,把自己所有子视图mParent都设置成当前ViewGroup。 从这一点也可以看出,ViewStub本身是一个View且加载的时候就已经添加到视图树中(View Tree)中,仅接着有另外一个问题既然页面显示的时候ViewStub已经被添加到界面上,为什么有看不到ViewStub视图呢?

疑问:为什么ViewStub虽然是懒加载,但是其自身是一个视图且展示界面就会添加到视图树中,为什么看不到ViewStub?

public final class ViewStub extends View {

    public ViewStub(Context context) {
        initialize(context);
    }
     
     private void initialize(Context context) {
        mContext = context;
        setVisibility(GONE); // 初始化时把自己设置为隐藏
        setWillNotDraw(true);
    }
     
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0); // 所有子视图都设置为宽高为0
    }

    @Override
    public void draw(Canvas canvas) { // 不对自身与子视图进行绘制
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }
}

从以上源码可以看出ViewStub用尽所有办法让自己添加到视图树上是不显示ViewStub自身。

2. inflate被加载视图     再来看下加载android:layout视图的源码。 final View view = factory.inflate(mLayoutResource, parent, false);     可以看到通过infalte方法记载的,其三个参数(int resource, ViewGroup root, boolean attachToRoot),分别是: mLayoutResource : 设置的android:layout的值                 parent : 通过getParent()获取即ViewStub的父视图                   false : attachToRoot设置为false说明忽略androd:layout中根节点的layoutParams参数,即width=50dip与margin50dip

3. 视图添加ViewStub.getLayoutParams参数 此处源码的是获取ViewStub.getLayoutParams参数设置到anroid:layout加载的视图上, 即width=100dip与marginTop=100dip生效。

三、总结 开头的疑问的答案,inflate出来的视图width=100dip与marginTop=100dip而android:layout视图中设置的width50dip和marginTop=50dip失效,等于没有设置。

ViewStub的原理简单描述是 1. ViewStub本身是一个视图,会被添加到界面上,之所以看不到是因为其源码设置为隐藏与不绘制。 2. 当调用infalte或者ViewStub.setVisibility(View.VISIBLE);时(两个都使用infalte方法逻辑),先从父视图上把当前ViewStub删除,再把加载的android:layotu视图添加上 3. 把ViewStub layoutParams 添加到加载的android:layotu视图上,而其根节点layoutParams 设置无效。 4. ViewStub是指用来占位的视图,通过删除自己并添加android:layout视图达到懒加载效果

    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/androiddevelop/article/details/46632323
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞