Android上的动画数字

美观且有吸引力的用户界面非常重要。 因此,对于Android来说,有大量的库可以漂亮地显示设计元素。 通常在应用程序中,您需要显示带有数字或某种计数器的字段。 例如,一个计数器,用于选择列表项的数量或一个月的支出额。 当然,借助常规TextView可以轻松解决此类任务,但是您可以优雅地解决它,并添加更改数字的动画:


演示


演示视频可在YouTube上获得。


本文将讨论如何实现所有这一切。


一个静态数字


对于每个数字,都有一个矢量图像,例如,对于8,它是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> 

除了数字0-9外,还需要负号图像( viv_vd_pathmorph_digits_minus.xml )和空白图像( viv_vd_pathmorph_digits_nth.xml ),它们将在动画过程中象征数字的消失。
XML图像文件仅在android:pathData有所不同。 为了方便起见,通过单独的资源设置了所有其他属性,并且所有矢量图像的属性都相同。
此处拍摄的数字为0-9。


过渡动画


所描述的矢量图像是静态图像。 对于动画,您需要添加动画矢量图像( <animated-vector> )。 例如,要为数字2设置动画,请将文件res/drawable/viv_avd_pathmorph_digits_2_to_5.xml到数字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> 

在这里,为方便起见,我们通过单独的资源设置动画的持续时间。 总共,我们有12个静态图像(0-9 +“减” +“无效”),每个图像都可以在其他任何图像中进行动画处理。 事实证明,为了完整起见,需要12 * 11 = 132个动画文件。 它们只会在android:valueFromandroid:valueTo属性上android:valueFrom ,并且不能手动创建它们。 因此,我们将编写一个简单的生成器:


动画文件生成器
 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()) } } } } 

一起


现在,您需要将静态矢量图像和过渡动画连接到一个<animated-selector>文件中,该文件与常规的<selector> ,根据当前状态显示其中一张图像。 此可绘制资源( res/drawable/viv_asl_pathmorph_digits.xml )包含图像状态的声明以及它们之间的过渡。


使用带有图像和定义此图像的状态属性(在本例中为app:viv_state_three )的<item>标签设置状态。 每个状态都有一个id ,用于确定所需的过渡动画:


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

状态属性在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> 

状态之间的过渡动​​画由<transition>标签设置,并带有<animated-vector> <transition>标记,该符号表示过渡以及初始状态和最终状态的id


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

res/drawable/viv_asl_pathmorph_digits.xml的内容几乎相同,还使用了生成器来创建它。 此可绘制资源包括12个状态以及它们之间的132个转换。


自定义视图


现在我们有了一个drawable ,它允许我们显示一位数字并为其变化添加动画效果,我们需要创建一个VectorIntegerView ,其中将包含多个数字并控制动画。 选择RecyclerView作为基础,因为数字中的位数是一个变量值,而RecyclerView是Android中连续显示可变数量的元素(数字)的最佳方法。 此外, RecyclerView允许您通过ItemAnimator控制项目动画。


DigitAdapter和DigitViewHolder


您需要首先创建一个包含一位数字的DigitViewHolder 。 这样的DigitViewHolder View将由具有android:src="@drawable/viv_asl_pathmorph_digits"的单个ImageView组成android:src="@drawable/viv_asl_pathmorph_digits" 。 为了在ImageView显示所需的数字,使用了mImageView.setImageState(state, true);方法mImageView.setImageState(state, true); 。 使用上面定义的viv_DigitState状态属性,基于显示的数字形成状态state数组。


在“ 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); } 

DigitAdapter适配器负责创建DigitViewHolder并在所需DigitViewHolder显示所需数字。


对于将一个数字转换为另一个数字的正确动画,使用了DiffUtil 。 使用它,可以将数十个级别的动画设置为数十个,几百个到数百个,数千万到数千万的级别,依此类推。 减号始终独立存在,只能出现或消失,变成一个空图像( viv_vd_pathmorph_digits_nth.xml )。


为此, DiffUtil.Callback在比较相同的数字位数时, DiffUtil.Callback方法中的DiffUtil.Callback返回true 。 减号是一个特殊类别,前一个数字的负号等于新数字的负号。


areContentsTheSame方法比较先前数字和新数字中某些位置的字符。 实现本身可以在DigitAdapter看到。


DigitItemAnimator


数字变化的动画,即数字的变换,外观和消失,将由RecyclerView - DigitItemAnimator的特殊动画控制。 为了确定动画的持续时间,使用与上述<animated-vector>中相同的integer资源:


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

DigitItemAnimator的大部分都覆盖了胺化方法。 数字显示的动画( animateAdd方法)是从空白图像到所需数字或减号的过渡。 消失动画( animateRemove方法)是从显示的数字或减号到空白图像的过渡。


为了执行数字转换动画,首先通过覆盖recordPreLayoutInformation方法来存储有关先前显示的数字的信息。 然后,在animateChange方法中,执行从先前显示的数字到新数字的转换。


RecyclerView.ItemAnimator要求覆盖的动画方法必须调用表示动画结束的方法。 因此,在每个animateAddanimateRemoveanimateChange方法中,都有一个对相应方法的调用,其延迟等于动画的持续时间。 例如, animateAdd方法调用延迟等于@integer/viv_animation_durationdispatchAddFinished方法:


 @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


在创建CustomView之前,您需要定义其xml属性。 为此,将<declare-styleable>添加到res/values/attrs.xml


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

创建的VectorIntegerView将具有2个用于自定义的xml属性:


  • viv_vector_integer创建视图时显示viv_vector_integer数字(默认为0)。
  • viv_digit_color数字的颜色(默认为黑色)。

可以通过覆盖应用程序中的资源来更改其他VectorIntegerView参数(就像在demo应用程序中一样 ):


  • @integer/viv_animation_duration确定动画的持续时间(默认为400ms)。
  • @dimen/viv_digit_size确定一位数字的大小(默认为24dp )。
  • @dimen/viv_digit_translateX适用于所有数字矢量图像,以使其水平对齐。
  • @dimen/viv_digit_translateY应用于数字的所有矢量图像,以使其垂直对齐。
  • @dimen/viv_digit_strokewidth适用于所有数字矢量图像。
  • @dimen/viv_digit_margin_horizontal适用于所有视图数字( DigitViewHolder )(默认为-3dp )。 由于数字的矢量图像是正方形的,因此有必要使数字之间的间距更小。

覆盖的资源将应用于应用程序中的所有VectorIntegerView


所有这些参数都是通过资源设置的,因为无法通过代码调整VectorDrawable大小或AnimatedVectorDrawable动画的持续时间。


VectorIntegerView添加到XML标记如下所示:


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

随后,您可以通过传递BigInteger来更改代码中显示的数字:


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

为了方便起见,有一种用于传输long类型的数字的方法:


 vectorIntegerView.setInteger(1918L, false); 

如果将false作为animated传递,则将为notifyDataSetChanged调用notifyDataSetChanged方法,并且新数字将不显示动画。


重新创建VectorIntegerView使用onSaveInstanceStateonRestoreInstanceState保存显示的数字。


源代码


源代码在github (库目录)上可用。 还有一个使用VectorIntegerView (应用程序目录)的演示应用程序。


还有一个演示apkminSdkVersion 21 )。

Source: https://habr.com/ru/post/zh-CN420919/


All Articles