O segundo Visualização do Desenvolvedor do Android 11 já está disponível, teste e compartilhe seu feedback.

Personalizar componentes de visualização

O Android oferece um modelo de componente sofisticado e eficiente para criar sua IU, com base nas classes fundamentais de layout: View e ViewGroup. Para começar, a plataforma inclui diversas subclasses View e ViewGroup pré-construídas, chamadas widgets e layouts, respectivamente, que você pode usar para construir sua IU.

Uma lista parcial de widgets disponíveis inclui Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner e, para fins especiais, AutoCompleteTextView, ImageSwitcher e TextSwitcher.

Entre os layouts disponíveis estão LinearLayout, FrameLayout, RelativeLayout e outros. Para ver mais exemplos, consulte Objetos de layout comuns.

Se nenhum dos widgets ou layouts predefinidos atender às suas necessidades, você poderá criar sua própria subclasse View. Se você precisar fazer apenas pequenos ajustes em um widget ou layout existente, poderá simplesmente criar uma subclasse do widget ou layout e substituir os métodos correspondentes.

Criar suas próprias subclasses View proporciona controle preciso sobre a aparência e a função de um elemento de tela. Para dar uma ideia do controle que você tem com as visualizações personalizadas, veja alguns exemplos do que é possível fazer com elas:

  • Você pode criar um tipo de visualização de renderização personalizada, por exemplo, um botão de "controle de volume" renderizado usando gráficos 2D e que se assemelha a um controle eletrônico analógico.
  • Você pode combinar um grupo de componentes de visualização em um novo componente. Pode fazer, por exemplo, algo como uma caixa de combinação, que é uma combinação de lista pop-up e campo de entrada livre de texto; um controle seletor de painel duplo, que é um painel esquerdo e direito com uma lista em para atribuição de itens; e assim por diante.
  • Você pode modificar a forma como um componente EditText é renderizado na tela. O Tutorial do bloco de notas (link em inglês) usa esse recurso para criar uma página de bloco de notas alinhada.
  • Você pode capturar outros eventos, como teclas pressionadas, e manipulá-los de alguma forma personalizada, por exemplo, para um jogo.

As seções abaixo explicam como criar visualizações personalizadas e usá-las no seu aplicativo. Para informações de referências detalhadas, consulte a classe View.

A abordagem básica

Veja a seguir uma visão geral do que você precisa saber para começar a criar seus próprios componentes de visualização:

  1. Estenda uma classe ou subclasse View existente com sua própria classe.
  2. Modifique alguns dos métodos da superclasse. Os métodos da superclasse que devem ser modificados começam com "on", por exemplo, onDraw(), onMeasure() e onKeyDown(). Isso é semelhante aos eventos on..., em Activity ou ListActivity, que você modifica para ciclo de vida e outros hooks de funcionalidade.
  3. Use sua nova classe de extensão. Depois de concluída, sua nova classe de extensão pode ser usada no lugar da visualização em que ela foi baseada.

Dica: as classes de extensão podem ser definidas como classes internas dentro das atividades que as utilizam. Isso é útil porque controla o acesso a elas, mas não é necessário. Talvez você queira criar uma nova visualização pública para uso mais amplo no seu aplicativo.

Componentes totalmente personalizados

Componentes totalmente personalizados podem ser usados para criar componentes gráficos que são exibidos como você quiser. Talvez um medidor VU gráfico parecido com um antigo medidor analógico ou uma visualização de texto simples, em que uma bola saltitante se move ao longo das palavras como em uma máquina de karaokê. De qualquer maneira, você quer algo que os componentes integrados simplesmente não farão, independentemente de como sejam combinados.

Felizmente, é fácil criar componentes que se parecem e se comportam da maneira que você quer. Talvez as limitações sejam apenas sua imaginação, o tamanho da tela e a capacidade de processamento disponível. Lembre-se de que seu aplicativo precisa funcionar em algo com muito menos capacidade do que seu desktop.

