Como você pode saber quando um layout foi desenhado?

201

Eu tenho uma exibição personalizada que desenha um bitmap rolável na tela. Para inicializá-lo, preciso passar o tamanho em pixels do objeto de layout pai. Porém, durante as funções onCreate e onResume, o Layout ainda não foi desenhado e, portanto, layout.getMeasuredHeight () retorna 0.

Como solução alternativa, adicionei um manipulador para aguardar um segundo e depois medir. Isso funciona, mas é desleixado, e não tenho idéia de quanto posso cortar o tempo antes de terminar antes que o layout seja desenhado.

O que eu quero saber é: como posso detectar quando um layout é desenhado? Existe um evento ou retorno de chamada?

Esturjão de plástico
fonte

Respostas:

391

Você pode adicionar um observador de árvore ao layout. Isso deve retornar a largura e altura corretas. onCreate()é chamado antes que o layout das visualizações filho seja concluído. Portanto, a largura e a altura ainda não estão calculadas. Para obter a altura e a largura, coloque isso no 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
fonte
28
Muito obrigado! Mas me pergunto por que não existe um método mais simples para fazer isso, porque é um problema comum.
Felixd 16/03/19
1
Surpreendente. Eu tentei o OnLayoutChangeListener e não funcionou. Mas OnGlobalLayoutListener funcionou. Muito obrigado!
YoYoMyo 22/01
8
removeGlobalOnLayoutListener foi descontinuado no nível 16. da API. Use removeOnGlobalLayoutListener.
Tounaobun 20/05
3
Uma "pegadinha" que estou vendo é que, se sua visão não estiver visível, o onGlobalLayout é chamado, mas mais tarde, quando visível, o getView é chamado, mas não o onGlobalLayout ... ugh.
Stephen McCormick
1
Outra solução rápida para isso é usar view.post(Runnable), é mais limpo e faz a mesma coisa.
dvdciri
74

Uma maneira realmente fácil é simplesmente chamar post()seu layout. Isso executará seu código na próxima etapa, depois que sua visualização já tiver sido criada.

YOUR_LAYOUT.post( new Runnable() {
    @Override
    public void run() {
        int width  = YOUR_LAYOUT.getMeasuredWidth();
        int height = YOUR_LAYOUT.getMeasuredHeight(); 
    }
});
Elliptica
fonte
4
Não sei se a postagem realmente executará seu código após a exibição já ter sido criada. Não há garantia, pois a postagem adicionará apenas o Runnable que você criar ao RunQueue, para ser executado após a fila anterior executada no thread da interface do usuário.
precisa saber é o seguinte
4
Pelo contrário, post () é executado após a próxima atualização da etapa da interface do usuário, que ocorre após a criação da exibição. Portanto, você tem a garantia de que será executado após a visualização.
11686 Elliptica
Testei esse código várias vezes, ele funciona melhor apenas no thread da interface do usuário. Em um thread de segundo plano, às vezes não funciona.
CoolMind 4/08
3
@HendraWijayaDjiono, talvez este post vai responder: stackoverflow.com/questions/21937224/...
CoolMind
1
Encontrei um problema em que o uso da primeira resposta não funcionava sempre, usando a postagem na visualização que eu precisava rolar (uma visualização de rolagem), permitiu-me rolar para a posição desejada corretamente assim que a visualização estivesse pronta para ser usada chamadas de método scrollTo.
Danuofr 12/09/19
21

Para evitar códigos e avisos obsoletos, você pode 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
fonte
1
Para usar esse código, 'view' deve ser declarado como 'final'.
BeccaP
2
usar esta condição, em vez Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN
HendraWD
4

Melhor com as funções de extensão 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
fonte
Solução muito agradável e elegante que pode ser reutilizada.
Ionut Negru
3

Outra resposta é:
Tente verificar as dimensões da visualização em onWindowFocusChanged.

Joe Plante
fonte
3

Uma alternativa aos métodos usuais é se conectar ao desenho da vista.

OnPreDrawListeneré chamado várias vezes ao exibir uma vista, portanto, não há iteração específica em que sua vista tenha largura ou altura medidas válidas. Isso requer que você verifique continuamente ( view.getMeasuredWidth() <= 0) ou defina um limite para o número de vezes que você verifica se é measuredWidthmaior que zero.

Também existe a chance de a visualização nunca ser desenhada, o que pode indicar outros problemas com o seu 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
fonte
A questão é: How can you tell when a layout has been drawn?você está fornecendo um método no qual chama um OnPreDrawListenere, don't stop interrogating that view until it has provided you with a reasonable answerportanto, as objeções à sua solução devem ser óbvias.
Carrinho abandonado
O que você precisa saber não é quando é desenhado, mas quando é medido. Meu código responde à pergunta que realmente precisa ser feita.
jwehrle
Existe algum problema com esta solução? Não consegue resolver o problema enfrentado?
Jwehrle 19/05/19
1
A vista não é medida até que seja definida, portanto, essa alternativa é excessiva e executa uma verificação redundante. Você admite não abordar a questão, mas o que você sente deve ser perguntado. O comentário original fez esses dois pontos, mas também há alguns problemas com o próprio código que foram enviados como uma edição.
Carrinho abandonado
Essa é uma boa edição. Obrigado. Acho que estou perguntando: você acha que essa resposta deve ser excluída ou não? Eu encontrei o problema do OP. Isso resolveu o problema. Eu ofereci de boa fé. Isso foi um erro? Isso é algo que não deve ser feito no StackOverflow?
jwehrle 21/05/19
2

Basta verificar chamando o método de postagem em seu layout ou visualizar

 view.post( new Runnable() {
     @Override
     public void run() {
        // your layout is now drawn completely , use it here.
      }
});
Shivam Yadav
fonte
Isso não é verdade e pode ocultar possíveis problemas. postapenas enfileirará o executável para executar depois, não garante que a vista seja medida, ainda menos desenhada.
Ionut Negru
@IonutNegru talvez leia este stackoverflow.com/a/21938380
Bruno Bieri
@BrunoBieri, você pode testar esse comportamento com funções assíncronas que alteram as medidas da vista e ver se a tarefa na fila é executada após cada medida e recebe os valores esperados.
Ionut Negru
1

Quando onMeasureé chamado, a visualização obtém sua largura / altura medidas. Depois disso, você pode ligar layout.getMeasuredHeight().

Pavan
fonte
4
Você não precisaria criar sua própria visualização personalizada para saber quando o onMeasure é acionado?
Geeks On Hugs
Sim, você precisa usar visualizações personalizadas para acessar por medida.
Trevor Hart