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:

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), 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
).