📜 ⬆️ ⬇️

Animated numbers on Android

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


Demo video is available on YouTube.


The article will tell a story about how to implement all this.


One static digit


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 .


Transition animation


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:


Animation File 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()) } } } } 

Together


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.


Customview


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 .


DigitAdapter and DigitViewHolder


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.


Display the correct number in `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); } 

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 .


DigitItemAnimator


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

VectorIntegerView


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:



Other VectorIntegerView parameters can be changed by overriding resources in the application (as done in the demo application ):



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 .


Sources


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