Números animados en Android

Una interfaz de usuario hermosa y atractiva es importante. Por lo tanto, para Android, hay una gran cantidad de bibliotecas para una hermosa visualización de elementos de diseño. A menudo, en la aplicación, debe mostrar un campo con un número o algún tipo de contador. Por ejemplo, un contador para el número de elementos de la lista seleccionados o la cantidad de gastos para un mes. Por supuesto, dicha tarea puede resolverse fácilmente con la ayuda de un TextView normal, pero puede resolverlo con elegancia y agregar animación para cambiar el número:


demo


Los videos de demostración están disponibles en YouTube.


El artículo hablará sobre cómo implementar todo esto.


Un dígito estático


Para cada uno de los números hay una imagen vectorial, por ejemplo, para 8 es 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> 

Además de los números 0-9, también se requieren imágenes de signos menos ( viv_vd_pathmorph_digits_minus.xml ) y una imagen en blanco ( viv_vd_pathmorph_digits_nth.xml ), que simbolizará el dígito que desaparece del número durante la animación.
Los archivos de imagen XML solo difieren en el android:pathData . Todos los demás atributos se configuran por conveniencia a través de recursos separados y son los mismos para todas las imágenes vectoriales.
Aquí se tomaron imágenes para los números 0-9.


Animación de transición


Las imágenes vectoriales descritas son imágenes estáticas. Para la animación, debe agregar imágenes vectoriales animadas ( <animated-vector> ). Por ejemplo, para animar el número 2, agregue el archivo res/drawable/viv_avd_pathmorph_digits_2_to_5.xml al 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> 

Aquí, por conveniencia, establecemos la duración de la animación a través de un recurso separado. En total, tenemos 12 imágenes estáticas (0 - 9 + "menos" + "vacío"), cada una de ellas puede ser animada en cualquiera de las otras. Resulta que para completar, se requieren 12 * 11 = 132 archivos de animación. Solo android:valueFrom en los atributos android:valueFrom y android:valueTo , y crearlos manualmente no es una opción. Por lo tanto, escribiremos un generador simple:


Generador de archivos de animación
 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


Ahora necesita conectar las imágenes vectoriales estáticas y las animaciones de transición en un archivo <animated-selector> , que, como un <selector> normal, muestra una de las imágenes según el estado actual. Este recurso res/drawable/viv_asl_pathmorph_digits.xml ( res/drawable/viv_asl_pathmorph_digits.xml ) contiene declaraciones de estados de imagen y transiciones entre ellos.


Los estados se establecen utilizando etiquetas <item> con una imagen y un atributo de estado (en este caso, app:viv_state_three ) que define esta imagen. Cada estado tiene una id , que se utiliza para determinar la animación de transición deseada:


 <item android:id="@+id/three" android:drawable="@drawable/viv_vd_pathmorph_digits_three" app:viv_state_three="true" /> 

Los atributos de estado se establecen en el 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> 

Las animaciones de las transiciones entre estados se establecen mediante etiquetas <transition> con una indicación de <animated-vector> , que simboliza la transición, así como la id estado inicial y final:


 <transition android:drawable="@drawable/viv_avd_pathmorph_digits_6_to_2" android:fromId="@id/six" android:toId="@id/two" /> 

El contenido de res/drawable/viv_asl_pathmorph_digits.xml prácticamente res/drawable/viv_asl_pathmorph_digits.xml mismo, y también se utilizó un generador para crearlo. Este recurso extraíble consta de 12 estados y 132 transiciones entre ellos.


Customview


Ahora que tenemos un drawable que nos permite mostrar un dígito y animar su cambio, necesitamos crear un VectorIntegerView que contendrá varios dígitos y controlará las animaciones. Se eligió RecyclerView como base, ya que el número de dígitos en el número es un valor variable, y RecyclerView es la mejor manera en Android para mostrar un número variable de elementos (números) en una fila. Además, RecyclerView permite controlar animaciones de elementos a través de ItemAnimator .


DigitAdapter y DigitViewHolder


DigitViewHolder comenzar creando un DigitViewHolder contenga un dígito. View dicho DigitViewHolder consistirá en un único ImageView con android:src="@drawable/viv_asl_pathmorph_digits" . Para mostrar el dígito deseado en ImageView , se utiliza el método mImageView.setImageState(state, true); . La matriz de estado del state se forma en función del dígito mostrado utilizando los viv_DigitState estado viv_DigitState definidos anteriormente.


