Utilizando custom views no Android para criar templates de layout
Recentemente o designer da empresa em que trabalho, decidiu fazer uma reformulação em algumas telas do nosso app, visto que elas tinham contexto parecido, mas design totalmente diferente, tornando a experiência do usuário um pouco conflituosa.
Para montar o novo design desta telas, basicamente eu teria um coordinator Layout, com um card sobrepondo o coordinator, no qual o próprio conteúdo do card tinha a sua estrutura de widgets parecida, com algumas pequenas diferenças aqui e ali.
Como sabemos, infelizmente o framework do Android não permite de forma nativa ter uma estrutura de templates parecida com o desenvolvimento web, onde podemos definir partes estáticas e dinâmicas de um layout, para o reuso. Uma das formas de reutilização de layouts presente no framework é utilizar a tag include, que permite inserir um layout completo, mas não conseguimos modificar muito o conteúdo do mesmo, apenas atributos do tipo android:layout do layout sendo incluido.
Utilizar a tag include não seria útil em nosso caso, visto que não conseguiriamos modificar apenas algumas partes do card, nem reutilizar todo o sistema de ter o coordinator com o card sobreposto, já que o resto da estrutura dos layouts não era igual.
Após assistir a excelente talk do Daniel Lew Design Like a Coder: Efficient Android Layouts, eu aprendi sobre reutilização de layouts utilizando a tag merge e criando custom views.
Custom views permitem não só reutilizar layouts, como inserir lógica em nossas Views, sendo uma forma extremamente poderosa para alcançarmos nosso objetivo. Com elas, eu fui capaz de reutilizar layouts da forma que descrevi acima, muito parecido com reutilização de htmls no desenvolvimento web.
Show me the code
Vamos mostrar um exemplo prático de como podemos criar um Card reutilizável, na qual a estrutura da parte superior e inferior é estática, mas queremos que o que está entre seja dinâmico.
Inicialmente iremos criar o layout que agirá como template para nosso card.
Podemos observar que não temos um ViewGroup como parent neste layout. Estamos utilizando a tag <merge>, que nos permite evitar redundância de layouts. Além disso, observe que utilizamos dois TextViews, um no topo e outro na parte inferior, para demonstrar a parte estática do nosso layout. Para a parte dinâmica, adicionamos uma View com id placeholder, com atributos de forma em que esta view fique entre os dois textos.
Após isso, iremos criar uma subclasse do ViewGroup que será o parent do nosso layout. Em nosso caso, queremos criar um CardView.
Observe que inflamos o layout criado anteriormente e que precisamos colocar o terceiro atributo attachToRoot como true, para que a nossa subclasse do CardView se torna o parent de todo o conteúdo que está entre a tag merge do layout sendo inflado.
Somente criando a subclasse e inflando o layout do nosso card ainda não temos o resultado desejado, ainda falta transformar nosso placeholder em um conteúdo dinâmico, para isso, iremos nos aproveitar de atributos customizados (custom attributes).
Adicionando atributos customizados
Iremos adicionar um atributo chamado cardType, que nos permite escolher entre dois modelos de card: cardA e cardB, para isso, é necessário adicionar o arquivo attrs dentro de res/values
Nosso atributo tem formato de enum, que contempla as duas opções mencionadas.
Após isso, iremos criar dois novos layouts, card_a.xml e card_b.xml, que terão o conteúdo dinâmico do nosso card:
Após isso, iremos modificar o MyCustomCard.java para que, baseado em nosso atributo, infle os layouts desejados no local do placeholder.
Vamos analisar o que está acontecendo em cada método do nosso Card.
O método initAttrs, procura dentro dos atributos associados com o card pelo atributo cardType e armazena no atributo mCardType.
O método addMiddleContent é o responsável pela mágica de colocar o layout correto no local do placeholder. Para isso, inicialmente procuramos pelo ViewGroup que é o pai do nosso placeholder (Como estamos utilizando um CardView, geralmente precisamos de um outro ViewGroup como filho para ser responsável pelo alinhamento das views, que neste caso é um ConstraintLayout) e o próprio placeholder. Após isso, utilzando o cardType, utilizamos o método retrieveMiddleLayoutRes para procurar pelo id do layout desejado.
Finalmente, procuramos pelo próprio placeholder e armazenamos o seu LayoutParams em uma variável. Neste ponto, pode parecer desnecessário termos uma view para ficar de placeholder somente para facilitar o alinhamento, visto que poderíamos criar o LayoutParams manualmente, porém, foi utilizada essa solução para que se por algum motivo quisermos trocar o ConstraintLayout para outro elemento, não ser necessário ter que ir na custom view e modificar a criação destes parâmetros. Dito isto, inflamos o layout correto, configuramos seu LayoutParams para ser igual ao do placeholder e realizamos a troca das Views.
Agora temos nosso card, no qual é possível dinamicamente modificarmos somente uma parte dele com atributos xml, tornando-o altamente reutilizável.
Utilizando nosso card
Para utilizar o card, basta adicionarmos nosso card em um layout qualquer, como qualquer outro card, e configurar nosso atributo cardType para uma das duas opções.
Ah, e o mais legal de tudo, quando você troca o cardType, você consegue ver as modificações ocorrerem no editor de Layout, não é necessário ter que instalar o app para ver somente em tempo de execução. =)
Você pode encontrar o sample no GitHub.