Para criar um componente totalmente personalizado:

  1. A visualização mais genérica que você pode estender é, obviamente, View. Assim, você geralmente começa estendendo-a para criar seu novo supercomponente.
  2. Você pode fornecer um construtor que pode receber atributos e parâmetros do XML. E você também pode consumir seus próprios atributos e parâmetros, talvez a cor e o intervalo do medidor VU, a largura e o amortecimento da agulha etc.
  3. É interessante que você crie seus próprios listeners de eventos, acessadores e modificadores de propriedade e, possivelmente, um comportamento mais sofisticado na sua classe de componentes.
  4. É muito recomendável modificar onMeasure() e provavelmente necessário modificar onDraw() se quiser que o componente mostre algo. Embora ambos tenham o comportamento padrão, o onDraw() padrão não fará nada, e o onMeasure() padrão sempre definirá um tamanho de 100 x 100, o que provavelmente não é o que você quer.
  5. Outros métodos on... também podem ser modificados conforme necessário.

Estender onDraw() e onMeasure()

O método onDraw() oferece um Canvas sobre o qual você pode implementar o que quiser: gráficos 2D, outros componentes padrão ou personalizados, texto estilizado ou qualquer outra coisa que você possa imaginar.

Observação: isso não se aplica a gráficos 3D. Se quiser usar gráficos 3D, você precisará estender SurfaceView em vez da visualização e desenhar a partir de uma linha de execução separada. Veja o exemplo de GLSurfaceViewActivity para detalhes.

onMeasure() está um pouco mais envolvido. onMeasure() é uma parte essencial do contrato de renderização entre seu componente e o contêiner dele. onMeasure() precisa ser modificado para informar com eficiência e precisão as medidas das partes contidas. Isso é um pouco mais complexo devido aos requisitos de limites do elemento pai (que são passados ao método onMeasure()) e pelo requisito de chamar o método setMeasuredDimension() com a largura e a altura medidas depois de serem calculadas. Se você não conseguir chamar esse método a partir de um método onMeasure() modificado, o resultado será uma exceção no momento da medição.

De forma geral, a implementação de onMeasure() é assim:

  1. O método onMeasure() modificado é chamado com especificações de medida de largura e altura (parâmetros widthMeasureSpec e heightMeasureSpec, ambos são códigos inteiros representando dimensões) que precisam ser tratadas como requisitos para as restrições das medidas de largura e altura a serem produzidas. Uma referência completa ao tipo de restrições que essas especificações podem exigir está disponível na documentação de referência em View.onMeasure(int, int). Essa documentação de referência explica muito bem toda a operação de medição.
  2. O método onMeasure() do componente precisa calcular a largura e a altura de medida que serão necessárias para renderizar o componente. Ele tem que tentar permanecer dentro das especificações passadas, embora possa optar por excedê-las. Nesse caso, o pai pode escolher o que fazer, incluindo recortar, rolar, lançar uma exceção ou pedir ao onMeasure() para tentar novamente, talvez com especificações de medição diferentes.
  3. Depois que a largura e a altura são calculadas, o método setMeasuredDimension(int width, int height) precisa ser chamado com as medidas calculadas. Se isso não for feito, uma exceção será lançada.

Veja um resumo de alguns dos outros métodos padrão que o framework chama em visualizações:

Categoria Métodos Descrição
Criação Construtores Há uma forma do construtor que é chamada quando a visualização é criada no código, e uma forma que é chamada quando a visualização é inflada de um arquivo de layout. A segunda forma precisa analisar e aplicar todos os atributos definidos no arquivo de layout.
onFinishInflate() Chamado depois que uma visualização e todos os filhos dela foram inflados do XML.
Layout onMeasure(int, int) Chamado para determinar os requisitos de tamanho da visualização e de todos os filhos.
onLayout(boolean, int, int, int, int) Chamado quando a visualização precisa atribuir um tamanho e uma posição a todos os filhos.
onSizeChanged(int, int, int, int) Chamado quando o tamanho da visualização mudou.
Desenho onDraw(Canvas) Chamado quando a visualização precisa renderizar o conteúdo.
Processamento de eventos onKeyDown(int, KeyEvent) Chamado quando ocorre um novo evento de tecla.
onKeyUp(int, KeyEvent) Chamado quando ocorre um evento de liberação de tecla.
onTrackballEvent(MotionEvent) Chamado quando ocorre um evento de movimento do trackball.
onTouchEvent(MotionEvent) Chamado quando ocorre um evento de movimento da tela touchscreen.
Foco onFocusChanged(boolean, int, Rect) Chamado quando a visualização ganha ou perde o foco.
onWindowFocusChanged(boolean) Chamado quando a janela que contém a visualização ganha ou perde o foco.
Anexo onAttachedToWindow() Chamado quando a visualização é anexada a uma janela.
onDetachedFromWindow() Chamado quando a visualização é separada da janela.
onWindowVisibilityChanged(int) Chamado quando a visibilidade da janela que contém a visualização foi alterada.

