RTFSC-ViewStub

前言

ViewStub控件,惰性装载控件。在介绍ViewStub之前,可以先了解一下<include/>标签,这是一个把其它布局资源包含进某个特定的布局中。可以和ViewStub做一个对照。区别在于使用include的方式,在初始化时就会加载进入布局,而ViewStub作为惰性装载控件,只有在被inflate或者设置为可见时,布局内容才会被加载。形成一种惰性装载的效果,可以在初始化时对不必要显示的资源做一个惰性装载,达到优化初始化的效果

示例

 <!--activity_main.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <ViewStub
        android:id="@+id/viewStud"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inflatedId="@+id/subTree"
        android:layout="@layout/text_layout" />
</LinearLayout>

text_layout就是需要惰性加载的布局,

 <!--text_layout.xml-->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">
<TextView
    android:id="@+id/tv_show"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="view_stub"/>
</FrameLayout>

如何使用?

//MainActivity .java
public class MainActivity extends AppCompatActivity {
    ViewStub viewStub;
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewStub = findViewById(R.id.viewStud);
        viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub stub, View inflated) {
                Log.i(TAG, "onInflate: ");
                TextView textView = inflated.findViewById(R.id.tv_show);
                textView.setText("chang text content");
            }
        });
        viewStub.inflate();
    }
}

使用方法的代码很简单,就是让viewstub做一个站位的操作,将需要懒加载的布局传递给viewstub控件(通过viewstub的layout属性),在代码中找到viewstub实例,可以给它设置一个inflate的监听,在真正调用inflate()方法或者设置可见的时候会监听到对应的懒加载view。

源码分析

(PS:ViewStub.java文件总共也就300多行,这可能是最好分析的Android SDK源码了吧!既然这样,代码全贴出来也不过分 ^.^ )

public final class ViewStub extends View {
    private int mInflatedId;
    private int mLayoutResource;

    private WeakReference<View> mInflatedViewRef;

    private LayoutInflater mInflater;
    private OnInflateListener mInflateListener;

    public ViewStub(Context context) {
        this(context, 0);
    }

    public ViewStub(Context context, @LayoutRes int layoutResource) {
        this(context, null);

        mLayoutResource = layoutResource;
    }

    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);
        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);
    }
    @IdRes
    public int getInflatedId() {
        return mInflatedId;
    }
    @android.view.RemotableViewMethod(asyncImpl = "setInflatedIdAsync")
    public void setInflatedId(@IdRes int inflatedId) {
        mInflatedId = inflatedId;
    }

    /** @hide **/
    public Runnable setInflatedIdAsync(@IdRes int inflatedId) {
        mInflatedId = inflatedId;
        return null;
    }

    @LayoutRes
    public int getLayoutResource() {
        return mLayoutResource;
    }
    @android.view.RemotableViewMethod(asyncImpl = "setLayoutResourceAsync")
    public void setLayoutResource(@LayoutRes int layoutResource) {
        mLayoutResource = layoutResource;
    }

    /** @hide **/
    public Runnable setLayoutResourceAsync(@LayoutRes int layoutResource) {
        mLayoutResource = layoutResource;
        return null;
    }

    public void setLayoutInflater(LayoutInflater inflater) {
        mInflater = inflater;
    }

    /**
     * Get current {@link LayoutInflater} used in {@link #inflate()}.
     */
    public LayoutInflater getLayoutInflater() {
        return mInflater;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }

    @Override
    @android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
    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();
            }
        }
    }

    /** @hide **/
    public Runnable setVisibilityAsync(int visibility) {
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            ViewGroup parent = (ViewGroup) getParent();
            return new ViewReplaceRunnable(inflateViewNoAdd(parent));
        } else {
            return null;
        }
    }

    private View inflateViewNoAdd(ViewGroup parent) {
        final LayoutInflater factory;
        if (mInflater != null) {
            factory = mInflater;
        } else {
            factory = LayoutInflater.from(mContext);
        }
        final View view = factory.inflate(mLayoutResource, parent, false);

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

    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

    public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                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 {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }
    public void setOnInflateListener(OnInflateListener inflateListener) {
        mInflateListener = inflateListener;
    }

    public static interface OnInflateListener {
        void onInflate(ViewStub stub, View inflated);
    }

    /** @hide **/
    public class ViewReplaceRunnable implements Runnable {
        public final View view;

        ViewReplaceRunnable(View view) {
            this.view = view;
        }

        @Override
        public void run() {
            replaceSelfWithView(view, (ViewGroup) getParent());
        }
    }
}

ViewStub也是继承自View,所以可以成为xml布局中的一个站位控件,除了ViewStub外,里面还包含了一个接口OnInflateListener 和一个ViewReplaceRunnable类,OnInflateListener 用于监听ViewStub加载布局时的inflate监听。ViewReplaceRunnable是一个异步加载布局的Runnable.在调用setVisibilityAsync()方法时会返回runnable对象。用于异步加载控件。
mInflatedId:需要加载的布局id,
mLayoutResource:需要加载的布局
mInflatedViewRef:对需要加载的布局view的一个弱引用的持有
我们主要关注一下ViewStub的三个方法

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

onMeasure方法,只有一句代码setMeasuredDimension(0, 0);所以说,不管ViewStub设置多大,在初始化时都是一个没有大小的(A ViewStub is an invisible, zero-sized View)。至于不可见,可以在初始化里面看到,默认是设置了setVisibility(GONE);

setVisibility(int visibility)

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

首先是看弱引用中是否持有当前需要加载的view对象,如果有的话,改变一下他的显示、隐藏状态。如果没有的话,说明是第一次加载,当然需要走到inflate()方法中去

inflate()

public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                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 {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

在inflate方法中首先会调用inflateViewNoAdd方法

private View inflateViewNoAdd(ViewGroup parent) {
        final LayoutInflater factory;
        if (mInflater != null) {
            factory = mInflater;
        } else {
            factory = LayoutInflater.from(mContext);
        }
        final View view = factory.inflate(mLayoutResource, parent, false);

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

inflateViewNoAdd方法就是通过factory加载设置的视图mLayoutResource,并返回view对象。然后会调用replaceSelfWithView(view, parent)方法;

private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

将inflateViewNoAdd方法返回的view对象加入到ViewStub的父view上,完成ViewStub占位的替换。最后

mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

将加载好的view放在弱引用中持有,用于后面的显示隐藏对view的重新操作,同时回调onInflate。

其中还有一个重要的setVisibilityAsync(int visibility)方法,同样会通过LayoutInflater把mLayoutResource转换成view对象,此方法和setVisibility(int visibility)唯一的区别就时,在于它会返回一个Runnable对象,然后可以在想要加载的时候将view加载到界面中。

以上,分析完毕!如有错误,欢迎指出~

    原文作者:二毛_coder
    原文地址: https://www.jianshu.com/p/16b8f6ab824d
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