Criar componentes de visualização personalizados

Teste o Compose
O Jetpack Compose é o kit de ferramentas de interface recomendado para Android. Aprenda a trabalhar com layouts no Compose.

O Android oferece um modelo de componentes sofisticado e eficiente para criar sua IU, com base nas classes fundamentais de layout View e ViewGroup. A plataforma inclui uma variedade de subclasses View e ViewGroup predefinidas, chamadas de widgets e layouts, respectivamente, que podem ser usadas para criar a interface.

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

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

Se nenhum dos widgets ou layouts pré-criados atender às suas necessidades, você pode criar sua própria subclasse de View. Se você só precisar fazer pequenos ajustes em um widget ou layout já existente, poderá criar uma subclasse para o widget ou layout e substituir os métodos correspondentes.

A criação das suas subclasses View oferece 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:

  • É possível criar um tipo View de renderização personalizada completamente. Por exemplo, um botão de "controle de volume", renderizado usando gráficos 2D, que se assemelha a um controle eletrônico analógico.
  • Você pode combinar um grupo de componentes View em um novo componente único, talvez para criar algo como uma caixa de combinação (uma combinação de lista pop-up e um campo de texto de entrada livre), um controle seletor de painel duplo (um painel esquerdo e direito com uma lista em cada um, em que é possível reatribuir qual item está em qual lista) e assim por diante.
  • É possível substituir a forma como um componente EditText é renderizado na tela. O app de exemplo NotePad usa isso para criar uma página de bloco de notas alinhada.
  • É possível capturar outros eventos, como pressionamentos de teclas, e processá-los de maneira personalizada, por exemplo, em um jogo.

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

A abordagem básica

Confira uma visão geral de alto nível do que você precisa saber para criar seus próprios componentes View:

  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 serão substituídos começam com on, por exemplo, onDraw(), onMeasure() e onKeyDown(). Isso é semelhante aos eventos on em Activity ou ListActivity que você substitui para o ciclo de vida e outros hooks de funcionalidade.
  3. Use sua nova classe de extensão. Depois de concluída, você pode usar a nova classe de extensão no lugar da visualização em que ela foi baseada.

Componentes totalmente personalizados

É possível criar componentes gráficos totalmente personalizados que aparecem como você quiser. Talvez você queira um medidor de VU gráfico que pareça um medidor analógico antigo ou uma visualização de texto em que uma bola saltitante se move ao longo das palavras enquanto você canta com uma máquina de karaokê. Você pode querer algo que os componentes integrados não podem fazer, independente de como você os combine.

Felizmente, é possível criar componentes com a aparência e o comportamento que você quiser, limitados apenas pela sua imaginação, pelo tamanho da tela e pela capacidade de processamento disponível, tendo em mente que seu aplicativo pode precisar ser executado em algo com significativamente menos energia do que a estação de trabalho do computador.

Para criar um componente totalmente personalizado, considere o seguinte:

  • A visualização mais genérica que você pode estender é View. Portanto, você geralmente começa estendendo isso para criar seu novo supercomponente.
  • Você pode fornecer um construtor, que pode receber atributos e parâmetros do XML, e consumir seus próprios atributos e parâmetros, como a cor e o intervalo do medidor VU ou a largura e o amortecimento da agulha.
  • Você provavelmente quer criar seus próprios listeners de eventos, acessadores de propriedades e modificadores, bem como um comportamento mais sofisticado na sua classe de componentes.
  • É muito provável que você queira substituir onMeasure(), e é provável que precise substituir onDraw() se quiser que o componente mostre algo. Embora ambos tenham o comportamento padrão, a onDraw() padrão não faz nada, e a onMeasure() padrão sempre define um tamanho de 100 x 100, o que você provavelmente não quer.
  • Também é possível substituir outros métodos on, conforme necessário.

Estender onDraw() e onMeasure()

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

