ConstraintHelper中的一个坑

用过ConstraintLayout的都说好。
看看我另一篇ConstraintLayout的使用教程

但是插件化中,使用ConstraintLayout有点不如意了。
在1.1.3版本中,继承于ConstraintHelper的Group和Barrier都无法正常起效!之前需求急就没去管,反正是有其它办法做到这两个的功能的。

这两天研究了下原因以及解决方法。
看一下源码,调试一下,发现原因确实挺简单的。

我们直接用举例子的形式来分析好了。
首先,在xml定义Group,用constraint_referenced_ids标签来为Group添加一组ViewId,当前是tv_load_error,btn_reload

也可以通过group.setReferencedIds(int[]) 来添加。

    <androidx.constraintlayout.widget.Group
        android:id="@+id/group_load"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:constraint_referenced_ids="tv_load_error,btn_reload" />

也就是正常来说我们可以通过group_load这个Group来控制这两个view的显示与否。当发现通过group_load.setVisible()并未能起作用。

打了下断点,发现group_load的引用id列表为空!!这咋回事呢?

直击源码:
看下ConstraintHelper初始化:

    protected void init(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray a = this.getContext().obtainStyledAttributes(attrs, styleable.ConstraintLayout_Layout);
            int N = a.getIndexCount();

            for(int i = 0; i < N; ++i) {
                int attr = a.getIndex(i);
                if (attr == styleable.ConstraintLayout_Layout_constraint_referenced_ids) {
                    this.mReferenceIds = a.getString(attr);
                    this.setIds(this.mReferenceIds);
                }
            }
        }

    }

读取属性标签ConstraintLayout_Layout_constraint_referenced_ids获取字符串"tv_load_error,btn_reload" 然后执行setIds()

注意,tv_load_error 现在只是viewId相对应的名称,现在我们需要通过这个名称去获取viewId (例如:0x7fxxxxxx)

    private void setIds(String idList) {
        if (idList != null) {
            int begin = 0;
            while(true) {
                int end = idList.indexOf(',', begin);
                if (end == -1) {
                    this.addID(idList.substring(begin));
                    return;
                }
                this.addID(idList.substring(begin, end));
                begin = end + 1;
            }
        }
    }

就是对tv_load_errorbtn_reload分别执行addID()

    private void addID(String idString) {
        if (idString != null) {
            if (this.myContext != null) {
                idString = idString.trim();
                int tag = 0;

                //1
                try {
                    Class res = id.class;
                    Field field = res.getField(idString);
                    tag = field.getInt((Object)null);
                } catch (Exception var5) {
                }
                
                //2
                if (tag == 0) {
                    tag = this.myContext.getResources().getIdentifier(idString, "id", this.myContext.getPackageName());
                }

                //3
                if (tag == 0 && this.isInEditMode() && this.getParent() instanceof ConstraintLayout) {
                    ConstraintLayout constraintLayout = (ConstraintLayout)this.getParent();
                    Object value = constraintLayout.getDesignInformation(0, idString);
                    if (value != null && value instanceof Integer) {
                        tag = (Integer)value;
                    }
                }
                if (tag != 0) {
                    this.setTag(tag, (Object)null);
                } else {
                    Log.w("ConstraintHelper", "Could not find id of \"" + idString + "\"");
                }

            }
        }
    }
    
    public void setTag(int tag, Object value) {
        if (this.mCount + 1 > this.mIds.length) {
            this.mIds = Arrays.copyOf(this.mIds, this.mIds.length * 2);
        }

        this.mIds[this.mCount] = tag;
        ++this.mCount;
    }

通过注释,有三个地方可以获得tag, 只要tag!=0 那么就可以执行setTag(), 这时group才算是真正持有了tv_load_errorbtn_reload相对应的viewId。

注释1是什么情况呢?

直接读id.class 即: androidx.constraintlayout.widget.R.id;
这个由ConstrainLayout库生成的R文件显然是不会有我们自己定义的id。

在build中找到这个文件

    public static final class id {
        public static final int bottom = 0x7f09006f;
        public static final int end = 0x7f0900cc;
        public static final int gone = 0x7f0900ef;
        public static final int invisible = 0x7f090133;
        public static final int left = 0x7f090144;
        public static final int packed = 0x7f09021d;
        public static final int parent = 0x7f090224;
        public static final int percent = 0x7f090227;
        public static final int right = 0x7f09025c;
        public static final int spread = 0x7f090299;
        public static final int spread_inside = 0x7f09029a;
        public static final int start = 0x7f09029f;
        public static final int top = 0x7f0902cf;
        public static final int wrap = 0x7f0902fd;
    }

emmm... 就是几个通用值,显然没有我们要找的tv_load_error

再来看注释3

是的,先看3,
毕竟this.isInEditMode()是否足以让我们直接排除3了,毕竟现在是处于运行时,而不是编辑模式。

注释2

所以我们现在集中精力,来看注释2,花上2分钟攻克它。

就一行代码

tag = this.myContext.getResources().getIdentifier(idString, "id", 
            this.myContext.getPackageName());

idString是tv_error, this.myContext.getPackageName()是包名.假设为 com.handsome.isme

这行代码会从com.handsome.isme这个应用的资源中查找名称为tv_error的id值。

正常情况下,是可以找到。除非...

是的,就是插件化的原因。我定义的这个tv_error所在的插件的包名id是与宿主包名是不同,而我们这里仅是从宿主中寻找tv_error, 那肯定找不到了..

也不能说肯定找不到..万一宿主也定义了一个tv_error,但是两者id肯定不同,所以也是毫无作用的,万一findViewById没判断,还就得崩了呢...

所以咋办呢?

解决方法:

  1. 思路简直不要太简单了
    val tvError = contentView.findViewById<View>(R.id.tv_load_error)
    val reloadBtn = contentView.findViewById<View>(R.id.btn_reload)
    group = contentView.findViewById(R.id.group_load) 
    group?.referencedIds = intArrayOf(tvError.id, reloadBtn.id)

初始化读不到值,那我手动赋值总行了吧...

  1. 自定义Group。

在addID()那一个方法中,修改注释2的获取ViewId的实现。

a. 当从宿主中找不到此viewId时,遍历所有插件进行寻找(传入所有插件的包名)

不过,还是有问题的...就是可能找到其他插件的相同名称的viewId...

b. ConstraintHelper肯定是ConstraintsLayout的子View。通过获取parent,读取parent的所有子View的ViewId,通过ViewId反查其名称

    res = resources.getResourceEntryName(child.getId());

判断名称相等即可。

当然这种方法,可以直接修改ConstraintHelper文件的字节码来实现,这样我们就可以正常使用所有的ConstraintHelper控件了

  1. 过阵子我再告诉你

为什么是过阵子呢?因为只要升级ConstraintLayout至2.0就可以解决这个问题了。其解决方式就是方法2中b方案。
只是ConstraintLayout的2.0正式版本还没有发布。

推荐阅读更多精彩内容