ViewStub源码剖析

QuibblerQuibbler 2022-03-02 约 25 分钟 1816 次阅读

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);
    }

首先,初始化mInflatedId、mLayoutResource、mID,分别从ViewStub的三个属性获取:inflatedId、layout、id。如何定义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) {
    }

当需要去加载布局展示的时候,只需调用ViewStub的inflate()方法。该方法在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);
        }
    }

ViewStub的inflate()方法只能调用一次,调用多次会抛出异常。ViewStub重写了View的setVisibility(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属性

        

相关推荐

精选
ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager
Android

ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager

ViewPager1、ViewPagerandroidx.viewpager.widget.ViewPager,Android中使用非常广泛的控件,可以说是APP必备:首次打开引导页、页面Banner广告等。常用方法:setAdapter() 设置适配器setOffscreenPageLimit() 设置缓存的页面个数,默认是 1setCurrentItem() 跳转到特定的页面setOnPage

1.1k
获取Android内置WebView内核版本
Android

获取Android内置WebView内核版本

获取Android内置WebView内核版本竟然能遇到这样奇葩的事情,网页用的技术过于新颖,以至于只支持高版本Chromium内核的,低版本安卓系统中内置的内核版本较低,无法加载前端页面。1、设置查看在系统设置里 > 应用 > 应用管理 > 显示系统应用,查看WebView组件:2、页面查看通过WebView发起的网络请求,都会带上浏览器的UA,通常页面都可以通过UA判断浏览器的内核版本。这里有两

1.1w
Android 14适配总结
Android

Android 14适配总结

Android 14适配总结毕业工作至今已经适配了三个Android大版本,从Android 11到Android 12、再到Android 13。2023年,Google即将推出的Android 14,上半年已经开始第一批适配。现在,第四个Android版本已经适配完,总结记录一下。1、Android 14计划Google一般会在2月份对外发布预告,同时放出开发者预览版。“拉通”各大平台、厂商以

1.1w