在 RTL 中的自定义视图中断中测量 children

Measuring children in custom view breaks in RTL

我有一个扩展 LinearLayout 并实现 onMeasure 的自定义视图。我希望 children 尽可能宽,或者填充可用的 space。

XML 个文件: Parent:

<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:

 
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))
    }
}

在 LTR 中运行良好: 然而,在 RTL 中,视图由于某种原因被偏移并绘制在 ViewGroup 之外:

这个偏移量来自哪里?看起来 children 的测量调用正在添加到零件中,或者至少添加了一半...

如果canvas在onDraw方法中是rtl,你试过反转吗?

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

onDraw 方法覆盖和

val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL

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

您可以使用 Layout Inspector(或设备上的“显示布局边界”)来确定其行为的原因。水平偏移的计算可能需要翻转;通过减去而不是添加...以解决布局方向的变化,其中以像素为单位的绝对偏移可能始终被理解为 LTR。

在阅读了更多 LinearLayout & View 测量和布局代码后,我明白了为什么会这样。

每当LinearLayout#measure被调用时就会计算出mTotalLength,它代表计算出的整个view的宽度。由于我手动重新测量 children 与不同的 MeasureSpec LinearLayout 无法缓存这些值。稍后在 layout 传递中,视图使用缓存的 mTotalLength 设置 child 的左侧,即偏移量。左边是基于 child 的引力,因此受到缓存值的影响。

参见: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;
        }

我已经更改了 impl 以确保它始终将重力设置为 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))
    }
}

我不明白你为什么需要自定义 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)
}