Как вы можете сказать, когда макет был нарисован?


201

У меня есть собственное представление, которое рисует прокручиваемое растровое изображение на экране. Чтобы инициализировать его, мне нужно передать размер в пикселях родительского объекта макета. Но во время функций onCreate и onResume макет еще не прорисован, и поэтому layout.getMeasuredHeight () возвращает 0.

В качестве обходного пути я добавил обработчик, чтобы подождать одну секунду, а затем измерить. Это работает, но неаккуратно, и я понятия не имею, насколько я могу урезать время до того, как я закончу до того, как будет нарисован макет.

Я хочу знать, как я могу определить, когда макет нарисован? Есть ли событие или обратный звонок?

Ответы:


391

Вы можете добавить дерево наблюдателя к макету. Это должно вернуть правильную ширину и высоту. onCreate()вызывается перед созданием дочерних представлений. Так что ширина и высота еще не рассчитаны. Чтобы получить высоту и ширину, поместите это в onCreate()метод:

    final LinearLayout layout = (LinearLayout) findViewById(R.id.YOUR_VIEW_ID);
    ViewTreeObserver vto = layout.getViewTreeObserver(); 
    vto.addOnGlobalLayoutListener (new OnGlobalLayoutListener() { 
        @Override 
        public void onGlobalLayout() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    layout.getViewTreeObserver()
                            .removeOnGlobalLayoutListener(this);
                } else {
                    layout.getViewTreeObserver()
                            .removeGlobalOnLayoutListener(this);
                }
            int width  = layout.getMeasuredWidth();
            int height = layout.getMeasuredHeight(); 

        } 
    });

28
Большое спасибо! Но мне интересно, почему не существует более простого способа сделать это, потому что это общая проблема.
Феликсд

1
Удивительный. Я пытался OnLayoutChangeListener, и он не работал. Но OnGlobalLayoutListener работал. Спасибо вам большое!
YoYoMyo

8
removeGlobalOnLayoutListener устарела на уровне API 16. Вместо этого используйте removeOnGlobalLayoutListener.
tounaobun

3
Одна «ошибка», которую я вижу, это то, что если ваш вид не виден, вызывается onGlobalLayout, но позже, когда видимый, вызывается getView, но не onGlobalLayout ... тьфу.
Стивен Маккормик

1
Другим быстрым решением для этого является использование view.post(Runnable), он чище и делает то же самое.
dvdciri

74

Действительно простой способ - просто вызвать post()ваш макет. Это запустит ваш код на следующем шаге, после того как ваше представление уже будет создано.

YOUR_LAYOUT.post( new Runnable() {
    @Override
    public void run() {
        int width  = YOUR_LAYOUT.getMeasuredWidth();
        int height = YOUR_LAYOUT.getMeasuredHeight(); 
    }
});

4
Я не знаю, будет ли post запускать ваш код после того, как представление уже создано. Нет никакой гарантии, поскольку post просто добавит созданный вами Runnable в RunQueue, который будет выполнен после предыдущей очереди, выполненной в потоке пользовательского интерфейса.
HendraWD

4
Напротив, post () выполняется после следующего шага обновления пользовательского интерфейса, который происходит после создания представления. Следовательно, вы гарантированно выполните его после создания представления.
Эллиптика

Я тестировал этот код несколько раз, лучше всего он работает только в UI-потоке. В фоновом потоке это иногда не работает.
CoolMind

3
@HendraWijayaDjiono, возможно, этот пост ответит: stackoverflow.com/questions/21937224/…
CoolMind

1
Я столкнулся с проблемой, когда использование первого ответа не работало каждый раз, использование сообщения на представлении, которое мне нужно было прокрутить (представление с прокруткой), позволило мне правильно прокрутить до нужной позиции, как только представление будет готово к принятию. вызов метода scrollTo.
Дануофр

21

Чтобы избежать устаревшего кода и предупреждений, вы можете использовать:

