在 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 的引力,因此受到缓存值的影响。
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)
}
我有一个扩展 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 中运行良好:
这个偏移量来自哪里?看起来 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 的引力,因此受到缓存值的影响。
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)
}