LinearLayout中的TextView的一个WRAP_CONTENT相关的问题

探讨在安卓中,如何处理控件A与控件B的宽度分配问题,特别是在RecyclerView中使用时,控件A的宽度在回收视图后不更新的bug及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

很久前遇到个问题,如下图,先称“测试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的代码才能把最终的莫名其妙的行为预测出来。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值