Android 开发小记:自定义点击动画的 CheckBox 实现
本文最后更新于 1120 天前,其中的信息可能已经有所发展或是发生改变。

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:从未选中到选中过程中图标的每个部分如何变化

可能这样看觉得文件太多了,而且各个文件的功能很复杂。那么我简单画个图吧:

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 属性要保持一致。

另外还要注意,在上面的代码中,那个圆圈(也就是 namecircle 的那个路径)是被一个 <group> 包裹起来的。这个 <group> 不能去掉,而且系统默认是不会加上这个 <group> 的。原因后面会讲。

未选中图标

上面介绍了选中状态下的图标,下面就是未选中状态下的图标代码。注意在这个图标的代码中,circle_groupcircle 都还在,但是 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:需要变化的属性
  • valueFromvalueTo:从什么变成什么
  • valueType:值的类型

下面介绍几个比较重要的属性吧。

首先是 durationstartOffset 这两个属性,它们的含义如下:

duration 指这个动画的时长,而 startOffset 指其偏移时长,单位都是毫秒

interpolator 这个属性是用来规定动画速度的,也就是“从头到尾一样快”、“先快后慢”、“先慢后快”,还是“先慢后快最后又变慢”。主要有以下这些值可选:

  • accelerate_decelerate:先加速后减速
  • accelerate:加速
  • decelerate:减速
  • linear:匀速
  • anticipate:先往回走一段,再加速往前
  • anticipate_overshoot:先往回走一段,再加速往前,等超过了终点一些,最后回到终点
  • overshoot:先加速往前,等超过了终点一些,再回到终点
  • bounce:回弹

propertyName 是指一个 View 的什么属性需要变化,valueFrom 表示这个属性从什么值开始变化,valueTo 表示这个属性最后变成什么值。而 valueType 表示这个属性是什么类型的值。有 colorTypefloatTypepathTypeintType 可选。

在这里是让 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" />

最后实现的效果就是这样:

其他几种 CheckBox 就是改一下颜色而已,也很简单的

0xFF 写在后面

没啥好说的。就这样吧。

暂无评论

发送评论 编辑评论

上一篇
下一篇