Mostrar el dígito deseado en el `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); } 

El adaptador DigitAdapter responsable de crear DigitViewHolder y de mostrar el dígito deseado en el DigitViewHolder deseado.


Para la animación correcta de convertir un número en otro, DiffUtil utiliza DiffUtil . Con él, el rango de decenas se anima al rango de decenas, cientos a cientos, decenas de millones a decenas de millones, y así sucesivamente. El símbolo menos siempre permanece solo y solo puede aparecer o desaparecer, convirtiéndose en una imagen vacía ( viv_vd_pathmorph_digits_nth.xml ).


Para hacer esto, DiffUtil.Callback en el método DiffUtil.Callback devuelve true solo si se comparan dígitos idénticos de números. El menos es una categoría especial, y el menos del número anterior es igual al menos del nuevo número.


El método areContentsTheSame compara caracteres en ciertas posiciones en los números anteriores y nuevos. La implementación en sí se puede ver en DigitAdapter .


DigitItemAnimator


La animación del cambio en el número, es decir, la transformación, aparición y desaparición de números, será controlada por un animador especial para RecyclerView - DigitItemAnimator . Para determinar la duración de las animaciones, se utiliza el mismo recurso integer que en el <animated-vector> descrito anteriormente:


 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; } 

La mayor parte de DigitItemAnimator está anulando los métodos de aminación. La animación de la apariencia de un dígito (método animateAdd ) se realiza como una transición de una imagen vacía al dígito deseado o al signo menos. La animación de desaparición (método animateRemove ) se realiza como una transición desde el dígito mostrado o el signo menos a una imagen en blanco.


Para realizar una animación de cambio de dígitos, la información sobre el dígito mostrado anteriormente se almacena primero anulando el método recordPreLayoutInformation . Luego, en el método animateChange , se realiza la transición del dígito mostrado anteriormente al nuevo.


RecyclerView.ItemAnimator requiere que los métodos de animación de anulación deben invocar métodos que simbolicen el final de la animación. Por lo tanto, en cada uno de los animateAdd , animateRemove y animateChange , hay una llamada al método correspondiente con un retraso igual a la duración de la animación. Por ejemplo, el método animateAdd llama al método dispatchAddFinished con un retraso 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 crear un CustomView, debe definir sus atributos xml. Para hacer esto, agregue <declare-styleable> al res/values/attrs.xml :


 <declare-styleable name="VectorIntegerView"> <attr name="viv_vector_integer" format="integer" /> <attr name="viv_digit_color" format="color" /> </declare-styleable> 

El VectorIntegerView creado tendrá 2 atributos xml para la personalización:


  • viv_vector_integer número que se muestra al crear la vista (0 por defecto).
  • color viv_digit_color de números (negro por defecto).

Se pueden cambiar otros parámetros de VectorIntegerView anulando los recursos en la aplicación (como se hace en la aplicación de demostración ):


  • @integer/viv_animation_duration determina la duración de la animación (400 ms por defecto).
  • @dimen/viv_digit_size determina el tamaño de un dígito ( 24dp por defecto).
  • @dimen/viv_digit_translateX aplica a todas las imágenes vectoriales de dígitos para alinearlas horizontalmente.
  • @dimen/viv_digit_translateY se aplica a todas las imágenes vectoriales de dígitos para alinearlas verticalmente.
  • @dimen/viv_digit_strokewidth aplica a todas las imágenes vectoriales de dígitos.
  • @dimen/viv_digit_margin_horizontal aplica a todos los dígitos de la vista ( DigitViewHolder ) ( -3dp por defecto). Esto es necesario para reducir los espacios entre los números, ya que las imágenes vectoriales de los números son cuadrados.

Los recursos anulados se aplicarán a todos los VectorIntegerView en la aplicación.


Todos estos parámetros se establecen a través de recursos, ya que es imposible cambiar el tamaño de VectorDrawable o la duración de la animación AnimatedVectorDrawable través del código.


Agregar VectorIntegerView al marcado XML se ve así:


 <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, puede cambiar el número que se muestra en el código pasando BigInteger :


 final VectorIntegerView vectorIntegerView = findViewById(R.id.vectorIntegerView); vectorIntegerView.setInteger( vectorIntegerView.getInteger().add(BigInteger.ONE), /* animated = */ true ); 

Por conveniencia, hay un método para transmitir un número de tipo long :


 vectorIntegerView.setInteger(1918L, false); 

Si false pasa false como animated , se notifyDataSetChanged método notifyDataSetChanged para el notifyDataSetChanged , y el nuevo número se mostrará sin animaciones.


Cuando recrea VectorIntegerView número que se muestra se guarda utilizando los onRestoreInstanceState onSaveInstanceState y onRestoreInstanceState .


Código fuente


El código fuente está disponible en github (directorio de la biblioteca). También hay una aplicación de demostración que usa VectorIntegerView (directorio de aplicaciones).


También hay una apk de demostración ( minSdkVersion 21 ).

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


All Articles