Explicação da visualização personalizada do onMeasure

316

Eu tentei fazer componente personalizado. Estendi a Viewclasse e fiz alguns desenhos no onDrawmétodo substituído. Por que eu preciso substituir onMeasure? Se não, tudo parecia certo. Alguém pode explicar isso? Como devo escrever meu onMeasuremétodo? Eu já vi alguns tutoriais, mas cada um é um pouco diferente do outro. Às vezes eles ligam super.onMeasureno final, às vezes usam setMeasuredDimensione não ligam. Onde está a diferença?

Afinal, eu quero usar vários exatamente os mesmos componentes. Adicionei esses componentes ao meu XMLarquivo, mas não sei quão grandes eles devem ser. Desejo definir sua posição e tamanho mais tarde (por que preciso definir o tamanho onMeasurese, onDrawquando o desenho, também está funcionando) na classe de componente personalizada. Quando exatamente eu preciso fazer isso?

Sennin
fonte

Respostas:

735

onMeasure()é a sua oportunidade de informar ao Android qual o tamanho que você deseja que sua visualização personalizada dependa das restrições de layout fornecidas pelos pais; também é a oportunidade da sua exibição personalizada para saber quais são essas restrições de layout (caso você queira se comportar de maneira diferente em uma match_parentsituação e em uma wrap_contentsituação). Essas restrições são agrupadas nos MeasureSpecvalores que são passados ​​para o método. Aqui está uma correlação aproximada dos valores de modo:

  • EXATAMENTE significa que o valor layout_widthou layout_heightfoi definido para um valor específico. Você provavelmente deve ter sua visualização desse tamanho. Isso também pode ser acionado quando match_parentusado, para definir o tamanho exatamente para a visualização pai (isso depende do layout na estrutura).
  • AT_MOST normalmente significa que o valor layout_widthou layout_heightfoi definido como match_parentou wrap_contentonde é necessário um tamanho máximo (isso depende do layout na estrutura), e o tamanho da dimensão pai é o valor. Você não deve ser maior que esse tamanho.
  • NÃO ESPECIFICADO normalmente significa que o valor layout_widthou layout_heightfoi definido como wrap_contentsem restrições. Você pode ter o tamanho que desejar. Alguns layouts também usam esse retorno de chamada para descobrir o tamanho desejado antes de determinar quais especificações realmente serão enviadas novamente em uma segunda solicitação de medida.

O contrato que existe onMeasure()é que setMeasuredDimension() DEVE ser chamado no final com o tamanho que você deseja que a vista seja. Esse método é chamado por todas as implementações da estrutura, incluindo a implementação padrão encontrada em View, e é por isso que é seguro chamar, superse isso se encaixa no seu caso de uso.

Concedido, porque a estrutura aplica uma implementação padrão, pode não ser necessário substituir esse método, mas você pode ver recortes nos casos em que o espaço de exibição é menor que o seu conteúdo, se não o fizer, e se você definir o seu Na visualização personalizada, wrap_contentnas duas direções, a visualização pode não aparecer, porque a estrutura não sabe o tamanho dela!

Geralmente, se você está substituindo Viewe não outro widget existente, provavelmente é uma boa idéia fornecer uma implementação, mesmo que seja tão simples quanto algo como isto:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int desiredWidth = 100;
    int desiredHeight = 100;

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;

    //Measure Width
    if (widthMode == MeasureSpec.EXACTLY) {
        //Must be this size
        width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        width = Math.min(desiredWidth, widthSize);
    } else {
        //Be whatever you want
        width = desiredWidth;
    }

    //Measure Height
    if (heightMode == MeasureSpec.EXACTLY) {
        //Must be this size
        height = heightSize;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        height = Math.min(desiredHeight, heightSize);
    } else {
        //Be whatever you want
        height = desiredHeight;
    }

    //MUST CALL THIS
    setMeasuredDimension(width, height);
}

Espero que ajude.

Devunwired
fonte
1
Hey @Devunwired boa explicação o melhor que eu li até agora. Sua explicação respondeu a muitas perguntas que eu tinha e tirei algumas dúvidas, mas ainda resta uma: Qual é a minha visão personalizada dentro de um ViewGroup juntamente com outras Views (não importa quais tipos), que o ViewGroup receberá todos os seus filhos e para cada uma delas investigar sua restrição de LayoutParams e pedir a cada criança que se avalie de acordo com suas restrições?
faraó
47
Observe que esse código não funcionará se você substituir a medição de qualquer subclasse de ViewGroup. Suas subvisões não serão exibidas e todas terão um tamanho de 0x0. Se você precisar substituir onMeasure de um ViewGroup personalizado, altere widthMode, widthSize, heightMode e heightSize, compile-os novamente para measureSpecs usando MeasureSpec.makeMeasureSpec e passe os números inteiros resultantes para super.onMeasure.
Alexey19
1
Resposta fantástica. Observe que, de acordo com a documentação do Google, é de responsabilidade do View lidar com o preenchimento.
jonstaff
4
Sobre c ** p complicado que faz do Android um sistema de layout doloroso para trabalhar. Eles poderiam ter apenas teve getParent () get *** () ....
Oliver Dixon
2
Existem métodos auxiliares na Viewclasse, chamados resolveSizeAndStatee resolveSize, que devem fazer o que as cláusulas 'if' fazem - eu os achei úteis, especialmente se você tiver que escrever esses FIs frequentemente.
stan0
5

na verdade, sua resposta não está completa, pois os valores também dependem do contêiner de embalagem. No caso de layouts relativos ou lineares, os valores se comportam assim:

  • EXATAMENTE match_parent é EXATAMENTE + tamanho do pai
  • AT_MOST wrap_content resulta em um AT_MOST MeasureSpec
  • NÃO ESPECIFICADO nunca disparado

No caso de uma exibição de rolagem horizontal, seu código funcionará.

Florian
fonte
57
Se você achar que alguma resposta aqui está incompleta, adicione-a em vez de fornecer uma resposta parcial.
Michaël
1
Bom por vincular isso a como os layouts funcionam, mas, no meu caso, onMeasure é chamado três vezes para minha visualização personalizada. A vista em questão tinha uma altura de wrap_content e uma largura ponderada (largura = 0, peso = 1). A primeira chamada foi ESPECIFICADA / NÃO ESPECIFICADA, a segunda teve AT_MOST / EXATAMENTE e a terceira teve EXATAMENTE / EXATAMENTE.
22420 William Th Mallard
0

Se você não precisar alterar algo no Measure - não há absolutamente nenhuma necessidade de substituí-lo.

O código Devunwired (a resposta selecionada e mais votada aqui) é quase idêntico ao que a implementação do SDK já faz por você (e eu verifiquei - fazia isso desde 2009).

Você pode verificar o método onMeasure aqui :

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

Substituir o código SDK a ser substituído pelo mesmo código não faz sentido.

A parte deste documento oficial que afirma que "o padrão onMeasure () sempre definirá um tamanho de 100x100" - está errada.

David Refaeli
fonte