onMeasure() está um pouco mais envolvido. O onMeasure() é uma parte essencial do contrato de renderização entre seu componente e o contêiner. onMeasure() precisa ser substituído para informar com eficiência e precisão as medidas das partes contidas. Isso é um pouco mais complexo pelos requisitos de limite do pai, que são transmitidos ao método onMeasure(), e pelo requisito de chamar o método setMeasuredDimension() com a largura e a altura medidas depois que eles são calculados. Se você não chamar esse método de um método onMeasure() substituído, isso resultará em uma exceção no momento da medição.

De modo geral, a implementação de onMeasure() tem esta aparência:

  • O método onMeasure() substituído é chamado com especificações de largura e altura, que são tratadas como requisitos para as restrições das medidas de largura e altura que você produz. Os parâmetros widthMeasureSpec e heightMeasureSpec são códigos inteiros que representam dimensões. 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 também explica toda a operação de medição.
  • O método onMeasure() do componente calcula a largura e a altura de medida, que são necessárias para renderizar o componente. Ele precisa tentar permanecer dentro das especificações transmitidas, embora possa excedê-las. Nesse caso, o pai pode escolher o que fazer, incluindo recorte, rolagem, geração de uma exceção ou pedir ao onMeasure() para tentar novamente, talvez com especificações de medição diferentes.
  • Quando a largura e a altura forem calculadas, chame o método setMeasuredDimension(int width, int height) com as medidas calculadas. Se isso não for feito, será gerada uma exceção.

Veja um resumo de 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 a partir do código, e uma forma que é chamada quando a visualização é inflada de um arquivo de layout. A segunda analisa e aplica atributos definidos no arquivo de layout.
onFinishInflate() Chamado depois que uma visualização e todos os filhos dela são inflados do XML.
Layout onMeasure(int, int) Chamado para determinar os requisitos de tamanho da visualização e de todas as filhas dela.
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 é alterado.
Desenho onDraw(Canvas) Chamado quando a visualização precisa renderizar o conteúdo.
Processamento de eventos onKeyDown(int, KeyEvent) Chamado quando ocorre um evento de pressionamento 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 na 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 é removida da janela.
onWindowVisibilityChanged(int) Chamado quando a visibilidade da janela que contém a visualização é alterada.

Controles compostos

Se você não quer criar um componente totalmente personalizado, mas quer montar um componente reutilizável que consiste em um grupo de controles existentes, a criação de um componente composto (ou controle composto) pode ser melhor. Em resumo, isso reúne vários controles ou visualizações atômicos em um grupo lógico de itens que podem ser tratados como uma única coisa. Por exemplo, uma caixa de combinação pode ser uma combinação de um campo EditText de uma linha e um botão adjacente com uma lista pop-up anexada. Se o usuário tocar no botão e selecionar algo da lista, ele preencherá o campo EditText, mas também poderá digitar algo diretamente no EditText, se preferir.

No Android, há duas outras visualizações disponíveis para fazer isso: Spinner e AutoCompleteTextView. Independentemente disso, esse conceito de caixa de combinação é um bom exemplo.

Para criar um componente composto, faça o seguinte:

  • Assim como com um Activity, use a abordagem declarativa (baseada em XML) para criar os componentes contidos ou aninhá-los de maneira programática no código. O ponto de partida comum é algum tipo de Layout. Portanto, crie uma classe que estenda um Layout. No caso de uma caixa de combinação, use uma LinearLayout com orientação horizontal. Você pode aninhar outros layouts dentro, de modo que o componente composto possa ser complexo e estruturado arbitrariamente.
  • No construtor da nova classe, use os parâmetros esperados pela superclasse e os transmita para o construtor da superclasse primeiro. Em seguida, você pode configurar as outras visualizações para usar no novo componente. É aqui que você cria o campo EditText e a lista pop-up. Você pode introduzir seus próprios atributos e parâmetros no XML que seu construtor pode extrair e usar.
  • Opcionalmente, crie listeners para eventos que suas visualizações contidas podem gerar. Um exemplo é um método listener para o listener de clique do item da lista atualizar o conteúdo de EditText se uma seleção de lista for feita.
  • Opcionalmente, crie suas próprias propriedades com acessadores e modificadores. Por exemplo, deixe o valor EditText ser definido inicialmente no componente e consulte o conteúdo dele quando necessário.
  • Opcionalmente, substitua onDraw() e onMeasure(). Isso geralmente não é necessário ao estender um Layout, já que o layout tem um comportamento padrão que provavelmente funciona bem.
  • Opcionalmente, substitua outros métodos on, como onKeyDown(), por exemplo, para escolher determinados valores padrão na lista pop-up de uma caixa de combinação quando uma determinada tecla for tocada.

