很久前遇到个问题,如下图,先称“测试Q医生团队”为控件A,“(成员1人)”为控件B。
产品要求控件A宽度可变分摊剩余空间,控件B占用宽度较少且与控件A相邻。
这个需求看起来也不难,怎么做呢,自己想了个方法:
方法1:
先对B控件进行setText,测量出B的宽度后可以计算出控件A可以用的最大宽度max,而A控件setText之后的宽度记为w,则A控件最终的宽度应该等于math.min(max,w),即如果w较大则A的宽度就是max,如果max较大则WRAP_CONTENT就好了。
实现这个功能需要写一个自定义TextView,重写它的onMeasure方法。置于怎么写就不在此文赘述了。
但这种方法有个缺点就是需要先测量一波,真正的绘制发生在第二个绘制循环导致及时性很差。
方法2:
对于方法1进行改进,使用TextView的StaticLayout测量出控件A和控件B的宽度,动态地控制控件A的宽度。
这种方法解决了不及时的问题,但是还是挺麻烦
方法3:
同事抖了个机灵:
创建布局C,C在签约按钮的左侧,使用剩余宽度,C的宽度基本是固定的。
把A、B放在一个横向线性布局D里,D的layout_width为WRAP_CONTENT,
A的layout_width为0,layout_weight为1,B的layout_width为WRAP_CONTENT。
这个布局挺怪异的,有种说不清鸡生蛋还是蛋生鸡的问题,但是代码好像运行良好,写法又简单,所以我也用了起来,置于原理就没有深究。
最近这个问题又出现了:
产品要求把这个布局放在列表里面,于是我把它放到了RecyclerView里面,可以bug出现了,滑动之后,控件A的宽度出现了问题:控件A在使用回收的视图之后宽度似乎不会进行改变,显示发生了错乱。
定位:
最开始是怀疑RecyclerView的问题,怀疑其拦截requestLayout导致子view无法绘制,于是debug追踪,却发现在bindView之后,控件D的onMeasure执行正常,先排除RecyclerView的问题。
第一次出现的itemView显示是正常的,而使用回收掉的视图的控件则不正常,于是比较两者的差异,发现:
第一次显示时,执行bindView之后,控件D执行onMeasure期间会触发控件A的onMeasure,而使用回收的视图则不会调用控件A的onMeasure方法。两个流程都会调用控件A的measure方法。
贴一段View#measure()方法的代码:
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
可以看出只要forceLayout为true就会触发onMeasure进行重新测量。
在setText之后增加requsetLayout方法的调用,问题得到解决。
思考:
setText为什么没有触发requestLayout呢?
在D进行测量期间,会先调用A进行测量,A和B都确定了D才确定,因为回收的原因,A已经测量过一次了,A使用剩余宽度,所以在setText时也认为自己没必要刷新。
会出现这种问题主要也是这种写法比较怪异,D需要先知道A的宽度,而A却要使用剩余的宽度,只有理解了onMeasure的代码才能把最终的莫名其妙的行为预测出来。