У меня есть собственное представление, которое расширяет 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))
}
}
В LTR работает нормально:
Однако в RTL представления по какой-то причине смещены и отображаются вне ViewGroup: 
Откуда могло взяться это смещение? Похоже, в часть добавляются звонки для детей, или, по крайней мере, половина...
Если холст rtl в методе onDraw, вы пытались его инвертировать?
Вы можете попробовать использовать Вид.getLayoutDirection()., чтобы вернуть направление компоновки.
Переопределение метода onDraw и
val isRtl = getLayoutDirection == View.LAYOUT_DIRECTION_RTL
if (isRtrl){
canvas.scale(-1f, 1f, width / 2, height / 2)
}
Вы можете использовать Инспектор макетов (или «показать границы макета» на устройстве), чтобы определить, почему он ведет себя именно так. Расчет смещения по горизонтали, возможно, придется перевернуть; путем вычитания вместо добавления ... для учета изменения направления макета, где абсолютное смещение в пикселях всегда будет пониматься как LTR.
Дело в том, что я не добавляю никакого смещения. Я просто перемеряю дочерние элементы, чтобы убедиться, что они занимают как минимум то пространство, которое им нужно для заполнения всего linearLayout. Последний дочерний элемент в RTL получает левый элемент onLayout, который составляет чуть более половины Viewgroup. Кажется, я не могу понять, где это левое вычисляется.
После прочтения меры LinearLayout & View и кода макета я понял, почему это происходит.
Всякий раз, когда вызывается LinearLayout#measure, вычисляется mTotalLength, который представляет рассчитанную ширину всего представления. Поскольку я вручную пересчитываю дочерние элементы с другим MeasureSpecLinearLayout, я не могу кэшировать эти значения. Позже в проходе layout представления используют кешированный mTotalLength для установки левого дочернего элемента, то есть смещения. Левый основан на гравитации ребенка и, таким образом, зависит от кэшированного значения.
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))
}
}
К сожалению, это просто переворачивает проблему :)