Jak rozpoznać, kiedy narysowano układ?

201

Mam niestandardowy widok, który rysuje przewijalną mapę bitową na ekranie. Aby go zainicjować, muszę przekazać rozmiar w pikselach nadrzędnego obiektu układu. Ale podczas funkcji onCreate i onResume układ nie został jeszcze narysowany, a więc layout.getMeasuredHeight () zwraca 0.

Aby obejść ten problem, dodałem moduł obsługi, który czeka jedną sekundę, a następnie mierzy. To działa, ale jest niechlujne i nie mam pojęcia, ile mogę skrócić czas, zanim skończę, zanim zostanie narysowany układ.

Chcę wiedzieć, jak mogę wykryć rysowanie układu? Czy jest zdarzenie lub oddzwonienie?

Jesiotr z tworzywa sztucznego
źródło

Odpowiedzi:

391

Możesz dodać obserwatora drzewa do układu. Powinno to zwrócić prawidłową szerokość i wysokość. onCreate()jest wywoływany przed wykonaniem układu widoków potomnych. Zatem szerokość i wysokość nie są jeszcze obliczane. Aby uzyskać wysokość i szerokość, umieść to na onCreate()metodzie:

    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(); 

        } 
    });
błogosławieństwo
źródło
28
Dziękuję Ci bardzo! Ale zastanawiam się, dlaczego nie ma prostszej metody, ponieważ jest to powszechny problem.
felixd
1
Niesamowity. Próbowałem OnLayoutChangeListener i to nie działało. Ale OnGlobalLayoutListener działał. Dziękuję bardzo!
YoYoMyo,
8
removeGlobalOnLayoutListener jest przestarzały na poziomie API 16. Zamiast tego użyj removeOnGlobalLayoutListener.
tounaobun
3
Jedno „gotcha”, które widzę, to jeśli twój widok nie jest widoczny, wywoływany jest onGlobalLayout, ale później, gdy jest widoczny, wywoływany jest getView, ale nie onGlobalLayout ... ugh.
Stephen McCormick,
1
Innym szybkim rozwiązaniem tego jest użycie view.post(Runnable), to czystsze i robi to samo.
dvdciri
74

Naprawdę prosty sposób to po prostu sprawdzić post()swój układ. Spowoduje to uruchomienie kodu w następnym kroku, po utworzeniu widoku.

YOUR_LAYOUT.post( new Runnable() {
    @Override
    public void run() {
        int width  = YOUR_LAYOUT.getMeasuredWidth();
        int height = YOUR_LAYOUT.getMeasuredHeight(); 
    }
});
Elliptica
źródło
4
Nie wiem, czy post rzeczywiście uruchomi Twój kod po utworzeniu widoku. Nie ma gwarancji, ponieważ post doda po prostu Runnable, którą utworzysz do RunQueue, która zostanie wykonana po poprzedniej kolejce wykonanej w wątku interfejsu użytkownika.
HendraWD,
4
Przeciwnie, post () wykonuje się po następnej aktualizacji kroku interfejsu użytkownika, która dzieje się po utworzeniu widoku. Dlatego masz gwarancję, że zostanie ono wykonane po utworzeniu widoku.
Elliptica,
Testowałem ten kod kilka razy, działa najlepiej tylko w wątku interfejsu użytkownika. W wątku w tle czasami to nie działa.
CoolMind
3
@HendraWijayaDjiono, może ten post odpowie: stackoverflow.com/questions/21937224/…
CoolMind
1
Natknąłem się na problem, w którym użycie pierwszej odpowiedzi nie działało za każdym razem, użycie postu w widoku, który musiałem przewinąć (widok przewijania), pozwoliło mi poprawnie przewinąć do żądanej pozycji, gdy tylko widok był gotowy do podjęcia wywołania metody scrollTo.
Danuofr,
21

Aby uniknąć przestarzałego kodu i ostrzeżeń, możesz użyć:

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();
            }
        });
