ViewStub原理,解决约束布局( constraintLayout)ViewStub约束失效问题,解决ViewStub约束失效约束不起作用的问题

解决ViewStub约束布局 约束失效的问题

1.背景

在项目中进行性能优化或包体积控制的时候,就要进行xml或布局优化。
通常我们进行布局优化或xml加载优化的时候,就不得不提到到的几个标签mergeincludeViewStub,这几个标签作用不做赘述,重点讲一下本期的ViewStub

ViewStub使用场景是,需要复用layout但第一时间用不到,使用ViewStub做延迟加载。其原理特性就是:将目标layout引入后,替换自己,并将自己的属性LayoutParams设置给目标View/layout

ViewStub在使用的时候,有两种方式:

  1. 在在xml中通过layout属性引入目标layout;
  2. 或者在代码中动态使用setLayoutResource注入;

最后在代码调用ViewStub.inflate()ViewStub.setVisibility(View.VISIBLE)使引入的布局替换ViewStub。

2.问题

上面使用方式没有任何问题,使用起来也非常快捷方便,但问题也随之而来。

  • 一定要有一个layout
    ViewStub引入布局,一定要有一个额外的Layout,即使是Layout里面只有一个View,也需要单独为它提供一个Layout;

  • 它只能引入Layout
    有时候我们在代码中,需要动态的添加一个View,如果ViewGroup比较简单例如线性布局或者FrameLayout还好收,但是复杂的View,相对布局就需要动态设置Rule,而约束布局就相对来说比较复杂了,不是太了解约束布局动态更改约束规则的朋友就头疼了(我是自己研究了一下午才完全搞明白的),需要动态的加好几行代码控制约束,碰到View动态变换位置的场景更是要写好多代码。

于是,我就在想,如何解决这两个问题呢?为什么不能让ViewStub 把这两者结合呢

为什么ViewStub不能用在动态添加View,结合自己的特性,把ViewStub本身的layoutParams设置给动态添加的View呢?

3.寻找可行性

带着上面的问题,我重新看了一下它的源码。

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

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

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

    @IdRes
    public int getInflatedId() {
        return mInflatedId;
    }

    /**
     * Defines the id taken by the inflated view. If the inflated id is
     * {@link View#NO_ID}, the inflated view keeps its original id.
     * 为引入的layout设置一个id
     */
    @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;
    }

    /**
     * Specifies the layout resource to inflate when this StubbedView becomes visible or invisible
     * or when {@link #inflate()} is invoked. The View created by inflating the layout resource is
     * used to replace this StubbedView in its parent.
     * 注入需要引入的layoutId
     */
    @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) {
    }

    /**
     * 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.
     * 设置visibility  为VISIBLE 或者INVISIBLE时,会直接引入布局
     */
    @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) {
        	//把目标view按照自身index添加到parent中,并把自身的layoutParams设置到目标View。
            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;
                //通过LayoutInflater 引入目标布局,inAttachToParent 为false。
                final View view = inflateViewNoAdd(parent);
                //移除ViewStub自身,并添加目标view到同样的位置,将自身的layoutParams设置给目标View
                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代码非常简单,相信各位大佬都能看懂;

  • 构造函数里setVisibility(GONE)setWillNotDraw(true)方法还有drawdispatchDraw()方法重写后是空实现我们可知,ViewStub没有被绘制出来,所以不会消耗过多性能。
  • inflate()作用就是引入目标布局但不直接加入parant,而是将自身从parent中移除,然后按照同样的viewIndex把目标View加入到parent中,并把自身的LayoutParams设置给目标View;

原理就是这么简单。

那么,如何解决我们的痛点呢?

4.解决只能引入layout不能动态添加View的问题

重新看一下inflate()方法:

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

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                //通过LayoutInflater 引入目标布局,inAttachToParent 为false。
                final View view = inflateViewNoAdd(parent);
                //移除ViewStub自身,并添加目标view到同样的位置,将自身的layoutParams设置给目标View
                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");
        }
    }

这里替换分为两步:

  • 1.使用LayoutInflate把layout引入;
  • 2.第一步引入的View替换自身;

是不是思路就来了,我完全可以多态出来一个inflate(View view)方法,把我们动态构造的View传进去啊!!

那我是不是可以自定义一个View继承ViewStub呢?😏😏😏

说干就干!

但当我兴致勃勃的去继承ViewStub时候,编译器却报错了,才发现ViewStub有一个注解@RemoteView,它不可以被继承。

那软的不行,来硬的,我直接自己实现一份(其实是就是ctrl+c,ctrl+v😏😏)可以了吧!



import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;

import com.avatr.ivi.enoch.R;
import com.avatr.ivi.enochsdk.base.tools.L;

import java.lang.ref.WeakReference;

