Comment savoir quand une mise en page a été dessinée?


201

J'ai une vue personnalisée qui dessine un bitmap défilant à l'écran. Afin de l'initialiser, j'ai besoin de passer la taille en pixels de l'objet de disposition parent. Mais pendant les fonctions onCreate et onResume, la disposition n'a pas encore été dessinée, et donc layout.getMeasuredHeight () renvoie 0.

Comme solution de contournement, j'ai ajouté un gestionnaire pour attendre une seconde, puis mesurer. Cela fonctionne, mais c'est bâclé, et je n'ai aucune idée de combien je peux couper le temps avant de finir avant que la mise en page ne soit dessinée.

Ce que je veux savoir, c'est comment savoir quand une mise en page est dessinée? Y a-t-il un événement ou un rappel?

Réponses:


391

Vous pouvez ajouter un observateur d'arbre à la mise en page. Cela devrait renvoyer la largeur et la hauteur correctes. onCreate()est appelée avant la mise en page des vues enfant. La largeur et la hauteur ne sont donc pas encore calculées. Pour obtenir la hauteur et la largeur, mettez ceci sur la onCreate()méthode:

    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
Merci beaucoup! Mais je me demande pourquoi il n'y a pas de méthode plus simple pour ce faire car c'est un problème courant.
felixd

1
Incroyable. J'ai essayé OnLayoutChangeListener et cela n'a pas fonctionné. Mais OnGlobalLayoutListener a fonctionné. Merci beaucoup!
YoYoMyo

8
removeGlobalOnLayoutListener est obsolète au niveau de l'API 16. Utilisez plutôt removeOnGlobalLayoutListener.
tounaobun

3
Un "piège" que je vois est que si votre vue n'est pas visible, onGlobalLayout est appelée, mais plus tard lorsqu'elle est visible, getView est appelée, mais pas onGlobalLayout ... ugh.
Stephen McCormick

1
Une autre solution rapide pour cela est d'utiliser view.post(Runnable), il est plus propre et il fait la même chose.
dvdciri

74

Un moyen très simple consiste simplement à appeler post()votre mise en page. Cela exécutera votre code à l'étape suivante, une fois votre vue déjà créée.

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

4
Je ne sais pas si la publication exécutera réellement votre code une fois la vue créée. Il n'y a aucune garantie, car post ajoutera simplement le Runnable que vous créez à RunQueue, à exécuter après la file d'attente précédente exécutée sur le thread d'interface utilisateur.
HendraWD

4
Au contraire, post () s'exécute après la prochaine mise à jour de l'étape d'interface utilisateur, qui se produit après la création de la vue. Par conséquent, vous êtes assuré qu'il s'exécutera une fois la vue effectuée.
Elliptica

J'ai testé ce code plusieurs fois, il ne fonctionne mieux que dans l'interface utilisateur. Dans un fil d'arrière-plan, cela ne fonctionne parfois pas.
CoolMind

3
@HendraWijayaDjiono, peut-être que ce message répondra: stackoverflow.com/questions/21937224/…
CoolMind

1
J'ai rencontré un problème où l'utilisation de la première réponse ne fonctionnait pas à chaque fois, l'utilisation de la publication sur la vue que je devais faire défiler (une vue de défilement), m'a permis de faire défiler correctement jusqu'à la position souhaitée dès que la vue était prête à prendre scrollTo appelle la méthode.
Danuofr

21

Pour éviter le code obsolète et les avertissements, vous pouvez utiliser:

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
Pour utiliser ce code, 'view' doit être déclaré comme 'final'.
BeccaP

2
utilisez plutôt cette condition Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN
HendraWD

4

Mieux avec les fonctions d'extension 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)
            }
        }
    })
}

Solution très agréable et élégante qui peut être réutilisée.
Ionut Negru

3

Une autre réponse est:
essayez de vérifier les dimensions de la vue à onWindowFocusChanged.


3

Une alternative aux méthodes habituelles consiste à accrocher le dessin de la vue.

OnPreDrawListenerest appelé plusieurs fois lors de l'affichage d'une vue, il n'y a donc pas d'itération spécifique où votre vue a une largeur ou une hauteur mesurée valide. Cela nécessite que vous vérifiiez continuellement ( view.getMeasuredWidth() <= 0) ou définissiez une limite au nombre de fois où vousmeasuredWidth supérieure à zéro.

Il est également possible que la vue ne soit jamais dessinée, ce qui peut indiquer d'autres problèmes avec votre code.

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

La question est How can you tell when a layout has been drawn?et vous fournissez une méthode où vous appelez un OnPreDrawListeneret don't stop interrogating that view until it has provided you with a reasonable answerdonc les objections à votre solution devraient être évidentes.
Panier abandonné

Ce que vous devez savoir, ce n'est pas quand il a été dessiné, mais quand il a été mesuré. Mon code répond à la question qui doit vraiment être posée.
jwehrle

Y a-t-il un problème avec cette solution? Ne résout-il pas le problème rencontré?
jwehrle

1
La vue n'est pas mesurée tant qu'elle n'est pas présentée. Cette alternative est donc excessive et effectue un contrôle redondant. Vous admettez ne pas avoir abordé la question, mais ce que vous pensez devrait être demandé. Le commentaire d'origine faisait état de ces deux points, mais il y a aussi quelques problèmes avec le code lui-même qui ont été soumis en tant que modification.
Panier abandonné

C'est un joli montage. Je vous remercie. Je suppose que ce que je demande est, pensez-vous que cette réponse devrait être supprimée ou non? J'ai rencontré le problème de l'OP. Cela a résolu le problème. Je l'ai offert de bonne foi. C'était une erreur? Est-ce quelque chose qui ne devrait pas être fait sur StackOverflow?
jwehrle

2

Vérifiez-le simplement en appelant la méthode de publication sur votre mise en page ou vue

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

Ce n'est pas vrai et pourrait masquer d'éventuels problèmes. postva simplement mettre en file d'attente le runnable pour l'exécuter, cela ne garantit pas que la vue est mesurée, encore moins dessinée.
Ionut Negru

@IonutNegru peut-être lire ce stackoverflow.com/a/21938380
Bruno Bieri

@BrunoBieri, vous pouvez tester ce comportement avec des fonctions asynchrones qui modifient les mesures de la vue et voir si votre tâche en file d'attente est exécutée après chaque mesure et recevoir les valeurs attendues.
Ionut Negru

1

Quand onMeasureest appelé, la vue obtient sa largeur / hauteur mesurée. Après cela, vous pouvez appeler layout.getMeasuredHeight().


4
N'auriez-vous pas à créer votre propre vue personnalisée pour savoir quand onMeasure se déclenche?
Geeks On Hugs

Oui, vous devez utiliser des vues personnalisées pour accéder sur mesure.
Trevor Hart
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.