ViewStub源码分析

 

为了优化UI加载,通常会把不需要立即显示的View放到ViewStub里,在需要的时候按需加载,以此来优化UI性能。

  • 特点

1.ViewStub 是一个轻量级的View,没有尺寸,不绘制任何东西

2.在视图树中充当占位符的作用,在需要的时候才加载真正显示的View,实现View的延迟加载,避免资源浪费,减少渲染时间。

3.缺点是ViewStub所要替代的layout根布局是<merge>标签
 

  • 基本用法:

1.在布局中直接引用ViewStub, 通过ViewStub的属性来指定对应的layout即可,如下:

  <ViewStub
        android:id="@+id/viewstub"
        android:layout_width="552dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="65dp"
        android:layout_marginTop="570dp"
        android:layout="@layout/loading_layout" />

2.使用时,通过调用ViewStub的setVisibility()或者inflate()方法,实现加载显示。两者的区别,后面结合源码分析

3.注意事项:a).layout只能加载一次  b)layout加载之后,就不能通过ViewStub的Id获取到了。

  • 源码分析

1.ViewStub在布局中起到占位符的作用,本身不显示,不绘制,如下。

    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();
        //设置ViewStub不可见,不绘制
        setVisibility(GONE);
        setWillNotDraw(true);
    }

 2 . 加载指定的layout布局并添加到ViewStub的Parent中,加载成功之后ViewStub会从它的父容器,因此无法再使用

      a) 通过inflate()方法加载layout,inflate会通过LayoutInflater加载对应资源id对应的布局文件,

   /**
     * 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() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            //mLayoutResource 真正要加载的布局文件
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                //使用真正要加载的布局替换viewStub
                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 {
            // 这里也说明了为什么ViewStub不能放在merge标签下,因为merge不是View,
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

       加载获取对应的View

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

       替换并移除ViewStub, 加载真正要显示的布局后,ViewStub便从父容器里移除掉了,也从整个视图树中移除了,所findViewById找不到。

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

  b) 通过setVisibility()方法加载,在已经加载过View的情况下,会从缓存中获取显示View,在没加载时会调用inflate()方法,也就是上面的加载流程

    /**
     * When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
     * {@link #inflate()} is invoked and this StubbedView is replaced in its parent
     * by the inflated layout resource. After that calls to this function are passed
     * through to the inflated view.
     *
     * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
     *
     * @see #inflate() 
     */
    @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();
            }
        }
    }

  c) ViewStub为什么不支持merge ?

     首先,了解一下merge标签的特点:引自https://www.jianshu.com/p/69e1a3743960

  • merge必须放在布局文件的根节点上。
  • merge并不是一个ViewGroup,也不是一个View,它相当于声明了一些视图,等待被添加。
  • merge标签被添加到A容器下,那么merge下的所有视图将被添加到A容器下。
  • 因为merge标签并不是View,所以在通过LayoutInflate.inflate方法渲染的时候, 第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点
  • 因为merge不是View,所以对merge标签设置的所有属性都是无效的。

    主要关注红色加深处,merge载通过LayoutInflate.inflate方法渲染时必须指定父容器,切attachToRoot必须为true。

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

回到ViewStub加载View的方法,如下,可以看到,加载布局的attachToRoot参数默认值为false,因此ViewStub不支持merge,当我们使用merge时会报错:Caused by: android.view.InflateException: can be used only with a valid ViewGroup root and attachToRoot=true 。

    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;
    }
  • 典型应用

      Viewpager+Fragment的形式,在有多个Fragment的情况下,由于想要进行预加载,在首次进入某一个页面时,虽然只有一个Fragment呈现在眼前,Viewpager下其他的Fragment的生命周期函数onCreateView(), onResume()也会执行(与setOffscreenPageLimit的设置有关),也就是说其他Fragment页面布局的加载,在onCreateView(), onResume()里的逻辑也会被执行,这显然不是很有必要。而且会增加我们想要打开的界面的加载时长。

此时就可以通过:ViewStub+setUserVisibleHint()实现布局的懒加载以及延迟初始化,在onCreateView中只加载布局的"壳",在真正切换到要显示的fragment页时,再将真正的布局加载进来。

主要代码如下:


 /**
 * 标志位,标志已经初始化完成
 */
private boolean isPrepared;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.joy_listen_fragment, container, false);
    viewStub = view.findViewById(R.id.enjoy_viewstub);
    ...
    isPrepared = true;
    return view;
}
 
public void initVew(View view) {
    ......
}
 
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    if (isPrepared && isVisibleToUser) {
        if (viewStub.getParent() != null) {
            View view = viewStub.inflate();
            initVew(view);
            mAppsPresenterImpl = new AppsPresenterImpl(this, getActivity());
            mAppsPresenterImpl.getCacheApps(DBConstants.APPS_TYPE_JOY_LISTEN, null);
            .....
        }
    }
}

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值