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_error
和btn_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_error
和btn_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没判断,还就得崩了呢...
所以咋办呢?
解决方法:
- 思路简直不要太简单了
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)
初始化读不到值,那我手动赋值总行了吧...
- 自定义Group。
在addID()那一个方法中,修改注释2的获取ViewId的实现。
a. 当从宿主中找不到此viewId时,遍历所有插件进行寻找(传入所有插件的包名)
不过,还是有问题的...就是可能找到其他插件的相同名称的viewId...
b. ConstraintHelper肯定是ConstraintsLayout的子View。通过获取parent,读取parent的所有子View的ViewId,通过ViewId反查其名称
res = resources.getResourceEntryName(child.getId());
判断名称相等即可。
当然这种方法,可以直接修改ConstraintHelper文件的字节码来实现,这样我们就可以正常使用所有的ConstraintHelper控件了
- 过阵子我再告诉你
为什么是过阵子呢?因为只要升级ConstraintLayout至2.0就可以解决这个问题了。其解决方式就是方法2中b方案。
只是ConstraintLayout的2.0正式版本还没有发布。