Beautiful and attractive UI is important. Therefore, for Android there are a huge number of libraries for the beautiful display of design elements. Often in the application you want to show a field with a number or a counter. For example, the count of the number of selected items in the list or the amount of expenses for the month. Of course, such a task is easily solved with the help of an ordinary TextView
, but you can solve it elegantly and add an animation of changing the number:
Demo video is available on YouTube.
The article will tell a story about how to implement all this.
For each of the digits there is a vector image, for example, for 8 this is 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>
In addition to the numbers 0-9, images of the minus sign ( viv_vd_pathmorph_digits_minus.xml
) and a blank image ( viv_vd_pathmorph_digits_nth.xml
) are also required, which will symbolize the disappearing digit of the number during the animation.
XML image files differ only in the android:pathData
. For the convenience, all other attributes are set through separate resources and the same for all vector images.
Images for numbers 0-9 were taken here .
The described vector images are static images. For animation, you need to add animated vector images ( <animated-vector>
). For example, to animate the number 2 in the number 5, add the file res/drawable/viv_avd_pathmorph_digits_2_to_5.xml
:
<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>
Here, for convenience, we set the duration of the animation through a separate resource. In total, we have 12 static images (0 - 9 + "minus" + "emptiness"), each of them can be animated in any of the others. So, for completeness, 12 * 11 = 132 animation files are required. They will android:valueFrom
only in the attributes android:valueFrom
and android:valueTo
, and creating them manually is not an option. Therefore, we write a simple generator:
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()) } } } }
Now we need to link static vector images and transition animations in one single <animated-selector>
file, which, like the regular <selector>
, displays one of the images depending on the current state. This drawable resource ( res/drawable/viv_asl_pathmorph_digits.xml
) contains declarations of image states and transitions between them.
States are defined by <item>
tags with an image and a state attribute (in this case, app:viv_state_three
) app:viv_state_three
given image. Each state has an id
, which is used to determine the required transition animation:
<item android:id="@+id/three" android:drawable="@drawable/viv_vd_pathmorph_digits_three" app:viv_state_three="true" />
State attributes are defined in the 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>
State transition animations are defined by <transition>
tags with an indication of the <animated-vector>
, symbolizing the transition, as well as the id
initial and final state:
<transition android:drawable="@drawable/viv_avd_pathmorph_digits_6_to_2" android:fromId="@id/six" android:toId="@id/two" />
The contents of res/drawable/viv_asl_pathmorph_digits.xml
pretty res/drawable/viv_asl_pathmorph_digits.xml
the same type, and a generator was also used to create it. This drawable resource consists of 12 states and 132 transitions between them.
Now that we have a drawable
that allows us to display a single digit and animate its change, we need to create a VectorIntegerView
, which will contain a number consisting of several digits, and control the animations. RecyclerView
was chosen as the basis, since the number of digits in a number is variable, and RecyclerView
is the best way for Android to display a variable number of items (digits) in a row. In addition, RecyclerView
allows you to control the animations of items through ItemAnimator
.
You need to start by creating a single digit DigitViewHolder
. View
such DigitViewHolder
will consist of one ImageView
, in which android:src="@drawable/viv_asl_pathmorph_digits"
. To display the desired digit in the ImageView
use the mImageView.setImageState(state, true);
method mImageView.setImageState(state, true);
. The state state
array is formed from the displayed digit using the viv_DigitState
state attributes defined above.
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); }
The DigitAdapter
adapter DigitAdapter
responsible for creating the DigitViewHolder
and for displaying the desired digit in the desired DigitViewHolder
.
DiffUtil
used to correctly animate the transformation of one number into another. With its help, the tens rank is animated into the tens rank, hundreds - into hundreds, tens of millions - into tens of millions, and so on. The minus symbol always remains by itself and can only appear or disappear, turning into a blank image ( viv_vd_pathmorph_digits_nth.xml
).
To do this, in the DiffUtil.Callback
method, areItemsTheSame
returns true
only if the same digits of numbers are compared. "Minus" is a special digit, and "minus" from the previous number is equal to "minus" from the new number.
The areContentsTheSame
method compares characters at certain positions in the previous and new numbers. The implementation itself can be seen in the DigitAdapter
.
The animation of the change in number, namely, the transformation, appearance and disappearance of numbers, will be monitored by a special animator for RecyclerView
- DigitItemAnimator
. To determine the duration of animations, the same integer
resource is used as in the <animated-vector>
described above:
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; }
The main part of DigitItemAnimator
is the redefinition of amination methods. Animation of the appearance of a digit (the animateAdd
method) is performed as a transition from an empty image to the desired digit or a minus sign. animateRemove
animation (the animateRemove
method) is performed as a transition from a displayed digit or a minus sign to an empty image.
To animate a digit change, the information about the previously displayed digit is first saved by overriding the recordPreLayoutInformation
method. Then, in the animateChange
method, a transition is made from the previous displayed digit to a new one.
RecyclerView.ItemAnimator
requires that when redefining animation methods, methods that symbolize the end of the animation must be called. Therefore, in each of the methods animateAdd
, animateRemove
and animateChange
there is a call to the corresponding method with a delay equal to the duration of the animation. For example, in the animateAdd
method, the dispatchAddFinished
method is called with a delay of @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; }
Before creating a CustomView, you need to define its xml attributes. To do this, add <declare-styleable>
to res/values/attrs.xml
:
<declare-styleable name="VectorIntegerView"> <attr name="viv_vector_integer" format="integer" /> <attr name="viv_digit_color" format="color" /> </declare-styleable>
Created by VectorIntegerView
will have 2 xml-attributes for customization:
viv_vector_integer
number displayed when creating a view (0 by default).viv_digit_color
color numbers (black by default).Other VectorIntegerView
parameters can be changed by overriding resources in the application (as done in the demo application ):
@integer/viv_animation_duration
determines the duration of the animation (400ms by default).@dimen/viv_digit_size
determines the size of a single digit ( 24dp
by default).@dimen/viv_digit_translateX
applies to all vector digit images to align them horizontally.@dimen/viv_digit_translateY
applies to all vectorial images of numbers to align them vertically.@dimen/viv_digit_strokewidth
applies to all vector digit images.@dimen/viv_digit_margin_horizontal
applies to all view digits ( DigitViewHolder
) ( -3dp
by default). This is needed to make the spaces between the digits smaller, since the vector images of the digits are square.Overridden resources will be applied to all VectorIntegerView
in the application.
All these parameters are set through the resources, since the VectorDrawable
size or animation duration AnimatedVectorDrawable
through the code is impossible to change.
Adding a VectorIntegerView
to the XML markup looks like this:
<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>
You can subsequently change the displayed number in the code by passing to BigInteger
:
final VectorIntegerView vectorIntegerView = findViewById(R.id.vectorIntegerView); vectorIntegerView.setInteger( vectorIntegerView.getInteger().add(BigInteger.ONE), /* animated = */ true );
For the sake of convenience, there is a method for passing a long
type number:
vectorIntegerView.setInteger(1918L, false);
If false
passed as animated
, the notifyDataSetChanged
method will be called for the notifyDataSetChanged
, and the new number will be displayed without animations.
When recreating a VectorIntegerView
displayed number is saved using the onSaveInstanceState
and onRestoreInstanceState
.
Source code is available on github (library directory). There is also a demo application using VectorIntegerView
(app directory).
There is also a demo apk ( minSdkVersion 21
).
Source: https://habr.com/ru/post/420919/
All Articles