murena
źródło
1
Aby użyć tego kodu, „widok” należy zadeklarować jako „końcowy”.
BeccaP
2
użyj tego warunku zamiast tego Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN
HendraWD
4

Lepiej dzięki funkcjom rozszerzenia 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)
            }
        }
    })
}
Sergio
źródło
Bardzo ładne i eleganckie rozwiązanie, które można ponownie wykorzystać.
Ionut Negru
3

Inna odpowiedź to:
spróbuj sprawdzić wymiary w onWindowFocusChanged.

Joe Plante
źródło
3

Alternatywą dla zwykłych metod jest zaczepienie się w rysunku widoku.

OnPreDrawListenerjest wywoływana wiele razy podczas wyświetlania widoku, więc nie ma określonej iteracji, w której widok ma prawidłową zmierzoną szerokość lub wysokość. Wymaga to ciągłego sprawdzania ( view.getMeasuredWidth() <= 0) lub ustawiania limitu liczby sprawdzeń na wartość measuredWidthwiększą niż zero.

Istnieje również szansa, że ​​widok nigdy nie zostanie narysowany, co może wskazywać na inne problemy z kodem.

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
    }
});
jwehrle
źródło
Powstaje pytanie, How can you tell when a layout has been drawn?a ty dostarczenie sposobu, w którym wybrany numer OnPreDrawListeneri don't stop interrogating that view until it has provided you with a reasonable answertak sprzeciw wobec rozwiązania powinny być oczywiste.
Opuszczony
To, co musisz wiedzieć, to nie kiedy jest rysowany, ale kiedy jest mierzony. Mój kod odpowiada na pytanie, które naprawdę należy zadać.
jwehrle
Czy jest jakiś problem z tym rozwiązaniem? Czy to nie rozwiązuje napotkanego problemu?
jwehrle
1
Widok nie jest mierzony, dopóki nie zostanie ułożony, więc ta alternatywa jest nadmierna i wykonuje zbędną kontrolę. Przyznajesz, że nie odpowiadasz na pytanie, ale o to, co czujesz, należy zapytać. Oryginalny komentarz zawierał oba te punkty, ale są też pewne problemy z samym kodem, które zostały przesłane jako edycja.
Opuszczony
To miła edycja. Dziękuję Ci. Chyba pytam: czy uważasz, że ta odpowiedź powinna zostać usunięta czy nie? Napotkałem problem PO. To rozwiązało problem. Ofiarowałem to w dobrej wierze. Czy to był błąd? Czy to coś, czego nie należy robić w StackOverflow?
jwehrle
2

Po prostu sprawdź to, wywołując metodę postu na swoim układzie lub widoku

 view.post( new Runnable() {
     @Override
     public void run() {
        // your layout is now drawn completely , use it here.
      }
});
Shivam Yadav
źródło
To nie jest prawda i może ukryć możliwe problemy. postpo prostu ustawia kolejkę, po której można uruchomić, nie zapewnia pomiaru widoku, a nawet mniej rysowania.
Ionut Negru
@IonutNegru może przeczytać ten stackoverflow.com/a/21938380
Bruno Bieri
@BrunoBieri, możesz przetestować to zachowanie za pomocą funkcji asynchronicznych, które zmieniają pomiary widoku i sprawdzają, czy zadanie w kolejce jest uruchamiane po każdym pomiarze i odbiera oczekiwane wartości.
Ionut Negru
1

Po onMeasurewywołaniu widok otrzymuje zmierzoną szerokość / wysokość. Następnie możesz zadzwonić layout.getMeasuredHeight().

Pavan
źródło
4
Czy nie musiałbyś tworzyć własnego widoku, aby wiedzieć, kiedy uruchamia się onMeasure?
Geeks On Hugs
Tak, musisz użyć niestandardowych widoków, aby uzyskać dostęp na miarę.
Trevor Hart,