Há vantagens em usar um Layout como base para um controle personalizado, incluindo:

  • É possível especificar o layout usando os arquivos XML declarativos, como em uma tela de atividades, ou criar visualizações de forma programática e aninhá-las no layout do código.
  • Os métodos onDraw() e onMeasure(), assim como a maioria dos outros métodos on, têm um comportamento adequado. Portanto, não é necessário substituí-los.
  • É possível construir rapidamente visualizações compostas arbitrariamente complexas e reutilizá-las como se fossem um único componente.

Modificar um tipo de visualização existente

Se houver um componente semelhante ao que você quer, estenda esse componente e modifique o comportamento que quer mudar. Você pode fazer tudo o que faz com um componente totalmente personalizado, mas, começando com uma classe mais especializada na hierarquia View, é possível obter um comportamento que faz o que você quiser sem custo financeiro.

O app de exemplo NotePad demonstra muitos aspectos do uso da plataforma Android. Entre elas, está a extensão de uma visualização EditText para criar um bloco de notas com linhas. Esse 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 o exemplo do Bloco de notas para o Android Studio ou consulte a fonte usando o link fornecido. Mais especificamente, consulte a definição de LinedEditText no arquivo NoteEditor.java.

Veja alguns itens a serem observados neste arquivo:

  1. 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.

    Além disso, LinedEditText é static, o que significa que ele não gera os chamados "métodos sintéticos" que permitem acessar dados da classe pai. Isso significa que ela 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 precisam de acesso ao estado da classe externa. Ela mantém a classe gerada pequena e permite que ela seja usada facilmente em outras classes.

    LinedEditText estende EditText, que é a visualização a ser personalizada nesse caso. Quando você terminar, a nova classe poderá substituir uma visualização normal de EditText.

  2. Inicialização da classe

    Como sempre, a superclasse é chamada primeiro. Este não é um construtor padrão, mas é parametrizado. O EditText é criado com esses parâmetros quando é inflado de um arquivo de layout XML. Assim, o construtor precisa pegá-los e transmiti-los para o construtor da superclasse.

  3. Métodos modificados

    Este exemplo substitui apenas o método onDraw(), mas talvez seja necessário substituir os outros à medida que você cria seus componentes personalizados.

    Neste exemplo, modificar o método onDraw() permite pintar as linhas azuis na tela de visualização EditText. A tela é transmitida para o método onDraw() substituído. O método super.onDraw() é chamado antes do término do método. É necessário invocar o método da superclasse. Nesse caso, invoque-o no final depois de pintar as linhas que você quer incluir.

  4. Componente personalizado

    Agora você tem seu componente personalizado, mas como usá-lo? No exemplo do bloco de notas, o componente personalizado é usado diretamente do layout declarativo. Portanto, observe 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. A classe interna que você define é 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á declarar o componente de visualização com o nome do elemento XML e excluir o atributo class. Por 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 transmitidos ao construtor do componente personalizado e, em seguida, transmitidos ao construtor EditText. Portanto, são os mesmos parâmetros usados para uma visualização EditText. Também é possível adicionar seus próprios parâmetros.

Criar componentes personalizados é tão complicado quanto você precisa que seja.

Um componente mais sofisticado pode substituir ainda mais métodos on e introduzir os 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.