¿Cómo puede saber cuándo se ha dibujado un diseño?

201

Tengo una vista personalizada que dibuja un mapa de bits desplazable en la pantalla. Para inicializarlo, necesito pasar el tamaño en píxeles del objeto de diseño primario. Pero durante las funciones onCreate y onResume, el diseño aún no se ha dibujado, por lo que layout.getMeasuredHeight () devuelve 0.

Como solución alternativa, he agregado un controlador para esperar un segundo y luego medir. Esto funciona, pero es descuidado, y no tengo idea de cuánto puedo recortar el tiempo antes de terminar antes de que se dibuje el diseño.

Lo que quiero saber es, ¿cómo puedo detectar cuándo se dibuja un diseño? ¿Hay algún evento o devolución de llamada?

Esturión de plástico
fuente

Respuestas:

391

Puede agregar un observador de árbol al diseño. Esto debería devolver el ancho y la altura correctos. onCreate()se llama antes de que se realice el diseño de las vistas secundarias. Por lo tanto, el ancho y la altura aún no se calculan. Para obtener la altura y el ancho, ponga esto en el onCreate()método:

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

        } 
    });
blessenm
fuente
28
¡Muchas gracias! Pero me pregunto por qué no hay un método más simple para hacerlo porque es un problema común.
Felixd
1
Asombroso. Intenté OnLayoutChangeListener y no funcionó. Pero OnGlobalLayoutListener funcionó. Muchas gracias!
YoYoMyo
8
removeGlobalOnLayoutListener está en desuso en el nivel 16 de la API. Utilice removeOnGlobalLayoutListener en su lugar.
tounaobun
3
Un "problema" que estoy viendo es que si su vista no es visible, se llama a onGlobalLayout, pero luego, cuando es visible, se llama a getView, pero no a onGlobalLayout ... ugh.
Stephen McCormick
1
Otra solución rápida para esto es usarlo view.post(Runnable), es más limpio y hace lo mismo.
dvdciri
74

Una forma realmente fácil es simplemente llamar post()a su diseño. Esto ejecutará su código en el siguiente paso, después de que su vista ya se haya creado.

YOUR_LAYOUT.post( new Runnable() {
    @Override
    public void run() {
        int width  = YOUR_LAYOUT.getMeasuredWidth();
        int height = YOUR_LAYOUT.getMeasuredHeight(); 
    }
});
Elliptica
fuente
44
No sé si la publicación realmente ejecutará su código después de que la vista ya se haya creado. No hay garantía, ya que la publicación solo agregará el Runnable que cree a RunQueue, para que se ejecute después de la cola anterior ejecutada en UI Thread.
HendraWD
44
Por el contrario, post () se ejecuta después de la próxima actualización del paso de la IU, que ocurre después de crear la vista. Por lo tanto, tiene la garantía de que se ejecutará una vez realizada la vista.
Elliptica
Probé este código varias veces, funciona mejor solo en UI-thread. En un hilo de fondo, a veces no funciona.
CoolMind
3
@HendraWijayaDjiono, tal vez esta publicación responda: stackoverflow.com/questions/21937224/…
CoolMind
1
Me encontré con un problema en el que usar la primera respuesta no funcionaba todo el tiempo, usar la publicación en la vista que necesitaba para desplazarme (una vista de desplazamiento), me permitía desplazarme a la posición deseada correctamente tan pronto como la vista estaba lista para tomar llamadas al método scrollTo.
Danuofr
21

Para evitar códigos y advertencias obsoletos, puede usar:

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
fuente
1
Para usar este código, 'view' debe declararse como 'final'.
BeccaP
2
use esta condición en su lugar Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN
HendraWD
4

Mejor con las funciones de extensión de 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
fuente
Solución muy bonita y elegante que se puede reutilizar.
Ionut Negru
3

Otra respuesta es:
intente comprobar las dimensiones de la vista en onWindowFocusChanged.

Joe Plante
fuente
3

Una alternativa a los métodos habituales es engancharse al dibujo de la vista.

OnPreDrawListenerse llama muchas veces cuando se muestra una vista, por lo que no hay una iteración específica en la que su vista tenga un ancho o alto medido válido. Esto requiere que verifique continuamente ( view.getMeasuredWidth() <= 0) o establezca un límite en la cantidad de veces que verifica si hay unmeasuredWidth mayor que cero.

También existe la posibilidad de que la vista nunca se dibuje, lo que puede indicar otros problemas con su código.

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
fuente
La pregunta es How can you tell when a layout has been drawn?y usted está proporcionando un método al que llama OnPreDrawListenery, don't stop interrogating that view until it has provided you with a reasonable answerpor lo tanto, las objeciones a su solución deberían ser obvias.
Carro abandonado
Lo que necesita saber no es cuándo se dibujó, sino cuándo se midió. Mi código responde a la pregunta que realmente debe hacerse.
jwehrle
¿Hay algún problema con esta solución? ¿No logra resolver el problema enfrentado?
Jwehrle
1
La vista no se mide hasta que se presenta, por lo que esta alternativa es excesiva y realiza una verificación redundante. Admite no abordar la pregunta, pero lo que siente debe preguntarse. El comentario original hizo ambos puntos, pero también hay algunos problemas con el código en sí que se han enviado como edición.
Carro abandonado
Esa es una buena edición. Gracias. Supongo que lo que pregunto es: ¿Crees que esta respuesta debería eliminarse o no? Encontré el problema del OP. Esto resolvió el problema. Lo ofrecí de buena fe. ¿Fue eso un error? ¿Es algo que no se debe hacer en StackOverflow?
jwehrle
2

Simplemente verifíquelo llamando al método de publicación en su diseño o vista

 view.post( new Runnable() {
     @Override
     public void run() {
        // your layout is now drawn completely , use it here.
      }
});
Shivam Yadav
fuente
Esto no es cierto y podría ocultar posibles problemas. postsolo pondrá en cola el ejecutable para que se ejecute después, no garantiza que se mida la vista, ni siquiera menos dibujada.
Ionut Negru
@IonutNegru quizás lea este stackoverflow.com/a/21938380
Bruno Bieri
@BrunoBieri, puede probar este comportamiento con funciones asíncronas que alteran las mediciones de la vista y ver si su tarea en cola se ejecuta después de cada medición y recibe los valores esperados.
Ionut Negru
1

Cuando onMeasurese llama, la vista obtiene su ancho / alto medido. Después de esto, puedes llamar layout.getMeasuredHeight().

Pavana
fuente
44
¿No tendría que hacer su propia vista personalizada para saber cuándo se activa onMeasure?
Geeks On Hugs
Sí, debe usar vistas personalizadas para acceder a medida.
Trevor Hart