美观且有吸引力的用户界面非常重要。 因此,对于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:valueFrom
和android: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
要求覆盖的动画方法必须调用表示动画结束的方法。 因此,在每个animateAdd
, animateRemove
和animateChange
方法中,都有一个对相应方法的调用,其延迟等于动画的持续时间。 例如, animateAdd
方法调用延迟等于@integer/viv_animation_duration
的dispatchAddFinished
方法:
@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), true );
为了方便起见,有一种用于传输long
类型的数字的方法:
vectorIntegerView.setInteger(1918L, false);
如果将false
作为animated
传递,则将为notifyDataSetChanged
调用notifyDataSetChanged
方法,并且新数字将不显示动画。
重新创建VectorIntegerView
使用onSaveInstanceState
和onRestoreInstanceState
保存显示的数字。
源代码
源代码在github (库目录)上可用。 还有一个使用VectorIntegerView
(应用程序目录)的演示应用程序。
还有一个演示apk ( minSdkVersion 21
)。