Controles compostos

Se você não quiser criar um componente completamente personalizado, mas quiser montar um componente reutilizável que consista em um grupo de controles existentes, a criação de um componente composto (ou controle composto) pode ser adequada. Para resumir, isso reúne uma série de controles mais atômicos (ou visualizações) em um grupo lógico de itens que podem ser tratados como uma única coisa. Por exemplo, uma caixa de combinação pode ser considerada uma combinação de um campo EditText de uma linha e um botão adjacente com uma PopupList anexada. Se você pressionar o botão e selecionar algo da lista, ele preencherá o campo "EditText", mas o usuário também poderá digitar algo diretamente no campo, se preferir.

No Android, há duas outras visualizações prontamente disponíveis para fazer isso: Spinner e AutoCompleteTextView. Mas independentemente disso, o conceito de caixa de combinação é um exemplo fácil de entender.

Para criar um componente composto:

  1. O ponto de partida usual é algum tipo de layout, então crie uma classe que estenda um layout. Talvez, no caso de uma caixa de combinação, possamos usar um LinearLayout com orientação horizontal. Lembre-se de que outros layouts podem ser aninhados internamente, de modo que o componente composto pode ser arbitrariamente complexo e estruturado. Observe que, assim como com uma atividade, você pode usar a abordagem declarativa (baseada em XML) para criar os componentes contidos ou pode aninhá-los de maneira programática no código.
  2. No construtor da nova classe, pegue os parâmetros esperados pela superclasse e passe-os para o construtor da superclasse primeiro. Em seguida, você pode configurar as outras visualizações para usar no novo componente. Este é o momento em que você cria o campo EditText e a PopupList. Você também pode introduzir seus próprios atributos e parâmetros no XML que podem ser extraídos e usados pelo construtor.
  3. Você também pode criar listeners para eventos que as visualizações contidas possam gerar, por exemplo, um método listener para o List Item Click Listener para atualizar o conteúdo do EditText se uma seleção de lista for feita.
  4. Você também pode criar suas próprias propriedades com acessadores e modificadores, por exemplo, permitir que o valor de EditText seja definido inicialmente no componente e consultar o conteúdo quando necessário.
  5. Ao estender um layout, não é necessário modificar os métodos onDraw() e onMeasure(), já que o layout terá um comportamento padrão que provavelmente funcionará bem. No entanto, você ainda poderá modificá-los se for necessário.
  6. Você pode modificar outros métodos on..., como onKeyDown(), para escolher determinados valores padrão na lista pop-up de uma caixa de combinação quando uma determinada tecla for pressionada.

Para resumir, o uso de um layout como base para um controle personalizado tem uma série de vantagens, incluindo:

  • Você pode especificar o layout usando os arquivos XML declarativos como faria com uma tela de atividades ou criar visualizações programaticamente e aninhá-las no layout do código.
  • Os métodos onDraw() e onMeasure() (e a maioria dos outros métodos on...) terão um comportamento adequado para que você não precise modificá-los.
  • No final, é possível construir rapidamente visualizações compostas arbitrariamente complexas e reutilizá-las como se fossem um único componente.

Como modificar um tipo de visualização existente

Há ainda uma opção mais fácil para criar uma visualização personalizada que é útil em determinadas circunstâncias. Se houver um componente que já é muito parecido com o que você quer, simplesmente estenda esse componente e modifique o comportamento que quer alterar. Você pode fazer tudo o que faria com um componente totalmente personalizado. No entanto, ao começar com uma classe mais especializada na hierarquia de visualização, também é possível ter muito comportamento livre que provavelmente faz exatamente o que você quer.

