Uma interface de usuário bonita e atraente é importante. Portanto, para Android, há um grande número de bibliotecas para uma bela exibição de elementos de design. Geralmente, um aplicativo precisa mostrar um campo com um número ou algum tipo de contador. Por exemplo, um contador para o número de itens da lista selecionados ou o valor das despesas de um mês. Obviamente, essa tarefa pode ser facilmente resolvida com a ajuda de um TextView
comum, mas você pode resolvê-la com elegância e adicionar a animação de alterar o número:

Vídeos de demonstração estão disponíveis no YouTube.
O artigo falará sobre como implementar tudo isso.
Um dígito estático
Para cada um dos números, há uma imagem vetorial, por exemplo, para 8 é res/drawable/viv_vd_pathmorph_digits_eight.xml
:
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="@dimen/viv_digit_size" android:height="@dimen/viv_digit_size" android:viewportHeight="1" android:viewportWidth="1"> <group android:translateX="@dimen/viv_digit_translateX" android:translateY="@dimen/viv_digit_translateY"> <path android:name="iconPath" android:pathData="@string/viv_path_eight" android:strokeColor="@color/viv_digit_color_default" android:strokeWidth="@dimen/viv_digit_strokewidth"/> </group> </vector>
Além dos números de 0 a 9, também são necessárias imagens de sinal de menos ( viv_vd_pathmorph_digits_minus.xml
) e uma imagem em branco ( viv_vd_pathmorph_digits_nth.xml
), que simbolizam o dígito que desaparece do número durante a animação.
Os arquivos de imagem XML diferem apenas no android:pathData
. Todos os outros atributos são definidos por conveniência através de recursos separados e são os mesmos para todas as imagens vetoriais.
Imagens para os números 0-9 foram tiradas aqui .
Animação de transição
As imagens vetoriais descritas são imagens estáticas. Para animação, você precisa adicionar imagens vetoriais animadas ( <animated-vector>
). Por exemplo, para animar o número 2, adicione o arquivo res/drawable/viv_avd_pathmorph_digits_2_to_5.xml
ao número 5:
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:drawable="@drawable/viv_vd_pathmorph_digits_zero"> <target android:name="iconPath"> <aapt:attr name="android:animation"> <objectAnimator android:duration="@integer/viv_animation_duration" android:propertyName="pathData" android:valueFrom="@string/viv_path_two" android:valueTo="@string/viv_path_five" android:valueType="pathType"/> </aapt:attr> </target> </animated-vector>
Aqui, por conveniência, definimos a duração da animação por meio de um recurso separado. No total, temos 12 imagens estáticas (0 - 9 + "menos" + "vazio"), cada uma delas pode ser animada em qualquer uma das outras. Acontece que, para completar, são necessários 12 * 11 = 132 arquivos de animação. Eles android:valueFrom
apenas nos atributos android:valueFrom
e android:valueTo
, e criá-los manualmente não é uma opção. Portanto, escreveremos um gerador simples:
Gerador de arquivo de animação import java.io.File import java.io.FileWriter fun main(args: Array<String>) { val names = arrayOf( "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "nth", "minus" ) fun getLetter(i: Int) = when (i) { in 0..9 -> i.toString() 10 -> "n" 11 -> "m" else -> null!! } val dirName = "viv_out" File(dirName).mkdir() for (from in 0..11) { for (to in 0..11) { if (from == to) continue FileWriter(File(dirName, "viv_avd_pathmorph_digits_${getLetter(from)}_to_${getLetter(to)}.xml")).use { it.write(""" <?xml version="1.0" encoding="utf-8"?> <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:drawable="@drawable/viv_vd_pathmorph_digits_zero"> <target android:name="iconPath"> <aapt:attr name="android:animation"> <objectAnimator android:duration="@integer/viv_animation_duration" android:propertyName="pathData" android:valueFrom="@string/viv_path_${names[from]}" android:valueTo="@string/viv_path_${names[to]}" android:valueType="pathType"/> </aapt:attr> </target> </animated-vector> """.trimIndent()) } } } }
Todos juntos
Agora você precisa conectar as imagens vetoriais estáticas e as animações de transição em um arquivo <animated-selector>
, que, como um <selector>
normal, exibe uma das imagens, dependendo do estado atual. Este recurso desenhável ( res/drawable/viv_asl_pathmorph_digits.xml
) contém declarações de estados de imagem e transições entre eles.
Os estados são definidos usando as tags <item>
com uma imagem e um atributo de estado (nesse caso, app:viv_state_three
) que define essa imagem. Cada estado possui um id
, usado para determinar a animação de transição desejada:
<item android:id="@+id/three" android:drawable="@drawable/viv_vd_pathmorph_digits_three" app:viv_state_three="true" />
Os atributos de estado são configurados no res/values/attrs.xml
:
<resources> <declare-styleable name="viv_DigitState"> <attr name="viv_state_zero" format="boolean" /> <attr name="viv_state_one" format="boolean" /> <attr name="viv_state_two" format="boolean" /> <attr name="viv_state_three" format="boolean" /> <attr name="viv_state_four" format="boolean" /> <attr name="viv_state_five" format="boolean" /> <attr name="viv_state_six" format="boolean" /> <attr name="viv_state_seven" format="boolean" /> <attr name="viv_state_eight" format="boolean" /> <attr name="viv_state_nine" format="boolean" /> <attr name="viv_state_nth" format="boolean" /> <attr name="viv_state_minus" format="boolean" /> </declare-styleable> </resources>
As animações de transições entre estados são definidas por tags <transition>
com uma indicação de <animated-vector>
, que simboliza a transição, bem como o id
inicial e final:
<transition android:drawable="@drawable/viv_avd_pathmorph_digits_6_to_2" android:fromId="@id/six" android:toId="@id/two" />
O conteúdo de res/drawable/viv_asl_pathmorph_digits.xml
praticamente res/drawable/viv_asl_pathmorph_digits.xml
mesmo, e um gerador também foi usado para criá-lo. Este recurso desenhável consiste em 12 estados e 132 transições entre eles.
Customview
Agora que temos um drawable
que permite exibir um dígito e animar sua alteração, precisamos criar um VectorIntegerView
que contenha vários dígitos e controle as animações. RecyclerView
foi escolhido como base, já que o número de dígitos no número é um valor variável, e o RecyclerView
é a melhor maneira no Android para exibir um número variável de elementos (números) em uma linha. Além disso, o RecyclerView
permite controlar animações de itens por meio do ItemAnimator
.
DigitAdapter e DigitViewHolder
Você precisa começar criando um DigitViewHolder
contendo um dígito. View
desse DigitViewHolder
consistirá em um único ImageView
com o android:src="@drawable/viv_asl_pathmorph_digits"
. Para exibir o dígito desejado no ImageView
, é usado o método mImageView.setImageState(state, true);
. A matriz de estado do state
é formada com base no dígito exibido usando os atributos de estado viv_DigitState
definidos acima.
Exiba o dígito desejado no `ImageView` private static final int[] ATTRS = { R.attr.viv_state_zero, R.attr.viv_state_one, R.attr.viv_state_two, R.attr.viv_state_three, R.attr.viv_state_four, R.attr.viv_state_five, R.attr.viv_state_six, R.attr.viv_state_seven, R.attr.viv_state_eight, R.attr.viv_state_nine, R.attr.viv_state_nth, R.attr.viv_state_minus, }; void setDigit(@IntRange(from = 0, to = VectorIntegerView.MAX_DIGIT) int digit) { int[] state = new int[ATTRS.length]; for (int i = 0; i < ATTRS.length; i++) { if (i == digit) { state[i] = ATTRS[i]; } else { state[i] = -ATTRS[i]; } } mImageView.setImageState(state, true); }
O adaptador DigitAdapter
responsável por criar o DigitViewHolder
e exibir o dígito desejado no DigitViewHolder
desejado.
Para a animação correta de transformar um número em outro, o DiffUtil
usado. Com ele, a ordem das dezenas é animada para a ordem das dezenas, centenas a centenas, dezenas de milhões a dezenas de milhões, e assim por diante. O símbolo de menos sempre permanece por si só e só pode aparecer ou desaparecer, transformando-se em uma imagem vazia ( viv_vd_pathmorph_digits_nth.xml
).
Para fazer isso, DiffUtil.Callback
no método DiffUtil.Callback
retorna true
somente se dígitos idênticos de números forem comparados. O menos é uma categoria especial e o menos do número anterior é igual ao menos do novo número.
O método areContentsTheSame
compara caracteres em determinadas posições nos números anteriores e novos. A implementação em si pode ser vista no DigitAdapter
.
DigitItemAnimator
A animação da mudança no número, a transformação, a aparência e o desaparecimento dos números, será controlada por um animador especial para o RecyclerView
- DigitItemAnimator
. Para determinar a duração das animações, o mesmo recurso integer
é usado como no <animated-vector>
descrito acima:
private final int animationDuration; DigitItemAnimator(@NonNull Resources resources) { animationDuration = resources.getInteger(R.integer.viv_animation_duration); } @Override public long getMoveDuration() { return animationDuration; } @Override public long getAddDuration() { return animationDuration; } @Override public long getRemoveDuration() { return animationDuration; } @Override public long getChangeDuration() { return animationDuration; }
A maior parte do DigitItemAnimator
está substituindo os métodos de aminação. A animação da aparência de um dígito (método animateAdd
) é realizada como uma transição de uma imagem vazia para o dígito ou sinal de menos desejado. A animação de desaparecimento (método animateRemove
) é executada como uma transição do dígito ou sinal de menos exibido para uma imagem em branco.
Para executar uma animação de alteração de dígito, as informações no dígito exibido anterior são armazenadas primeiro substituindo o método recordPreLayoutInformation
. Em seguida, no método animateChange
, a transição do dígito exibido anterior para o novo é realizada.
RecyclerView.ItemAnimator
requer que os métodos de animação substituídos precisem invocar métodos que simbolizem o final da animação. Portanto, em cada um dos animateAdd
, animateRemove
e animateChange
, há uma chamada para o método correspondente com um atraso igual à duração da animação. Por exemplo, o método animateAdd
chama o método dispatchAddFinished
com um atraso igual a @integer/viv_animation_duration
:
@Override public boolean animateAdd(final RecyclerView.ViewHolder holder) { final DigitAdapter.DigitViewHolder digitViewHolder = (DigitAdapter.DigitViewHolder) holder; int a = digitViewHolder.d; digitViewHolder.setDigit(VectorIntegerView.DIGIT_NTH); digitViewHolder.setDigit(a); holder.itemView.postDelayed(new Runnable() { @Override public void run() { dispatchAddFinished(holder); } }, animationDuration); return false; }
VectorIntegerView
Antes de criar um CustomView, você precisa definir seus atributos xml. Para fazer isso, adicione <declare-styleable>
ao res/values/attrs.xml
:
<declare-styleable name="VectorIntegerView"> <attr name="viv_vector_integer" format="integer" /> <attr name="viv_digit_color" format="color" /> </declare-styleable>
O VectorIntegerView
criado terá 2 atributos xml para personalização:
viv_vector_integer
número exibido ao criar a visualização (0 por padrão).viv_digit_color
cor dos números (preto por padrão).
Outros parâmetros do VectorIntegerView
podem ser alterados substituindo os recursos no aplicativo (como é feito no aplicativo de demonstração ):
@integer/viv_animation_duration
determina a duração da animação ( @integer/viv_animation_duration
por padrão).@dimen/viv_digit_size
determina o tamanho de um dígito ( 24dp
por padrão).@dimen/viv_digit_translateX
se aplica a todas as imagens vetoriais de dígitos para alinhá-las horizontalmente.@dimen/viv_digit_translateY
é aplicado a todas as imagens vetoriais de dígitos para alinhá-las verticalmente.@dimen/viv_digit_strokewidth
se aplica a todas as imagens vetoriais de dígitos.@dimen/viv_digit_margin_horizontal
se aplica a todos os dígitos da visualização ( DigitViewHolder
) ( -3dp
por padrão). Isso é necessário para diminuir os espaços entre os números, pois as imagens vetoriais dos números são quadradas.
Recursos VectorIntegerView
serão aplicados a todos os VectorIntegerView
no aplicativo.
Todos esses parâmetros são definidos por meio de recursos, pois é impossível redimensionar o VectorDrawable
ou a duração da animação AnimatedVectorDrawable
através do código.
A adição de VectorIntegerView
à marcação XML fica assim:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.qwert2603.vector_integer_view.VectorIntegerView android:id="@+id/vectorIntegerView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" app:viv_digit_color="#ff8000" app:viv_vector_integer="14" /> </FrameLayout>
Posteriormente, você pode alterar o número exibido no código passando BigInteger
:
final VectorIntegerView vectorIntegerView = findViewById(R.id.vectorIntegerView); vectorIntegerView.setInteger( vectorIntegerView.getInteger().add(BigInteger.ONE), true );
Por conveniência, existe um método para transmitir um número de tipo long
:
vectorIntegerView.setInteger(1918L, false);
Se false
passado como animated
, o método notifyDataSetChanged
será chamado para o notifyDataSetChanged
e o novo número será exibido sem animações.
Ao recriar o VectorIntegerView
número exibido é salvo usando os onRestoreInstanceState
e onRestoreInstanceState
.
Código fonte
O código fonte está disponível no github (diretório da biblioteca). Há também um aplicativo de demonstração usando o VectorIntegerView
(diretório de aplicativos).
Há também um apk de demonstração ( minSdkVersion 21
).