public class ViewRocket extends View {
    private static final L.Tag TAG = new L.Tag("ViewRocket");
    private Context mContext;
    private WeakReference<View> mInflatedViewRef;
    @LayoutRes
    private int mLayoutResource;
    @IdRes
    private int mInflatedId = NO_ID;
 	private boolean mIsLaunched = false;

    public ViewRocket(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
        mContext = context;
    }

    public ViewRocket(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewRocket);
        mLayoutResource = a.getResourceId(R.styleable.ViewRocket_layout, 0);
        mInflatedId = a.getResourceId(R.styleable.ViewRocket_inflatedId, NO_ID);
        setVisibility(GONE);
        setWillNotDraw(true);
    }

 	//省略不必要代码

    public View launch() {
        if (mLayoutResource == 0) {
            L.e(TAG, "launch: ");
            return null;
        }
        return launch(mLayoutResource);
    }

    public View launch(@LayoutRes int layoutRes) {
        final View view = inflateViewNoAdd((ViewGroup) getParent(), layoutRes);
        launch(view);
        return view;
    }


    public void launch(View view) {
        if (mIsLaunched) {
            L.e(TAG, "sendView: The rocket is disposable and cannot be fired repeatedly");
            return;
        }

        final ViewGroup parent = (ViewGroup) getParent();
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);
        //如果View没有id,就给view生成一个id
        if (view.getId() == View.NO_ID) {
            mInflatedId = View.generateViewId();
            view.setId(mInflatedId);
        }

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

        mIsLaunched = true;
        mInflatedViewRef = new WeakReference<>(view);
    }

    private View inflateViewNoAdd(ViewGroup parent, int layoutRes) {
        final LayoutInflater factory = LayoutInflater.from(mContext);
        final View view = factory.inflate(layoutRes, parent, false);

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

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

    public boolean isLaunched() {
        return mIsLaunched;
    }
}

attr属性,兼容ViewStub本身的属性:

	<declare-styleable name="ViewRocket">
        <attr name="inflatedId" format="reference" />
        <attr name="layout" format="reference" />
    </declare-styleable>

方法几乎和ViewStub一模一样,但是我扩展了几个方法:


/**
* 在布局中设置了layout,或通过调用setLayoutResource后调用。
*/
	public View launch() {
        if (mLayoutResource == 0) {
            L.e(TAG, "launch: ");
            return null;
        }
        return launch(mLayoutResource);
    }
	
	/**
	* 直接引入布局,不需要在xml或调用setLayoutResource设置layoutId
	*/
    public View launch(@LayoutRes int layoutRes) {
        final View view = inflateViewNoAdd((ViewGroup) getParent(), layoutRes);
        launch(view);
        return view;
    }

	/**
	* 核心方法,可以在内部或外部,直接传入目标View
	*/
   public void launch(View view) {
        if (mIsLaunched) {
            L.e(TAG, "sendView: The rocket is disposable and cannot be fired repeatedly");
            return;
        }

        final ViewGroup parent = (ViewGroup) getParent();
        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);
        }

        mIsLaunched = true;
        mInflatedViewRef = new WeakReference<>(view);
    }

可以看到,关键方法我加了参数,这样不管在内部或外部都可以直接调用传入目标View,这样我们在实际使用就变成这样:

  • 1.在布局中使用ViewRocket和ViewStub同样的方式占位;
  • 2.在代码中:
View view = new CustomComplicateView(context);
viewRocket.launch(view);

怎么样,是不是如此的丝滑柔顺!这样就解决了我们的第一个痛点,动态添加构造出来的View,不需要额外添加一个xml!

如此解决了这个问题,我开心的大喊,我真是个小聪明!

5. 随之而来的问题!

当我开开心心的在项目中使用时候,突然发现出问题了。

我的场景是,约束布局中有两个自定义View A,B使用ViewRocket引入,A、B有依赖关系。正常操作就是B的ViewRocket依赖A的ViewRocket,但是我这么用的时候,launch之后发现出问题了。两个View位置根本不对!
吓我一跳,有问题?我赶紧替换了ViewStub重试,发现ViewStub和我一样的问题。

什么原理呢?

正好我前两天研究过约束布局,知道动态建立约束需要connect目标View的Id;另外我们在launch时候,如果不主动设置inflateId或之前给目标View设置过id,目标View是没有id的!

也就是说,我的ViewRocket在引入目标view之后,和ViewRocket的约束关系,没有转移到新添加的VIew上!所以导致布局出现错乱。

那么,我是不是可以把ViewRocket的id设置给新的View呢?

6.解决约束布局约束失效

 public void launch(View view) {
        if (mIsLaunched) {
            L.e(TAG, "sendView: The rocket is disposable and cannot be fired repeatedly");
            return;
        }

        final ViewGroup parent = (ViewGroup) getParent();
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);
        //如果parentView是约束布局,把自身id设置给运送的View
        if (parent instanceof ConstraintLayout) {
            mInflatedId = this.getId();
            view.setId(mInflatedId);
        }
        //如果View没有id,就给view生成一个id
        if (view.getId() == View.NO_ID) {
            mInflatedId = View.generateViewId();
            view.setId(mInflatedId);
        }

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

        mIsLaunched = true;
        mInflatedViewRef = new WeakReference<>(view);
    }

