0x00 写在前面
在 Android 应用的开发中,因为要用到带有点击动画的自定义 CheckBox,因此记录一下这样的 CheckBox 是如何实现的。
最终实现的效果如下:
0x01 分析 CheckBox 动画的组成部分
要描述一个动画,首先要搞明白这个动画是由哪些部分组成的。我把这个动画拆分成这样几个部分:
CheckBox选中时:
- 外面的圆形边框:先由2缩小成0,然后由0扩大为2;
- 中间的圆形填充:从浅颜色变成深颜色;
- 圆形整体缩放:先由原来的100%大小缩小为原来的90%,然后再重新扩大为100%;
- 白色的对勾:沿着图形的路径描绘一遍。
CheckBox取消选中时:
- 外面的圆形边框:先由2缩小成0,然后由0扩大为2;
- 中间的圆形填充:从深颜色变成浅颜色;
- 圆形整体缩放:先由原来的100%大小缩小为原来的90%,然后再重新扩大为100%;
- 白色的对勾:沿着图形的路径描绘一遍。
使用到的文件以及这些文件各自的作用如下:
- res
- animator
- task_circle_check.xml:圆形填充以及圆形边框从未选中到选中状态的变化
- task_circle_uncheck.xml:圆形填充以及圆形边框从选中到未选中的变化
- task_circle_group_check.xml:圆形整体缩放
- task_tick_check.xml:白色对勾从左到右绘制
- task_tick_uncheck.xml:白色对勾从右到左取消绘制
- drawable
- checkbox_task.xml:可供使用的 Checkbox 效果
- ic_task_button_checked.xml:选中时 Checkbox 的图标
- ic_task_button_normal.xml:未选中时 Checkbox 的图标
- transition_task_checked_unchecked.xml:从选中到未选中过程中图标的每个部分如何变化
- transition_task_unchecked_checked.xml:从未选中到选中过程中图标的每个部分如何变化
- animator
可能这样看觉得文件太多了,而且各个文件的功能很复杂。那么我简单画个图吧:
0x02 准备两个图标:选中和未选中
CheckBox 分为两个状态,一个是选中状态,一个是未选中状态。这两种状态的图标如下图所示。为了实现 CheckBox 功能,就必须准备好这两个图标。
因为是要让图标的不同元素实现不同的动画效果,因此这里最好使用矢量的图标,并通过 Android Studio 将 SVG 或 PSD 格式的图标文件转换为代码形式。尽量不要使用位图。
选中图标
选中图标的代码如下:
ic_task_button_checked.xml:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group android:name="circle_group" android:pivotX="12" android:pivotY="12">
<path
android:name="circle"
android:pathData="M12.0001,12m-9,0a9,9 90,1 1,18 0a9,9 90,1 1,-18 0"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#1E90FF"
android:strokeColor="#1E90FF"/>
</group>
<path
android:name="tick"
android:pathData="M7.0023,13L10.0023,16L17.0024,9"
android:strokeWidth="2"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round"/>
</vector>
上面的这串代码中,注意 android:name
这个属性,这个属性是用来标识元素的,后面设置不同元素的不同动画时就要用到它。而且选中状态下的图标和未选中状态下图标对应元素的 android:name
属性要保持一致。
另外还要注意,在上面的代码中,那个圆圈(也就是 name
为 circle
的那个路径)是被一个 <group>
包裹起来的。这个 <group>
不能去掉,而且系统默认是不会加上这个 <group>
的。原因后面会讲。
未选中图标
上面介绍了选中状态下的图标,下面就是未选中状态下的图标代码。注意在这个图标的代码中,circle_group
和 circle
都还在,但是 tick
已经没有了。因为未选中状态下的图标里没有那个白色的勾勾。
ic_task_button_normal.xml:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group android:name="circle_group" android:pivotX="12" android:pivotY="12">
<path
android:name="circle"
android:pathData="M12.0001,12m-9,0a9,9 90,1 1,18 0a9,9 90,1 1,-18 0"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#E1F0FF"
android:strokeColor="#1E90FF"/>
</group>
</vector>
0x03 描述图标中各部分的变化
光有两个图标显然是远远不够的,系统不可能直接根据两个图标就生成一个我想要的动画。因此还需要将图标的每个元素拆分开,并且分别描述这些元素都是怎么动起来的。
由于 CheckBox 有两个动画,分别是“从未选中到选中”和“从选中到未选中”,因此,下面也分成这两步骤来进行介绍。
从未选中变为选中
前面已经介绍了,从未选中状态变成选中状态,不同元素的动画是不一样的:
- 外面的圆形边框:先由2缩小成0,然后由0扩大为2;
- 中间的圆形填充:从浅颜色变成深颜色;
- 圆形整体缩放:先由原来的100%大小缩小为原来的90%,然后再重新扩大为100%;
- 白色的对勾:沿着图形的路径描绘一遍。
因此也就需要分开进行描述。
所有动画
首先是创建 transition_task_unchecked_checked.xml 这个文件。这跟文件的任务是描述所有元素的动画,或者说,是“从未选中变成选中”这个动画过程中,各个元素对应动画的一个“登记表”。通过这个文件,系统就知道图标里的每个元素的动画都是由哪些文件指定的。
transition_task_unchecked_checked.xml:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_task_button_arranged_checked">
<!--打勾动画,由 task_tick_check.xml 指定-->
<target
android:animation="@animator/task_tick_check"
android:name="tick"/>
<!--圆形变色,由 task_arranged_circle_check.xml 指定-->
<target
android:animation="@animator/task_circle_check"
android:name="circle" />
<!--放大缩小,由 task_circle_group_check.xml 指定-->
<target
android:animation="@animator/task_circle_group_check"
android:name="circle_group" />
</animated-vector>
对勾的动画
对勾的动画是沿着路径从头到尾绘制一遍。
task_tick_check.xml:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="100"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:startOffset="100"
android:propertyName="trimPathEnd"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
这里有几个比较重要的属性。
duration
:动画持续的时长interpolator
:插值器,其实说得通俗点就是这个动画的快慢规律startOffset
:起始时间的便宜,也就是相比于整个动画的开头来说,延迟多久开始当前动画propertyName
:需要变化的属性valueFrom
和valueTo
:从什么变成什么valueType
:值的类型
下面介绍几个比较重要的属性吧。
首先是 duration
和 startOffset
这两个属性,它们的含义如下:
interpolator
这个属性是用来规定动画速度的,也就是“从头到尾一样快”、“先快后慢”、“先慢后快”,还是“先慢后快最后又变慢”。主要有以下这些值可选:
- accelerate_decelerate:先加速后减速
- accelerate:加速
- decelerate:减速
- linear:匀速
- anticipate:先往回走一段,再加速往前
- anticipate_overshoot:先往回走一段,再加速往前,等超过了终点一些,最后回到终点
- overshoot:先加速往前,等超过了终点一些,再回到终点
- bounce:回弹
propertyName
是指一个 View 的什么属性需要变化,valueFrom
表示这个属性从什么值开始变化,valueTo
表示这个属性最后变成什么值。而 valueType
表示这个属性是什么类型的值。有 colorType
、floatType
、pathType
、intType
可选。
在这里是让 trimPathEnd
这个属性从 0 变成 1,其代表的含义就是从头画到尾,看起来就是打了个勾。如果要想它反着来,那就把 valueFrom
改为 1,把 valueTo
改为 0。
圆形变色的动画
根据上面的介绍,这里应该不难理解了。
task_circle_check.xml:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 圆形的描边宽度从 2 变为 0 -->
<objectAnimator
android:duration="100"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:propertyName="strokeWidth"
android:valueFrom="2"
android:valueTo="0"
android:valueType="floatType" />
<!-- 圆形的填充颜色由浅变深 -->
<objectAnimator
android:duration="100"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:propertyName="fillColor"
android:valueFrom="#E1F0FF"
android:valueTo="#1E90FF"
android:valueType="colorType" />
<!-- 圆形的描边宽度从 0 重新变为 2,注意偏移时间不要漏了 -->
<objectAnimator
android:duration="100"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:startOffset="100"
android:propertyName="strokeWidth"
android:valueFrom="0"
android:valueTo="2"
android:valueType="floatType" />
</set>
缩放的动画
task_circle_group_check.xml:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 圆形整体先缩小到原来的 90% -->
<objectAnimator
android:duration="100"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:propertyName="scaleX"
android:valueFrom="1"
android:valueTo="0.9"
android:valueType="floatType" />
<objectAnimator
android:duration="100"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:propertyName="scaleY"
android:valueFrom="1"
android:valueTo="0.9"
android:valueType="floatType" />
<!-- 然后圆形整体复原为原来的大小 -->
<objectAnimator
android:duration="100"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:startOffset="100"
android:propertyName="scaleX"
android:valueFrom="0.9"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="100"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:startOffset="100"
android:propertyName="scaleY"
android:valueFrom="0.9"
android:valueTo="1"
android:valueType="floatType" />
</set>
从选中变为未选中
从选中变为未选中的代码是类似的,有些步骤是反过来的,有些步骤是相同的。这里就不再赘述。
0x04 指明 CheckBox 的状态与动画
现在虽然已经规定了 CheckBox 中各个元素的动画,但是现在系统并不知道 CheckBox 什么时候用哪种动画。因此需要创建一个 checkbox_task.xml 文件,这个文件规定了四样东西:
- CheckBox 平时看起来的样子
- CheckBox 勾选以后看起来的样子
- 从未勾选到勾选,使用哪个动画
- 从勾选到未勾选,使用哪个动画
checkbox_task.xml:
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android"
android:visible="true"
android:dither="true">
<!--勾选-->
<item
android:id="@+id/checked"
android:state_checked="true"
android:drawable="@drawable/ic_task_button_checked" />
<!--未勾选-->
<item
android:id="@+id/unchecked"
android:drawable="@drawable/ic_task_button_normal" />
<!--未勾选过渡到已勾选-->
<transition
android:drawable="@drawable/transition_task_unchecked_checked"
android:fromId="@id/unchecked"
android:toId="@id/checked" />
<!--已勾选过渡到未勾选-->
<transition
android:drawable="@drawable/transition_task_checked_unchecked"
android:fromId="@id/checked"
android:toId="@id/unchecked" />
</animated-selector>
这样一来,只要给所有需要应用该样式和动画的 CheckBox 设置其 android:drawableStart
属性为 @drawable/checkbox_task
即可。
但是由于 CheckBox 的样子长得都差不多,所以可以直接写入 style 文件中,保存为一个主题,这样就免去重复设定的代码了。
themes.xml:
<style name="Theme.DribDaily.TaskButton">
<item name="android:button">@null</item>
<item name="android:drawableStart">@drawable/checkbox_task</item>
<item name="android:padding">8dp</item>
<item name="android:theme">@style/Theme.DribDaily.CheckBoxRippleBlue</item>
</style>
<style name="Theme.DribDaily.CheckBoxRippleBlue" parent="Theme.DribDaily">
<item name="colorAccent">@color/blue</item>
</style>
0x05 测试一下!
最后使用的时候,只要这样写就行了:
<CheckBox
android:layout_width="40dp"
android:layout_height="40dp"
style="@style/Theme.DribDaily.TaskButton" />
最后实现的效果就是这样:
0xFF 写在后面
没啥好说的。就这样吧。