ViewStub源码剖析

Quibbler 2022-3-2 687

ViewStub源码剖析


        之前在ViewStub懒加载一文中记录了如何使用ViewStub懒加载布局,当时没有继续对ViewStub深入研究。现在回头看看ViewStub源码,只有三百多行,属实需要细细品一下。

        nukc在看了ViewStub源码之后突发灵感,写了StateView库,吸收了ViewStub的一些特性,内存占用少。


        从ViewStub的构造函数开始看,ViewStub继承自View,所以至少有这四个标准构造函数

    public ViewStub(Context context) {
        this(context, 0);
    }
        
    public ViewStub(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }
    
    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);
        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
        mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
        a.recycle();
        setVisibility(GONE);
        setWillNotDraw(true);
    }

        除了以上的四个构造函数,ViewStub中还增加了一个构造函数ViewStub(Context context, int layoutResource):其中参数用于指定ViewStub加载的布局资源layout

    /**
     * Creates a new ViewStub with the specified layout resource.
     *
     * @param context The application's environment.
     * @param layoutResource The reference to a layout resource that will be inflated.
     */
    public ViewStub(Context context, @LayoutRes int layoutResource) {
        this(context, null);
        mLayoutResource = layoutResource;
    }

 

       上面的几个构造方法,最后都会调用下面带4个参数的构造方法ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)。在该构造方法里做了几件重要的事情:

    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);
        
        //加载的布局id,默认NO_ID
        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
        //加载的布局资源id
        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
        //当前ViewStub控件的id
        mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
        a.recycle();    //回收TypedArray
        
        //设置当前ViewStub可见性为GONE
        setVisibility(GONE);
        //不绘制当前ViewStub
        setWillNotDraw(true);
    }


        首先,初始化mInflatedIdmLayoutResourcemID,分别从ViewStub的三个属性获取:inflatedIdlayoutid。如何定义View的属性另见自定义View属性一文。ViewStub的几个属性定义如下:

    <!-- A {@link android.view.ViewStub} lets you lazily include other XML layouts inside your application at runtime. -->
    <declare-styleable name="ViewStub">
        <!-- Supply an identifier name for this view. -->
        <attr name="id" />
        
        <!-- Supply an identifier for the layout resource to inflate when the ViewStub
             becomes visible or when forced to do so. The layout resource must be a
             valid reference to a layout. -->
        <attr name="layout" format="reference" />
        
        <!-- Overrides the id of the inflated View with this value. -->
        <attr name="inflatedId" format="reference" />
    </declare-styleable>

        这三个属性就是在布局中使用ViewStub时所用到的:inflatedId就是加载进来的布局id,如果需要获取加载的布局,就要用这个inflatedId,原来的id会被替代(如果有)layout就是要加载进来的布局。id就是当前ViewStub的id。

    <ViewStub
        android:id="@+id/view_stub"
        android:inflatedId="@+id/inflate_layout"
        android:layout="@layout/include_layout" />


        其次,在构造方法的最后执行setVisibility(GONE)设置当前ViewStub可见性为GONE,和调用setWillNotDraw(true)方法设置不绘制当前ViewStub。并且还重写了下面这三个方法:onMeasure()draw()dispatchDraw(),最大程度降低ViewStub在布局绘制阶段对性能的影响。只有在需要的时候才去真正的加载、绘制布局。

    //当前ViewStub大小始终为0
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0);
    }
    
    @Override
    public void draw(Canvas canvas) {
    }
    
    @Override
    protected void dispatchDraw(Canvas canvas) {
    }


        当需要去加载布局展示的时候,只需调用ViewStubinflate()方法。该方法在ViewStub中的定义如下:

    /**
     * Inflates the layout resource identified by {@link #getLayoutResource()}
     * and replaces this StubbedView in its parent by the inflated layout resource.
     *
     * @return The inflated layout resource.
     *
     */
    public View inflate() {
        //拿到当前ViewStub所在的父容器
        final ViewParent viewParent = getParent();
        
        //
        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                //ViewParent通常为ViewGroup
                final ViewGroup parent = (ViewGroup) viewParent;
                
                //从方法名就可以看出,仅加载布局,这一步先不将加载的布局添加到父容器中
                final View view = inflateViewNoAdd(parent);
                
                //而是在这里,用加载出来的布局 替换 当前ViewStub
                replaceSelfWithView(view, parent);
                
                //通过弱引用持有加载出来的布局
                mInflatedViewRef = new WeakReference<>(view);
                
                //回调OnInflateListener监听器,如果有的话
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }
                
                //返回懒加载出来的布局对象
                return view;
            } else {
                //使用ViewStub必须指定要懒加载的布局资源id,也就是layout属性
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            //如果ViewStub已经加载了布局,那么通过getParent()获取到的viewParent就为null
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

        在inflate()方法中通过inflateViewNoAdd(ViewGroup parent)先去加载布局:

    private View inflateViewNoAdd(ViewGroup parent) {
        //使用LayoutInflater加载布局
        final LayoutInflater factory;
        
        //可以用setLayoutInflater(LayoutInflater inflater)设置mInflater
        if (mInflater != null) {
            factory = mInflater;
        } else {
            //或者使用默认的LayoutInflater
            factory = LayoutInflater.from(mContext);
        }
        
        //加载布局,注意attachToRoot参数为false
        final View view = factory.inflate(mLayoutResource, parent, false);
        
        //注意这里,如果给ViewStub提供了inflatedId属性值,就会将其设置给加载出来的布局
        if (mInflatedId != NO_ID) {
            view.setId(mInflatedId);
        }
        
        return view;
    }

        加载到布局后,紧接调用replaceSelfWithView(View view, ViewGroup parent)方法,ViewStub用加载到的布局替换自身:

    private void replaceSelfWithView(View view, ViewGroup parent) {
        //ViewStub在父容器中的索引位置
        final int index = parent.indexOfChild(this);
        
        //从父容器中移除自身
        parent.removeViewInLayout(this);
        
        //细节的一步,将自身的LayoutParams设置给加载到的布局
        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            //如果没有,则直接添加到自己的父容器里,ViewGroup会为其设置默认的LayoutParams 
            parent.addView(view, index);
        }
    }

         ViewStubinflate()方法只能调用一次,调用多次会抛出异常。ViewStub重写了ViewsetVisibility(int visibility)方法,通过该方法设置可见性间接的去懒加载布局。要注意弱引用关联的对象如果被回收,再调用此方法会抛出异常。关于弱引用另见《Java四种引用类型》一文。

    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) {
                //当设置的可见性值为VISIBLE或INVISIBLE的时候,调用inflate()方法去加载布局。
                inflate();
            }
        }
    }

        

       这里补充一下,ViewStub中定义了一个接口OnInflateListener。前面inflate()方法中也看到,当ViewStub成功加载布局的时候会回调该接口的onInflate(ViewStub stub, View inflated)方法(业务可以用来回调监听布局的加载)

    /**
     * Listener used to receive a notification after a ViewStub has successfully
     * inflated its layout resource.
     *
     * @see android.view.ViewStub#setOnInflateListener(android.view.ViewStub.OnInflateListener) 
     */
    public static interface OnInflateListener {
        /**
         * Invoked after a ViewStub successfully inflated its layout resource.
         * This method is invoked after the inflated view was added to the
         * hierarchy but before the layout pass.
         *
         * @param stub The ViewStub that initiated the inflation.
         * @param inflated The inflated View.
         */
        void onInflate(ViewStub stub, View inflated);
    }

        ViewStub提供setOnInflateListener(OnInflateListener inflateListener)方法用于设置一个布局加载监听器给mInflateListener

    /**
     * Specifies the inflate listener to be notified after this ViewStub successfully
     * inflated its layout resource.
     *
     * @param inflateListener The OnInflateListener to notify of successful inflation.
     *
     * @see android.view.ViewStub.OnInflateListener
     */
    public void setOnInflateListener(OnInflateListener inflateListener) {
        mInflateListener = inflateListener;
    }



相关资料:

         github/LittleFriendsGroup/AndroidSdkSourceAnalysis/ViewStub 源码解析

        https://github.com/nukc/StateView

        自定义View属性

        

不忘初心的阿甘
最新回复 (0)
    • 安卓笔记本
      2
        登录 注册 QQ
返回
仅供学习交流,切勿用于商业用途。如有错误欢迎指出:fluent0418@gmail.com