在项目里重新尝试,可以了!!!完美,哈哈哈哈

7.总结

一顿操作,就解决了ViewStub的两个问题:

  • 只能引入layout而不能动态添加new出来的View的问题;
  • 解决约束布局约束失效的问题。

其实这个这个问题比较简单,相信各位大佬轻松可以想到解决方案。不过不管问题是复杂还是简单,作为一个小趴菜,最重要的还是要有发现问题是,自己去努力探索去尝试的态度。每天都去靠自己的努力和思考解决问题,这样就能不断进步,不断提高,直到有一天能和各位大佬一样,嘿嘿嘿😏😏😏

完整代码:


import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;

import com.avatr.ivi.enoch.R;
import com.avatr.ivi.enochsdk.base.tools.L;

import java.lang.ref.WeakReference;

/**
 * Created By : Wilson.lu
 * Created At : 2022/12/2 at 22:16
 * Name       : View火箭.因为像一次性火箭把人送到空间站,所以命名
 * Abilities  : ViewStub的Pro版本;
 * <p>
 * 支持ViewStub本身的功能;
 * 可以动态添加单个View,不用再为单个View写一个xml布局
 * 修复约束布局使用ViewStub 约束失效的问题;
 * <p>
 * <p>
 * 用法eg:
 *
 * <p>
 * viewRocket.setLayoutResource(R.layout.layout_any_layout);
 * viewRocket.launch();
 * <p>
 * View customView = new CustomView(context);
 * viewRocket.launch(customView)
 * <p>
 * viewRocket.launch(R.layout.layout_any_layout);
 * <p>
 * 注意:暂时不支持<merge>标签
 */
public class ViewRocket extends View {
    private static final L.Tag TAG = new L.Tag("ViewRocket");
    private Context mContext;
    private WeakReference<View> mInflatedViewRef;
    @LayoutRes
    private int mLayoutResource;
    @IdRes
    private int mInflatedId = NO_ID;


    public ViewRocket(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
        mContext = context;
    }

    public ViewRocket(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewRocket);
        mLayoutResource = a.getResourceId(R.styleable.ViewRocket_layout, 0);
        mInflatedId = a.getResourceId(R.styleable.ViewRocket_inflatedId, NO_ID);
        setVisibility(GONE);
        setWillNotDraw(true);
    }

    public void setInflatedId(@IdRes int inflatedId) {
        mInflatedId = inflatedId;
    }

    /**
     * 获取要运送的View的id,没有launch之前获取不到真正的值
     *
     * @return
     */
    public int getInflatedId() {
        return mInflatedId;
    }

    public void setLayoutResource(@LayoutRes int layoutResource) {
        mLayoutResource = layoutResource;
    }

    public View launch() {
        if (mLayoutResource == 0) {
            L.e(TAG, "launch: ");
            return null;
        }
        return launch(mLayoutResource);
    }

    public View launch(@LayoutRes int layoutRes) {
        final View view = inflateViewNoAdd((ViewGroup) getParent(), layoutRes);
        launch(view);
        return view;
    }


    public void launch(View view) {
        if (mIsLaunched) {
            L.e(TAG, "sendView: The rocket is disposable and cannot be fired repeatedly");
            return;
        }

        final ViewGroup parent = (ViewGroup) getParent();
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);


        if (parent instanceof ConstraintLayout) {
            //如果parentView是约束布局,把自身id设置给运送的View
            mInflatedId = this.getId();
            view.setId(mInflatedId);
        }
        //如果View没有id,就给view生成一个id
        if (view.getId() == View.NO_ID) {
            mInflatedId = View.generateViewId();
            view.setId(mInflatedId);
        }

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

        mIsLaunched = true;
        mInflatedViewRef = new WeakReference<>(view);
    }

    private View inflateViewNoAdd(ViewGroup parent, int layoutRes) {
        final LayoutInflater factory = LayoutInflater.from(mContext);
        final View view = factory.inflate(layoutRes, parent, false);

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

    public void setVisibility(int visibility) {
        // 当真正的布局文件被加载之后
        if (mInflatedViewRef != null) {
            // 获取到当前的View
            View view = mInflatedViewRef.get();
            if (view != null) {
                //操纵当前View的可见行
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else {
            //没有调用inflate的话,会设置可见性
            super.setVisibility(visibility);
            //当 当前设置可见性为 VISIBLE或者INVISIBLE的时候,会调用inflate方法。
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                launch();
            }
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(null);
    }

    private boolean mIsLaunched = false;

    public boolean isLaunched() {
        return mIsLaunched;
    }
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值