Números animados no Android

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:


demonstração


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), /* animated = */ 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 ).

Source: https://habr.com/ru/post/pt420919/


All Articles