Пользовательский вид onMeasure: как получить ширину по высоте

Предыдущая версия моего вопроса была слишком многословной. Люди не могли это понять, поэтому следующее переписывание. Просмотрите историю изменений, если вас интересует старая версия.

Родитель RelativeLayout отправляет MeasureSpec s методу onMeasure своего дочернего представления, чтобы увидеть, насколько большой ребенок хотел бы быть. Это происходит в нескольких проходах.

Мое пользовательское представление

У меня есть пользовательский вид . По мере увеличения содержимого представления высота представления увеличивается. Когда представление достигает максимальной высоты, которую разрешает родитель, ширина представления увеличивается для любого дополнительного содержимого (если для ширины выбрано значение wrap_content ). Таким образом, ширина пользовательского представления напрямую зависит от того, какой родитель говорит, что максимальная высота должна быть.

Введите описание изображения здесь

Беспокойная беседа родителей

onMeasure pass 1

Родитель RelativeLayout сообщает мое мнение: «Вы можете быть любой шириной до 900 и любой высотой до 600 Что вы говорите?»

Мое мнение гласит: «Ну, на этой высоте, я могу поместиться все с шириной 100 Поэтому я возьму ширину 100 и высоту 600 ».

onMeasure pass 2

Родитель RelativeLayout сообщает мое мнение: «В прошлый раз вы сказали мне, что хотите ширину в 100 , поэтому давайте установим ее как точную ширину. Теперь, исходя из этой ширины, какая высота вам нужна? ОК."

"Привет!" Мой взгляд отвечает. «Если вы только даете мне максимум 500 , то 100 слишком узкий. Мне нужна ширина 200 для этой высоты. Но, отлично, сделайте это по-своему. Я не нарушу правила (пока … ) . Я возьму ширину в 100 и 500 .

Конечный результат

Родитель RelativeLayout присваивает виду конечный размер 100 для ширины и 500 для высоты. Это, конечно, слишком узкое для просмотра, и часть содержимого обрезается.

«Вздох», – думает мой взгляд. «Почему мой родитель не позволит мне быть шире? Существует много места. Может быть, кто-то из Stack Overflow может дать мне несколько советов».

    Обновление: измененный код для исправления некоторых вещей.

    Во-первых, позвольте мне сказать, что вы задали большой вопрос и поставили проблему очень хорошо (дважды!) Вот мое решение:

    Похоже, что с onMeasure многое происходит, что на onMeasure взгляд не имеет большого смысла. Так как это так, мы будем запускать onMeasure как он будет, и в конце вынести суждение о границах onLayout в onLayout , установив mStickyWidth на новую минимальную ширину, которую мы примем. В onPreDraw , используя ViewTreeObserver.OnPreDrawListener , мы ViewTreeObserver.OnPreDrawListener другой макет ( requestLayout ). Из документации (выделено мной):

    boolean onPreDraw ()

    Метод обратного вызова, который будет вызываться, когда дерево просмотра будет нарисовано. На этом этапе все представления в дереве были измерены и заданы рамкой. Клиенты могут использовать это, чтобы настроить свои полосы прокрутки или даже запросить новый макет перед тем, как нарисовать рисунок .

    Новая минимальная ширина, установленная в onLayout , теперь будет принудительно применяться onMeasure которая теперь умнее о том, что возможно.

    Я проверил это с вашим примером кода и, похоже, работает нормально. Это потребует гораздо большего тестирования. Могут быть другие способы сделать это, но это суть подхода.

    CustomView.java

     import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewTreeObserver; public class CustomView extends View implements ViewTreeObserver.OnPreDrawListener { private int mStickyWidth = STICKY_WIDTH_UNDEFINED; public CustomView(Context context) { super(context); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { logMeasureSpecs(widthMeasureSpec, heightMeasureSpec); int desiredHeight = 10000; // some value that is too high for the screen int desiredWidth; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; // Height if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(desiredHeight, heightSize); } else { height = desiredHeight; } // Width if (mStickyWidth != STICKY_WIDTH_UNDEFINED) { // This is the second time through layout and we are trying renogitiate a greater // width (mStickyWidth) without breaking the contract with the View. desiredWidth = mStickyWidth; } else if (height > BREAK_HEIGHT) { // a number between onMeasure's two final height requirements desiredWidth = ARBITRARY_WIDTH_LESSER; // arbitrary number } else { desiredWidth = ARBITRARY_WIDTH_GREATER; // arbitrary number } if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(desiredWidth, widthSize); } else { width = desiredWidth; } Log.d(TAG, "setMeasuredDimension(" + width + ", " + height + ")"); setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int w = right - left; int h = bottom - top; super.onLayout(changed, left, top, right, bottom); // Here we need to determine if the width has been unnecessarily constrained. // We will try for a re-fit only once. If the sticky width is defined, we have // already tried to re-fit once, so we are not going to have another go at it since it // will (probably) have the same result. if (h <= BREAK_HEIGHT && (w < ARBITRARY_WIDTH_GREATER) && (mStickyWidth == STICKY_WIDTH_UNDEFINED)) { mStickyWidth = ARBITRARY_WIDTH_GREATER; getViewTreeObserver().addOnPreDrawListener(this); } else { mStickyWidth = STICKY_WIDTH_UNDEFINED; } Log.d(TAG, ">>>>onLayout: w=" + w + " h=" + h + " mStickyWidth=" + mStickyWidth); } @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); if (mStickyWidth == STICKY_WIDTH_UNDEFINED) { // Happy with the selected width. return true; } Log.d(TAG, ">>>>onPreDraw() requesting new layout"); requestLayout(); return false; } protected void logMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); String measureSpecHeight; String measureSpecWidth; if (heightMode == MeasureSpec.EXACTLY) { measureSpecHeight = "EXACTLY"; } else if (heightMode == MeasureSpec.AT_MOST) { measureSpecHeight = "AT_MOST"; } else { measureSpecHeight = "UNSPECIFIED"; } if (widthMode == MeasureSpec.EXACTLY) { measureSpecWidth = "EXACTLY"; } else if (widthMode == MeasureSpec.AT_MOST) { measureSpecWidth = "AT_MOST"; } else { measureSpecWidth = "UNSPECIFIED"; } Log.d(TAG, "Width: " + measureSpecWidth + ", " + widthSize + " Height: " + measureSpecHeight + ", " + heightSize); } private static final String TAG = "CustomView"; private static final int STICKY_WIDTH_UNDEFINED = -1; private static final int BREAK_HEIGHT = 1950; private static final int ARBITRARY_WIDTH_LESSER = 200; private static final int ARBITRARY_WIDTH_GREATER = 800; } 

    Чтобы создать пользовательский макет, вам необходимо прочитать и понять эту статью https://developer.android.com/guide/topics/ui/how-android-draws.html.

    Нетрудно реализовать поведение, которое вы хотите. Вам просто нужно переопределить onMeasure и onLayout в своем пользовательском представлении.

    В onMeasure вы получите максимальную высоту своего пользовательского вида и функцию call () для дочерних элементов в цикле. После того, как детское измерение получит желаемую высоту от каждого ребенка, и подсчитайте, соответствует ли ребенок текущему столбцу или нет, если не увеличивать пользовательский вид широко для нового столбца.

    В onLayout вы должны вызвать layout () для всех дочерних представлений, чтобы установить их позиции в родительском. Это позиции, которые вы рассчитали в onMeasure раньше.