在RTL(从右到左)布局下,在自定义视图中测量儿童会出现问题。

3

我有一个扩展了LinearLayout并实现了onMeasure的自定义视图。我希望子视图可以根据需要设置宽度,或者填充可用空间。

XML文件:

父视图:

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.myapplication.AtMostLinearLayout
        android:id="@+id/at_most_linear_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</FrameLayout>

按钮示例:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="center"
        android:src="@android:drawable/ic_delete" />
</FrameLayout>

视图可以通过编程方式添加,例如:

   findViewById<AtMostLinearLayout>(R.id.at_most_linear_layout).apply {
            repeat(4) {
                LayoutInflater.from(context).inflate(R.layout.button, this)
            }
        }

最后是自定义视图类:

 
class AtMostLinearLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {
    private val maxTotalWidth = context.resources.getDimensionPixelOffset(R.dimen.max_buttons_width)
    init {
        orientation = HORIZONTAL
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (childCount < 1) return

        val newWidth = min(measuredWidth, maxTotalWidth)
        var availableWidth = newWidth
        var numberOfLargeChildren = 0
        repeat(childCount) {
            getChildAt(it).let { child ->
                if (child.measuredWidth > availableWidth / childCount) {
                    availableWidth -= child.measuredWidth
                    numberOfLargeChildren++
                }
            }
        }

        val minChildWidth = availableWidth / max(childCount - numberOfLargeChildren, 1)
        repeat(childCount) {
            getChildAt(it).apply {
                measure(
                    MeasureSpec.makeMeasureSpec(max(measuredWidth, minChildWidth), EXACTLY),
                    UNSPECIFIED
                )
            }
        }

        setMeasuredDimension(
            makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(measuredHeight, EXACTLY))
    }
}

在从左到右的情况下它工作得很好:从左到右的截图 但是在从右到左的情况下,视图会因为某些原因偏移并且绘制在了ViewGroup的外部:从右到左的截图

这个偏移可能来自哪里?看起来子元素的测量调用被添加到了该部分中,或者至少加了一半…

4个回答

2
您可以使用布局检查器(或设备上的“显示布局边界”)来确定其行为原因。水平偏移量的计算可能需要翻转;通过减去而不是加上...以考虑布局方向的变化,其中像素中的绝对偏移量始终可以理解为从左到右。请注意保留HTML标签。

问题是,我没有添加任何偏移量。我只是重新测量子项,以确保它们至少占用所需的空间,以填充整个线性布局。在RTL中,最后一个子项在onLayout中接收到的左侧位置超过了Viewgroup的一半。我似乎无法弄清楚这个左侧位置是在哪里计算的。 - Novae

1

如果在onDraw方法中画布是从右到左的,您是否尝试过翻转它?

您可以尝试使用View.getLayoutDirection()来返回布局方向。

重写onDraw方法并实现以上操作。

val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL

if(isRtrl){
  canvas.scale(-1f, 1f, width / 2, height / 2)
}

不幸的是,这只是问题的反向 :) - Novae

0

在进一步阅读LinearLayout和View的测量和布局代码后,我发现了为什么会出现这种情况。

每当调用LinearLayout#measure时,都会计算mTotalLength,它表示整个视图的计算宽度。由于我正在使用不同的MeasureSpec手动重新测量子项,因此LinearLayout无法缓存这些值。稍后在layout过程中,视图使用缓存的mTotalLength来设置子项的左侧即偏移量。左侧基于子项的重力而受到缓存值的影响。

参见:LinearLayout#onlayout

        final int layoutDirection = getLayoutDirection();
        switch (Gravity.getAbsoluteGravity(majorGravity, layoutDirection)) {
            case Gravity.RIGHT:
                // mTotalLength contains the padding already
                childLeft = mPaddingLeft + right - left - mTotalLength;
                break;

            case Gravity.CENTER_HORIZONTAL:
                // mTotalLength contains the padding already
                childLeft = mPaddingLeft + (right - left - mTotalLength) / 2;
                break;

            case Gravity.LEFT:
            default:
                childLeft = mPaddingLeft;
                break;
        }

我已经更改了实现方式,以确保它始终将重力设置为Gravity.LEFT。我应该手动实现onLayout

class AtMostLinearLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {
    private val maxTotalWidth = context.resources.getDimensionPixelOffset(R.dimen.max_buttons_width)
    init {
        orientation = HORIZONTAL
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (childCount < 1) return

        val newWidth = min(measuredWidth, maxTotalWidth)
        var availableWidth = newWidth
        var numberOfLargeChildren = 0
        repeat(childCount) {
            getChildAt(it).let { child ->
                if (child.measuredWidth > availableWidth / childCount) {
                    availableWidth -= child.measuredWidth
                    numberOfLargeChildren++
                }
            }
        }

        val minChildWidth = availableWidth / max(childCount - numberOfLargeChildren, 1)
        repeat(childCount) {
            getChildAt(it).let { child ->
                child.measure(
                    makeMeasureSpec(max(child.measuredWidth, minChildWidth), EXACTLY),
                    UNSPECIFIED
                )
            }
        }

        // Effectively always set it to Gravity.LEFT to prevent LinearLayout using its
        // internally-cached mTotalLength to set the Child's left.
        gravity = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) Gravity.END else Gravity.START
        setMeasuredDimension(
            makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(measuredHeight, EXACTLY))
    }
}

0

我不明白为什么需要自定义ViewGroup来完成这项工作。当您将子视图添加到LinearLayout中时,如何设置layout_weight

只需简单操作:

val layout = findViewById<LinearLayout>(R.id.linear_layout)
repeat(4) {
  val view = LayoutInflater.from(this).inflate(R.layout.button, layout, false)
  view.apply {
    updateLayoutParams<LinearLayout.LayoutParams> {
      width = 0
      weight = 1f
    }
  }
  layout.addView(view)
}

权重均匀分配宽度,不考虑子元素所需的宽度。 - Novae

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接