Por exemplo, o app Bloco de notas demonstra muitos aspectos do uso da plataforma Android. Entre eles, está a extensão da visualização EditText para criar um bloco de notas com linhas. Este não é um exemplo perfeito, e as APIs para fazer isso podem mudar, mas os princípios são demonstrados.

Se você ainda não tiver feito isso, importe a amostra do bloco de notas para o Android Studio ou veja o código fonte usando o link fornecido. Em particular, observe a definição de LinedEditText no arquivo NoteEditor.java.

Veja alguns itens a serem observados neste arquivo:

  1. A definição

    A classe é definida com a seguinte linha:
    public static class LinedEditText extends EditText

    • LinedEditText é definida como uma classe interna dentro da atividade NoteEditor, mas é pública para que possa ser acessada como NoteEditor.LinedEditText de fora da classe NoteEditor, se você quiser.
    • É static, o que significa que ela não gera os chamados “métodos sintéticos” que permitem acessar dados da classe pai, o que, por sua vez, significa que ela realmente se comporta como uma classe separada em vez de algo fortemente relacionado a NoteEditor. Essa é uma maneira mais limpa de criar classes internas se elas não precisarem acessar o estado da classe externa, manter a classe gerada pequena e permitir que ela seja usada facilmente de outras classes.
    • Ela estende EditText, que é a visualização que escolhemos para personalizar nesse caso. Quando terminarmos, a nova classe poderá substituir uma visualização normal de EditText.
  2. Inicialização da classe

    Como sempre, a superclasse é chamada primeiro. Além disso, este não é um construtor padrão, mas um construtor parametrizado. O EditText é criado com esses parâmetros quando é inflado de um arquivo de layout XML. Portanto, nosso construtor precisa pegá-los e passá-los para o construtor da superclasse também.

  3. Métodos modificados

    Este exemplo modifica apenas um método, onDraw(), mas talvez seja necessário modificar outros à medida que você cria seus próprios componentes personalizados.

    Para esse exemplo, modificar o método onDraw() nos permite pintar as linhas azuis na tela de visualização EditText. A tela é passada para o método modificado. O método super.onDraw() é chamado antes do término do método. O método da superclasse precisa ser invocado e, nesse caso, fazemos isso no final depois de pintarmos as linhas que queremos incluir.

  4. Usar o componente personalizado

    Temos o componente personalizado, mas como podemos usá-lo? No exemplo do bloco de notas, o componente personalizado é usado diretamente do layout declarativo. Veja note_editor.xml na pasta res/layout.

        <view xmlns:android="http://schemas.android.com/apk/res/android"
            class="com.example.android.notepad.NoteEditor$LinedEditText"
            android:id="@+id/note"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/transparent"
            android:padding="5dp"
            android:scrollbars="vertical"
            android:fadingEdge="vertical"
            android:gravity="top"
            android:textSize="22sp"
            android:capitalize="sentences"
        />
        
    • O componente personalizado é criado como uma visualização genérica no XML, e a classe é especificada usando o pacote completo. Observe também que a classe interna que definimos é referenciada usando a notação NoteEditor$LinedEditText, que é uma maneira padrão de se referir a classes internas na linguagem de programação Java.

      Se o componente de visualização personalizado não estiver definido como uma classe interna, você poderá, como alternativa, declarar o componente de visualização com o nome do elemento XML e excluir o atributo class. Exemplo:

          <com.example.android.notepad.LinedEditText
            id="@+id/note"
            ... />
          

      A classe LinedEditText agora é um arquivo de classe separado. Quando a classe está aninhada na classe NoteEditor, essa técnica não funciona.

    • Os outros atributos e parâmetros na definição são aqueles passados para o construtor de componente personalizado e passados para o construtor de EditText, de modo que sejam os mesmos parâmetros que você usaria para uma visualização de EditText. Observe que também é possível adicionar seus próprios parâmetros, e falaremos sobre isso novamente abaixo.

E isso é tudo. Evidentemente, esse é um caso simples, mas este é o ponto: a criação de componentes personalizados só é complicada se você precisar que seja.

Um componente mais sofisticado pode modificar ainda mais métodos on... e introduzir alguns dos próprios métodos auxiliares, personalizando substancialmente suas propriedades e comportamento. O único limite é sua imaginação e o que você precisa que o componente faça.