view.getViewTreeObserver().addOnGlobalLayoutListener(
        new ViewTreeObserver.OnGlobalLayoutListener() {
            @SuppressWarnings("deprecation")
            @Override
            public void onGlobalLayout() {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    view.getViewTreeObserver()
                            .removeOnGlobalLayoutListener(this);
                } else {
                    view.getViewTreeObserver()
                            .removeGlobalOnLayoutListener(this);
                }
                yourFunctionHere();
            }
        });

1
Чтобы использовать этот код, «view» должен быть объявлен как «final».
BeccaP

2
используйте вместо этого условие Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN
HendraWD

4

Лучше с функциями расширения kotlin

inline fun View.waitForLayout(crossinline yourAction: () -> Unit) {
    val vto = viewTreeObserver
    vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            when {
                vto.isAlive -> {
                    vto.removeOnGlobalLayoutListener(this)
                    yourAction()
                }
                else -> viewTreeObserver.removeOnGlobalLayoutListener(this)
            }
        }
    })
}

Очень красивое и элегантное решение, которое можно использовать повторно.
Ионут Негру


3

Альтернативой обычным методам является подключение к чертежу вида.

OnPreDrawListenerвызывается многократно при отображении вида, поэтому не существует конкретной итерации, в которой ваш вид имеет правильную измеренную ширину или высоту. Это требует, чтобы вы постоянно проверяли ( view.getMeasuredWidth() <= 0) или устанавливали ограничение на количество проверок, measuredWidthпревышающее ноль.

Существует также вероятность того, что представление никогда не будет отображено, что может указывать на другие проблемы с вашим кодом.

final View view = [ACQUIRE REFERENCE]; // Must be declared final for inner class
ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        if (view.getMeasuredWidth() > 0) {     
            view.getViewTreeObserver().removeOnPreDrawListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
            //Do something with width and height here!
        }
        return true; // Continue with the draw pass, as not to stop it
    }
});

Вопрос в том, How can you tell when a layout has been drawn?и вы предоставляете метод, по которому вы вызываете метод, OnPreDrawListenerи don't stop interrogating that view until it has provided you with a reasonable answerпоэтому возражения против вашего решения должны быть очевидны.
Заброшенная корзина

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

Есть ли проблемы с этим решением? Разве это не решает проблему?
jwehrle

1
Представление не измеряется до тех пор, пока оно не будет размечено, поэтому эта альтернатива является чрезмерной и выполняет избыточную проверку. Вы признаете, что не отвечаете на вопрос, но что вы чувствуете, следует спросить. Исходный комментарий содержал обе эти точки, но есть также некоторые проблемы с самим кодом, которые были представлены в качестве редактирования.
Заброшенная корзина

Это хорошее редактирование. Спасибо. Я думаю, что я спрашиваю, как вы думаете, этот ответ должен быть удален или нет? Я столкнулся с проблемой ОП. Это решило проблему. Я предложил это добросовестно. Было ли это ошибкой? Это то, что не должно быть сделано на StackOverflow?
jwehrle

2

Просто проверьте это, вызвав метод post на вашем макете или представлении

 view.post( new Runnable() {
     @Override
     public void run() {
        // your layout is now drawn completely , use it here.
      }
});

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

@IonutNegru, возможно, прочитал этот stackoverflow.com/a/21938380
Бруно Биери

@BrunoBieri, вы можете проверить это поведение с помощью асинхронных функций, которые изменяют измерения вида и проверяют, выполняется ли задание в очереди после каждого измерения и получают ожидаемые значения.
Ионут Негру

1

Когда onMeasureвызывается, вид получает свою измеренную ширину / высоту. После этого вы можете позвонить layout.getMeasuredHeight().


4
Разве вам не нужно создавать свой собственный вид, чтобы знать, когда срабатывает onMeasure?
Гики на объятиях

Да, вы должны использовать собственные представления для доступа к мере.
Тревор Харт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.