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 problem is easily solved with the help of the usual one TextView, but you can solve it elegantly and add another animation of the number change:


    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 numbers there is a vector image, for example, for 8 it is res/drawable/viv_vd_pathmorph_digits_eight.xml:


    <vectorxmlns: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"><groupandroid:translateX="@dimen/viv_digit_translateX"android:translateY="@dimen/viv_digit_translateY"><pathandroid: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 an empty 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 attribute 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 must 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-vectorxmlns:android="http://schemas.android.com/apk/res/android"xmlns:aapt="http://schemas.android.com/aapt"android:drawable="@drawable/viv_vd_pathmorph_digits_zero"><targetandroid:name="iconPath"><aapt:attrname="android:animation"><objectAnimatorandroid: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. It turns out that for completeness, 12 * 11 = 132 animation files are required. They will differ only in the attributes android:valueFromand android:valueTo, and create 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) {
            in0..9 -> i.toString()
            10 -> "n"
            11 -> "m"
            else -> null!!
        }
        val dirName = "viv_out"
        File(dirName).mkdir()
        for (fromin0..11) {
            for (toin0..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 you need to link static vector images and transition animations in one file <animated-selector>, which, like a regular one <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 set by tags <item>with an image and an attribute of the state (in this case - app:viv_state_three) defining this image. Each state has id, which is used to determine the required transition animation:


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

    State attributes are specified in the file res/values/attrs.xml:


    <resources><declare-styleablename="viv_DigitState"><attrname="viv_state_zero"format="boolean" /><attrname="viv_state_one"format="boolean" /><attrname="viv_state_two"format="boolean" /><attrname="viv_state_three"format="boolean" /><attrname="viv_state_four"format="boolean" /><attrname="viv_state_five"format="boolean" /><attrname="viv_state_six"format="boolean" /><attrname="viv_state_seven"format="boolean" /><attrname="viv_state_eight"format="boolean" /><attrname="viv_state_nine"format="boolean" /><attrname="viv_state_nth"format="boolean" /><attrname="viv_state_minus"format="boolean" /></declare-styleable></resources>

    Animations of transitions between states are defined by tags <transition>with an indication <animated-vector>that symbolizes the transition, as well as the idinitial and final states:


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

    The content is res/drawable/viv_asl_pathmorph_digits.xmlpretty much the same, 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 one drawablethat allows us to display one digit and animate its change, we need to create VectorIntegerViewone that will contain a number consisting of several digits and control the animations. As the basis was chosen RecyclerView, since the number of digits in a number is a variable value, and RecyclerViewthis is the best way for Android to display a variable number of elements (digits) in a row. In addition, RecyclerViewallows you to manage the animations of items through ItemAnimator.


    DigitAdapter and DigitViewHolder


    You need to start by creating a DigitViewHoldersingle digit. Viewsuch DigitViewHolderwill consist of one ImageViewin which android:src="@drawable/viv_asl_pathmorph_digits". To display the desired digit in the ImageViewused method mImageView.setImageState(state, true);. The state array stateis formed on the basis of the displayed digit using the state attributes viv_DigitStatedefined above.


    Display the desired number in `ImageView`
    privatestaticfinalint[] 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,
    };
    voidsetDigit(@IntRange(from = 0, to = VectorIntegerView.MAX_DIGIT)int digit) {
        int[] state = newint[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 adapter DigitAdapteris responsible for creating DigitViewHolderand displaying the desired number in the right one DigitViewHolder.


    For correct animation of turning one number into another is used DiffUtil. 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 an empty image ( viv_vd_pathmorph_digits_nth.xml).


    For this, DiffUtil.Callbackthe method areItemsTheSamereturns in the method trueonly 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 method areContentsTheSamecompares characters in certain positions in the previous and new numbers. The implementation itself can be seen in DigitAdapter.


    DigitItemAnimator


    The animation of the change in number, namely, the transformation, appearance and disappearance of numbers, will be controlled by a special animator for RecyclerView- DigitItemAnimator. To determine the duration of animations, the same integerresource is used as in <animated-vector>, described above:


    privatefinalint animationDuration;
    DigitItemAnimator(@NonNull Resources resources) {
        animationDuration = resources.getInteger(R.integer.viv_animation_duration);
    }
    @OverridepubliclonggetMoveDuration(){ return animationDuration; }
    @OverridepubliclonggetAddDuration(){ return animationDuration; }
    @OverridepubliclonggetRemoveDuration(){ return animationDuration; }
    @OverridepubliclonggetChangeDuration(){ return animationDuration; }

    The main part DigitItemAnimatoris the redefinition of amination methods. Animation of the appearance of a digit (method animateAdd) is performed as a transition from an empty image to the desired digit or the minus sign. The fade animation (method animateRemove) is performed as a transition from a displayed digit or a minus sign to an empty image.


    To perform a digit change animation, first save information about the previously displayed digit using a method override recordPreLayoutInformation. After that, the method animateChangeperforms a transition from the previous displayed digit to a new one.


    RecyclerView.ItemAnimatorrequires that when overriding animation methods, methods that symbolize the end of the animation must be called. Therefore, in each of the methods animateAdd, animateRemoveand animateChangethere is a call to the corresponding method with a delay equal to the duration of the animation. For example, a method animateAddis called in a method dispatchAddFinishedwith a delay equal to @integer/viv_animation_duration:


    @OverridepublicbooleananimateAdd(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() {
            @Overridepublicvoidrun(){
                dispatchAddFinished(holder);
            }
        }, animationDuration);
        returnfalse;
    }

    VectorIntegerView


    Before creating a CustomView, you need to define its xml attributes. To do this, add <declare-styleable>to the file res/values/attrs.xml:


    <declare-styleablename="VectorIntegerView"><attrname="viv_vector_integer"format="integer" /><attrname="viv_digit_color"format="color" /></declare-styleable>

    The created one VectorIntegerViewwill have 2 xml-attributes for customization:


    • viv_vector_integer the number displayed when creating the view (0 by default).
    • viv_digit_color color numbers (black by default).

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


    • @integer/viv_animation_duration determines the duration of the animation (400ms by default).
    • @dimen/viv_digit_sizedetermines the size of a single digit ( 24dpdefault).
    • @dimen/viv_digit_translateX applies to all vector digit images to align them horizontally.
    • @dimen/viv_digit_translateY applies to all vector digit images to align them vertically.
    • @dimen/viv_digit_strokewidth applies to all vector digit images.
    • @dimen/viv_digit_margin_horizontalapplies to all view digits ( DigitViewHolder) ( -3dpdefault). This is necessary to make the spaces between the digits smaller, since the vector images of the digits are square.

    Redefined resources will be applied to everyone VectorIntegerViewin the application.


    All these parameters are set through the resources, since the size change VectorDrawableor duration of the animation AnimatedVectorDrawablethrough the code is impossible.


    Adding VectorIntegerViewto the XML markup looks like this:


    <FrameLayoutxmlns: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.VectorIntegerViewandroid: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>

    Subsequently, you can change the displayed number in the code by passing 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 transmitting a number like long:


    vectorIntegerView.setInteger(1918L, false);

    If animatedsent as false, the method will be called for the adapter notifyDataSetChanged, and the new number will be displayed without animations.


    When recreating, the VectorIntegerViewdisplayed number is saved using the onSaveInstanceStateand methods 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).


    Also popular now: