如何在绘制前测量带权重的View?常规方法失效求解决方案
我完全懂你的痛点——带权重的View(比如LinearLayout里设置了weight属性的子View)因为需要父容器经过两次测量Pass才能确定最终尺寸,普通的View.measure()或者post()方法根本拿不到准确值,而onGlobalLayout/onPreDrawListener又会因为触发二次布局导致闪烁。下面给你两个可靠的解决方案,都是在绘制前完成测量和修改,不会出现闪烁问题:
方案一:自定义父容器,利用系统测量流程获取最终尺寸
最稳妥的方式是重写父容器(比如LinearLayout)的onMeasure方法,因为父容器的onMeasure完成后,所有子View(包括带权重的)的最终测量尺寸已经确定了,这时候你可以直接获取尺寸并修改子View的布局参数,整个过程都在绘制前完成,不会触发闪烁。
举个自定义LinearLayout的例子:
public class WeightSafeLinearLayout extends LinearLayout { public WeightSafeLinearLayout(Context context) { super(context); } public WeightSafeLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } public WeightSafeLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 先让系统完成完整的测量流程(包括权重的二次Pass) super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 这里可以安全地获取带权重子View的最终测量尺寸 View targetView = findViewById(R.id.your_weighted_view); if (targetView != null) { int finalWidth = targetView.getMeasuredWidth(); int finalHeight = targetView.getMeasuredHeight(); // 修改View的宽高(示例:给宽度增加100px) ViewGroup.LayoutParams params = targetView.getLayoutParams(); params.width = finalWidth + 100; params.height = finalHeight; targetView.setLayoutParams(params); // 因为修改了布局参数,需要重新触发一次测量(但此时还未进入绘制阶段,不会闪烁) super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } }
这个方法的核心是利用系统自带的测量流程,避免自己手动计算权重分配的复杂逻辑,同时所有操作都在onMeasure阶段完成,不会触发额外的绘制导致闪烁。
方案二:手动模拟父容器的权重测量流程
如果你不想自定义父容器,可以手动模拟LinearLayout的两次测量Pass,计算出带权重View的最终尺寸,然后调用measure()得到准确值。这个方法适合偶尔需要测量单个带权重View的场景:
public void getWeightedViewFinalSize(View weightedView) { ViewGroup parent = (ViewGroup) weightedView.getParent(); if (!(parent instanceof LinearLayout)) { // 非LinearLayout权重场景,直接测量 weightedView.measure( View.MeasureSpec.makeMeasureSpec(parent.getMeasuredWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(parent.getMeasuredHeight(), View.MeasureSpec.EXACTLY) ); return; } LinearLayout linearParent = (LinearLayout) parent; LinearLayout.LayoutParams llParams = (LinearLayout.LayoutParams) weightedView.getLayoutParams(); boolean isHorizontal = linearParent.getOrientation() == LinearLayout.HORIZONTAL; // 第一次测量:不考虑权重,获取子View的期望尺寸 int widthSpec, heightSpec; if (isHorizontal) { widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); heightSpec = View.MeasureSpec.makeMeasureSpec( linearParent.getMeasuredHeight() - llParams.topMargin - llParams.bottomMargin, View.MeasureSpec.EXACTLY ); } else { widthSpec = View.MeasureSpec.makeMeasureSpec( linearParent.getMeasuredWidth() - llParams.leftMargin - llParams.rightMargin, View.MeasureSpec.EXACTLY ); heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); } weightedView.measure(widthSpec, heightSpec); // 计算父容器剩余空间和总权重 int parentAvailable = isHorizontal ? linearParent.getMeasuredWidth() : linearParent.getMeasuredHeight(); float totalWeight = 0; int usedSpace = 0; for (int i = 0; i < linearParent.getChildCount(); i++) { View child = linearParent.getChildAt(i); LinearLayout.LayoutParams childParams = (LinearLayout.LayoutParams) child.getLayoutParams(); if (childParams.weight > 0) { totalWeight += childParams.weight; } else { usedSpace += isHorizontal ? (child.getMeasuredWidth() + childParams.leftMargin + childParams.rightMargin) : (child.getMeasuredHeight() + childParams.topMargin + childParams.bottomMargin); } } // 计算当前View的最终尺寸 int remainingSpace = parentAvailable - usedSpace; float weightRatio = llParams.weight / totalWeight; int finalSize = (int) (remainingSpace * weightRatio) - (isHorizontal ? (llParams.leftMargin + llParams.rightMargin) : (llParams.topMargin + llParams.bottomMargin)); // 第二次测量:设置最终的MeasureSpec if (isHorizontal) { widthSpec = View.MeasureSpec.makeMeasureSpec(finalSize, View.MeasureSpec.EXACTLY); } else { heightSpec = View.MeasureSpec.makeMeasureSpec(finalSize, View.MeasureSpec.EXACTLY); } weightedView.measure(widthSpec, heightSpec); // 现在可以通过weightedView.getMeasuredWidth()/getMeasuredHeight()拿到准确值 int finalWidth = weightedView.getMeasuredWidth(); int finalHeight = weightedView.getMeasuredHeight(); // 在这里修改View的宽高即可 }
为什么这个方法有效?
LinearLayout处理权重时会分两次测量:第一次测量所有子View的期望尺寸,计算剩余空间;第二次根据权重比例分配剩余空间给带权重的子View。这个方法手动模拟了这个流程,所以能拿到和系统测量一致的最终尺寸,而且整个过程不需要等待布局完成,不会触发闪烁。
内容的提问来源于stack exchange,提问作者OmerCohen1994

