动画那些事

[TOC]

动画基础

在日常开发中,我们往往不是仅仅局限于实现产品相应的功能就可以了,为了达到更好的用户体验我们还会加入一些比较好看的动画效果。今天我就来总结一下动画相关的一些知识。

Android系统提供了很多丰富的API去实现UI的动画,最主要的划分为如下几类:

  • Property Animation:即属性动画,他是从Android 3.0(API level 11)以后开始加入的,这种动画不局限于一般的View,可以给任何Object添加,包括没有在屏幕上渲染的对象。而且属性动画非常灵活,提供了简单的扩展方法,可以定义任何类型的动画,适配比较复杂的动画场景。
  • Drawable Animation:即帧动画,这种动画相对来说比较简单,他的原来跟早期动画片差不多,将一组连续的动作拆分成一系列的序列帧,然后快速播放这组序列帧,利用视觉暂留产生动画效果。每一个drawable就相当于一个帧。
  • View Animation:视图动画是针对View的动画,通过设置,可以再一定时间范围内改变View的位置,透明度,大小以及旋转角度从而实现动画效果。

每一种动画的实现方法与实现的复杂度都是不尽相同的,针对的应用场景也是不是一样的,针对每一种动画我们慢慢展开来讲一下。

另外,Airbnb发布了一个Lottie动画库,实现原理与上面集中都是不同,他可以直接将AE里面的视频转化成动效,并且实现很高效,性能更加优良,让我们几行代码实现复杂绚丽的动画,这个具体后面也会讲到。

最后结合这些,看一下项目中的实际应用效果。

Drawable Animation

帧动画允许你实现像播放幻灯片一样的效果,这种动画的实质其实就是一组连续的drawable,这种动画的XML定义文件一般放在res/drawable目录下面。具体的XML使用方法可以看下官方文档.

使用说明

我们可以使用Java代码或者XML来定义一组序列帧,不过Android官方还是推荐我们使用XML的方式来定义。规则如下:

  • 为根节点,包含一个或者多个item,有如下属性
    1. android:oneshot,true代表只执行一次,false代表循环执行
    2. item,表示动画的第一帧
  • 子节点为,包含如下属性:
    1. android:drawable 动画中的某一帧
    2. android:duration 该帧显示的时间
实例

在drawable下面定义如下xml文件:

1
2
3
4
5
6
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item android:drawable="@drawable/rocket_thrust1" android:duration="200" />
<item android:drawable="@drawable/rocket_thrust2" android:duration="200" />
<item android:drawable="@drawable/rocket_thrust3" android:duration="200" />
</animation-list>

然后在代码中开启动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AnimationDrawable rocketAnimation;

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);
rocketImage.setBackgroundResource(R.drawable.rocket_thrust);
rocketAnimation = (AnimationDrawable) rocketImage.getBackground();
}

public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
rocketAnimation.start();
return true;
}
return super.onTouchEvent(event);
}

以上就是帧动画的使用方法,比较简单。

View Animation

视图动画,也叫补间动画,可以对一个View执行一系列的简单变换,比如大小,位置,旋转角度和透明度等等。同样的,补间动画也可以通过Java代码和XML文件两种方式来定义。和补间动画相关的class主要有:

  • Animation:指一个补间动画,是抽象类
  • AnimationSet:指一组补间动画,可以包含多个Animation,并且可以同时执行几个动画,比如在修改透明度的动画下一起修改位置
  • AlphaAnimation:透明度动画
  • RotateAnimation:旋转动画
  • ScaleAnimation:缩放动画
  • TranslateAnimation:位移动画
Animation类

Animation是补间动画所有类的基类,它提供了一些通用的动画属性方法。

xml属性 Java方法 说明
android:detachWallpaper setDetachWallpaper(boolean) 是否在壁纸上运行
android:duration setDuration(long) 动画持续时间,毫秒为单位
android:fillAfter setFillAfter(boolean) 控件动画结束时是否保持动画最后的状态
android:fillBefore setFillBefore(boolean) 控件动画结束时是否还原到开始动画前的状态
android:fillEnabled setFillEnabled(boolean) 与android:fillBefore效果相同
android:interpolator setInterpolator(Interpolator) 设定插值器(指定的动画效果,譬如回弹等)
android:repeatCount setRepeatCount(int) 重复次数
android:repeatMode setRepeatMode(int) 重复类型有两个值,reverse表示倒序回放,restart表示从头播放
android:startOffset setStartOffset(long) 调用start函数之后等待开始运行的时间,单位为毫秒
android:zAdjustment setZAdjustment(int) 表示被设置动画的内容运行时在Z轴上的位置(top/bottom/normal),默认为normal

上面这些属性还是比较好理解的,唯独这个插值器可能会比较难以理解,下面我们就来了解一下什么是插值器。

定义:插值器是用来定义动画的变化速率的,他可以让动画的变化(透明度变化,位置变化,旋转角度变化,大小变化)是加速,减速或者是匀速。

系统为我们提供了很多个插值器:

java类 xml id值 描述
AccelerateDecelerateInterpolator @android:anim/accelerate_decelerate_interpolator 动画始末速率较慢,中间加速
AccelerateInterpolator @android:anim/accelerate_interpolator 动画开始速率较慢,之后慢慢加速
DecelerateInterpolator @android:anim/decelerate_interpolator 动画开始快然后慢
LinearInterpolator @android:anim/linear_interpolator 动画匀速改变
OvershootInterpolator @android:anim/overshoot_interpolator 向前弹出一定值之后回到原来位置

如果系统提供的插值器不能满足你的需要的话,你还可以自己自定义:

1
2
3
4
5
6
7
8
public class AccelerateDecelerateInterpolator extends BaseInterpolator
implements NativeInterpolatorFactory {
......
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}
......
}
AlphaAnimation类
java类 xml id值 描述
android:fromAlpha AlphaAnimation(float fromAlpha, …) 动画开始的透明度(0.0到1.0,0.0是全透明,1.0是不透明)
android:toAlpha AlphaAnimation(…, float toAlpha) 动画结束的透明度,同上

在Animation的基础上,AlphaAnimation只多了这两个属性,用来控制开始与结束的透明度。

RotateAnimation类
java类 xml id值 描述
android:fromDegrees RotateAnimation(float fromDegrees, …) 旋转开始角度,正代表顺时针度数,负代表逆时针度数
android:toDegrees RotateAnimation(…, float toDegrees, …) 旋转结束角度,正代表顺时针度数,负代表逆时针度数
android:pivotX RotateAnimation(…, float pivotX, …) 缩放起点X坐标(数值、百分数、百分数p,譬如50表示以当前View左上角坐标加50px为初始点、50%表示以当前View的左上角加上当前View宽高的50%做为初始点、50%p表示以当前View的左上角加上父控件宽高的50%做为初始点)
android:pivotY RotateAnimation(…, float pivotY) 缩放起点Y坐标,同上规律
ScaleAnimation类
java类 xml id值 描述
android:fromXScale ScaleAnimation(float fromX, …) 初始X轴缩放比例,1.0表示无变化
android:toXScale ScaleAnimation(…, float toX, …) 结束X轴缩放比例
android:fromYScale ScaleAnimation(…, float fromY, …) 初始Y轴缩放比例
android:toYScale ScaleAnimation(…, float toY, …) 结束Y轴缩放比例
android:pivotX ScaleAnimation(…, float pivotX, …) 缩放起点X轴坐标(数值、百分数、百分数p,譬如50表示以当前View左上角坐标加50px为初始点、50%表示以当前View的左上角加上当前View宽高的50%做为初始点、50%p表示以当前View的左上角加上父控件宽高的50%做为初始点)
android:pivotY ScaleAnimation(…, float pivotY) 缩放起点Y轴坐标,同上规律
TranslateAnimation类
java类 xml id值 描述
android:fromXDelta TranslateAnimation(float fromXDelta, …) 起始点X轴坐标(数值、百分数、百分数p,譬如50表示以当前View左上角坐标加50px为初始点、50%表示以当前View的左上角加上当前View宽高的50%做为初始点、50%p表示以当前View的左上角加上父控件宽高的50%做为初始点)
android:fromYDelta TranslateAnimation(…, float fromYDelta, …) 起始点Y轴从标,同上规律
android:toXDelta TranslateAnimation(…, float toXDelta, …) 结束点X轴坐标,同上规律
android:toYDelta TranslateAnimation(…, float toYDelta) 结束点Y轴坐标,同上规律
AnimationSet类

AnimationSet继承自Animation,是上面四种的组合容器管理类,他可以同时启动多个动画,一般常用的API有;

  • void addAnimation(Animaiton a):添加一个Animation
  • List getAnimations():获取AnimationSet里的所有Animation
  • void setDutation(long durationMillis):动画时长

总的来说,AnimationSet只是对Animation的一种组合而已。

通过xml也可以直接定义一个AnimationSet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@[package:]anim/interpolator_resource"
android:shareInterpolator=["true" | "false"] >
<alpha
android:fromAlpha="float"
android:toAlpha="float" />
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float" />
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float" />
<rotate
android:fromDegrees="float"
android:toDegrees="float"
android:pivotX="float"
android:pivotY="float" />
<set>
...
</set>
</set>

然后在代码中加载这个动画:

1
2
3
ImageView spaceshipImage = (ImageView) findViewById(R.id.spaceshipImage);
Animation animationDemo = AnimationUtils.loadAnimation(this, R.anim.hyperspace_jump);
spaceshipImage.startAnimation(animationDemo);

Property Animation

自Android 3.0版本开始,系统给我们提供了一种全新的动画模式,属性动画(property animation),它的功能非常强大,弥补了之前补间动画的一些缺陷,几乎是可以完全替代掉补间动画了。

为什么需要Property Animation
  1. 补间动画相对来说局限性比较大,只能实现位移,缩放,旋转和透明度这些变化,想要其他效果就要自己动手实现了。
  2. 补间动画只能对一个View来操作才可以
  3. 补间动画仅仅是改变了View的显示效果,并没有真正的改变View的属性。比较我们将一个Button通过TranslateAnimation将他从位置1移动到了位置2,并且将其停留在了位置2,这时候你去点击位置2是没有点击效果的,而点击位置你会发现却触发了button的点击事情,这就是补间动画的最大问题。我们常常还要通过其他的方式来规避这种问题。

也正是因为这些原因,Android开发团队决定在3.0版本当中引入属性动画这个功能,属性动画把上述的问题全部解决掉了,下面我们就来一起看一看。

新引入的属性动画机制已经不再是针对于View来设计的了,也不限定于只能实现移动、缩放、旋转和淡入淡出这几种动画操作,同时也不再只是一种视觉上的动画效果了。它实际上是一种不断地对值进行操作的机制,并将值赋值到指定对象的指定属性上,可以是任意对象的任意属性。我们只需要告诉系统动画的运行时长,需要执行哪种类型的动画,以及动画的初始值和结束值,剩下的工作就可以全部交给系统去完成了。

既然属性动画的实现机制是通过对目标对象进行赋值并修改其属性来实现的,那么之前所说的按钮显示的问题也就不复存在了,如果我们通过属性动画来移动一个按钮,那么这个按钮就是真正的移动了,而不再是仅仅在另外一个位置绘制了而已。也就不需要考虑动画带来的View的交互问题了。

ValueAnimator

ValueAnimator是整个属性动画机制当中最核心的一个类,前面我们已经提到了,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。

本质来说,ValueAnimation只是对一个初始值到终点値之间输出一系列的中间值,然后赋值对应的属性,从而实现动画效果。看一个例子:

1
2
3
4
5
6
7
8
9
10
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);  
anim.setDuration(300);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentValue = (float) animation.getAnimatedValue();
Log.d("TAG", "cuurent value is " + currentValue);
}
});
anim.start();

运行后会产生一系列的值:

运行结果

另外,ofFloat()方法是一个变参函数,可以输入任意多的参数,那么他就会从参数1变道参数2,然后变道参数3…

类似的,ValueAnimator还有ofIntofObject两个方法。

此外,还可以

  • 通过setStartDelay()方法来设置动画延迟播放的时间。
  • 通过setRepeatCount()方法来设置动画的循环播放的次数
  • 通过setRepeatMode()方法来设置循环模式,RESTART表示从头开始,REVERSE表示倒序循环播放。
ObjectAnimator

ObjectAnimator是ValueAnimator的子类,相比起ValueAnimation,它是可以直接对任意对象的任意属性进行动画操作的,比如说View的alpha属性。我们看一个透明度的小实例:

1
2
3
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);  
animator.setDuration(5000);
animator.start();

可以看到,我们还是调用了ofFloat()方法来去创建一个ObjectAnimator的实例,只不过ofFloat()方法当中接收的参数有点变化了。这里第一个参数要求传入一个object对象,我们想要对哪个对象进行动画操作就传入什么,这里我传入了一个textview。第二个参数是想要对该对象的哪个属性进行动画操作,由于我们想要改变TextView的不透明度,因此这里传入”alpha”。后面的参数就是不固定长度了,想要完成什么样的动画就传入什么值,这里传入的值就表示将TextView从常规变换成全透明,再从全透明变换成常规。之后调用setDuration()方法来设置动画的时长,然后调用start()方法启动动画,效果如下图所示:

如果要将一个TextView旋转,可以这样:

1
2
3
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);  
animator.setDuration(5000);
animator.start();

再比如模拟实现一个跑马灯效果:

1
2
3
4
float curTranslationX = textview.getTranslationX();  
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "translationX", curTranslationX, 1500f);
animator.setDuration(5000);
animator.start();

其实ObjectAnimation的工作原理很简单,我们拿透明度变化的那个动画来看:

1
2
3
4
5
6
7
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);  
animator.setDuration(5000);
animator.start();

// ObjectAnimator会不断的去修改`textview`的alpha的值,并且在修改后将View标记为invalidate,
// 等到屏幕下次再次绘制View的时候将会根据新的alpha值去绘制View
// 这样View的透明度就会不断的变化了,也就实现了动画效果。
AnimatorSet

与AnimationSet相对应,属性动画也提供了一个组合动画管理类。独立的动画能够实现的视觉效果毕竟是相当有限的,因此将多个动画组合到一起播放就显得尤为重要。AnimatorSet提供了一套非常丰富的API来让我们将多个动画组合到一起。

实现组合动画功能主要需要借助AnimatorSet这个类,这个类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象(ValueAnimator或ObjectAnimator)将会返回一个AnimatorSet.Builder的实例,AnimatorSet.Builder中包括以下四个方法:

  • after(Animator anim) 将现有动画插入到传入的动画之后执行
  • after(long delay) 将现有动画延迟指定毫秒后执行
  • before(Animator anim) 将现有动画插入到传入的动画之前执行
  • with(Animator anim) 将现有动画和传入的动画同时执行

比如我们可以将我们上面的例子组合一下:

1
2
3
4
5
6
7
ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, "translationX", -500f, 0f);  
ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).after(moveIn);
animSet.setDuration(5000);
animSet.start();
AnimatorListener

在很多时候,我们希望可以监听到动画的各种事件,比如动画何时开始,何时结束,然后在开始或者结束的时候去执行一些逻辑处理。为了方便开发,Google为我们提供了AnimatorListener来监听动画的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
anim.addListener(new AnimatorListener() {  
@Override
public void onAnimationStart(Animator animation) {
// 动画开始执行
// do something
}

@Override
public void onAnimationRepeat(Animator animation) {
// 动画重复的时候
// do something
}

@Override
public void onAnimationEnd(Animator animation) {
// 动画结束
// do something
}

@Override
public void onAnimationCancel(Animator animation) {
// 动画取消
// do something
}
});

以上就是Animator的简单使用,一般来说已经可以满足我们绝大多数的动画需求了。但是对于一个复杂绚丽变化不是很规律的动画实现起来可能会比较复杂,Airbnb公司开源了一个Lottie工具很好地解决的这个问题,我们继续往下看。

Lottie动画库

简介

Lottie is a mobile library for Android and iOS that parses Adobe After Effects animations exported as json with Bodymovin and renders them natively on mobile!
For the first time, designers can create and ship beautiful animations without an engineer painstakingly recreating it by hand.

大体意思就是,通过Lottie,可以将Bodymovin这个AE插件导出的动画Json文件在Android和ios上面解析并渲染在屏幕上面。Lottie第一次实现了视觉设计师设计动画后就可以直接使用了而不需要开发人员再次开发。

对于开发者来说,这真是一个天大的喜讯啊,再也不需要也视觉争论实现难度(你现在没有机会争论了),讲什么开发成本了。直接将Json丢给你,然后就可以在设备上面实现动画效果了,是不是很酷!那就让我们来看一下怎么用吧~

使用方法

  1. 首先第一个就是要为我们的项目添加依赖:

Gradle is the only supported build configuration, so just add the dependency to your project build.gradle file:

目前官方仅支持gradle方式。

1
2
3
4
5
dependencies {
...
compile 'com.airbnb.android:lottie:2.2.5'
...
}
  1. 在Layout里面添加LottieAnimationView
1
2
3
4
5
6
7
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="hello-world.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />

注意:Lottie现在支持的最低版本是level 14,LottieAnimationView继承自ImageView

  • lottie_fileName:是指需要加载的json动画文件
  • app:lottie_loop=”true”:动画是否需要循环播放
  • app:lottie_autoPlay=”true”:动画是否需要自动播放
  1. 在Java代码中执行动画
1
2
3
4
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);
animationView.playAnimation();

这个方法会在后台加载并解析动画,解析完成后将会开始并渲染动画。这样的AE里面的动画就会很神奇的运行在你的设备上了。

Lottie还提供了一些其他的东西:

如果想要加载一个网络Json资源可以这样:

1
2
3
4
5
6
7
8
9
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
...
Cancellable compositionCancellable = LottieComposition.Factory.fromJson(getResources(), jsonObject, (composition) -> {
animationView.setComposition(composition);
animationView.playAnimation();
});

// Cancel to stop asynchronous loading of composition
// compositionCancellable.cancel();

还可以通过下面的方法控制监听动画的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
animationView.addAnimatorUpdateListener((animation) -> {
// Do something.
});
animationView.playAnimation();
...
if (animationView.isAnimating()) {
// Do something.
}
...
animationView.setProgress(0.5f);
...
// Custom animation speed or duration.
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)
.setDuration(500);
animator.addUpdateListener(animation -> {
animationView.setProgress(animation.getAnimatedValue());
});
animator.start();
...
animationView.cancelAnimation();

项目实际效果实践

近期项目中有几个动画效果要实现,结合上面所讲的内容,一起来看一个例子:

思路分析

首先点赞手势的那个粒子效果是通过Lottie来实现的,所以我们使用上面讲到的Lottie去加载json文件就好了。至于下面的按钮的波纹效果就需要我们自己去实现了。所以可以将这个动画简单的拆分成两部分,一个是点赞手势的粒子效果,一个是按钮的博文效果。

效果实现

  1. 由于粒子效果的扩散,所以这个点赞动画的View有200px*200px这么大,比这个Button的高度还要大。所以我们首先要解决这个button和动画的显示问题。重叠显示两个View只能使用FrameLayout来实现了。按照View的绘制规则,只要将动画视图放在按钮的下面,那么动画就会悬浮显示在button上面了。根据这个思路,我们可以将布局修改成这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<FrameLayout
android:layout_gravity="center_horizontal"
android:layout_width="180dp"
android:layout_height="100dp">

<com.netease.snailread.view.WaterRippleButton
android:id="@+id/bt_like"
android:gravity="center_vertical|right"
android:textSize="16sp"
android:text="@string/activity_bookreview_detail_like"
android:textColor="@color/color_ffffff"
android:layout_gravity="center"
android:background="@drawable/selector_like_btn_bg"
android:layout_width="180dp"
android:layout_height="55dp" />

<ImageView
android:id="@+id/iv_good"
android:layout_marginLeft="39dp"
android:layout_gravity="center_vertical"
android:src="@drawable/like_ic_big_outline"
android:elevation="10dp"
android:translationZ="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<com.airbnb.lottie.LottieAnimationView
android:id="@+id/iv_like"
android:elevation="12dp"
android:translationZ="12dp"
android:layout_width="100dp"
android:layout_height="100dp" />

</FrameLayout>

在4.4的模拟器上运行了一下是没有问题的,但是当我在6.0的模拟器上运行时却出现了奇怪的一幕:Button居然是遮盖住动画的!这就很奇怪了,难道6.0修改了View的绘制顺序?但是跟踪了一个源代码,发现和4.4并没有什么不一样的,无奈只能Google了一下,发现原来在android 5.0之后引入了材料设计的概念,View有了Z轴的概念,而button默认的Z轴数值是2,而这个数值也会影响绘制的结果,所有导致了这个问题。解决办法就是让动画比Button还要高,这样就不会被遮盖了。

1
2
android:elevation="12dp"
android:translationZ="12dp"
  1. 解决了这个问题,我们再来实现一下波纹效果。这个波纹效果其实就是在背景上面不断绘制一个更大的圆,最终填充满整个Button就可以了。思路是比较简单的。唯一需要我们解决的问题就是这个Button是一个圆角形状的,但是你的背景绘制的时候都是在矩形的canvas上绘制的。我们可以在绘制的时候先将canvas裁切成一个圆角矩形的形状,然后绘制的时候就不需要考虑边界问题了。

那么问题又来了,怎么裁剪这个canvas呢?我采用的办法是用Path一段一段的将这个形状描绘出来,利用canvas的clipPath方法来剪裁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (mPath == null) {
mPath = new Path();
int width = getWidth();
int height = getHeight();
int radius = height / 2;
mPath.addCircle(radius, radius, radius, Path.Direction.CCW);
mPath.addCircle(width - radius, radius, radius, Path.Direction.CCW);
RectF rectF = new RectF(radius, 0, width - radius, height);
mPath.addRect(rectF, Path.Direction.CCW);
}
canvas.save();
canvas.clipPath(mPath);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, mPaint);
canvas.restore();

解决了这个问题,剩下的也比较简单了,获取好绘制的圆的圆心,通过ObjectAnimator不断修改这个圆的半径,最终绘制成一个覆盖全部按钮的大的背景即可。

贴一下完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package com.netease.snailread.view;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.support.annotation.Keep;
import android.util.AttributeSet;

import com.netease.snailread.R;
import com.netease.snailread.skin.SkinManager;

/**
* description:
*
* @author jimbo zhongjinbao1994@gmail.com
* @since 2017/10/18 下午2:35
*/
public class WaterRippleButton extends android.support.v7.widget.AppCompatButton {

private int mDrawRadiues;

private int mRadius;

private boolean mIsNeedAnimation = false;

private Paint mPaint;
private Path mPath;

public WaterRippleButton(Context context) {
super(context);
init();
}

public WaterRippleButton(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public WaterRippleButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
mPaint = new Paint();
mPaint.setColor(getResources().getColor(R.color.color_e2e2e2));
mPaint.setStyle(Paint.Style.FILL);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mDrawRadiues = getMeasuredWidth() > getMeasuredHeight() ?
getMeasuredWidth() / 2 : getMeasuredHeight() / 2;
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}

@Override
protected void onDraw(Canvas canvas) {

if (mIsNeedAnimation) {
if (mPath == null) {
mPath = new Path();
int width = getWidth();
int height = getHeight();
int radius = height / 2;
mPath.addCircle(radius, radius, radius, Path.Direction.CCW);
mPath.addCircle(width - radius, radius, radius, Path.Direction.CCW);
RectF rectF = new RectF(radius, 0, width - radius, height);
mPath.addRect(rectF, Path.Direction.CCW);
}
canvas.save();
canvas.clipPath(mPath);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, mPaint);
canvas.restore();
}

super.onDraw(canvas);
}

@Keep
public void setMRadius(int i) {
mRadius = i;
invalidate();
}

public void startWaterRipple() {

ObjectAnimator animator = ObjectAnimator.ofInt(this, "mRadius",
100, mDrawRadiues);
animator.setDuration(300);

animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mIsNeedAnimation = false;
setBackgroundDrawable(getResources().getDrawable(R.drawable.like_btn_bg));
}

@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mIsNeedAnimation = true;
setBackgroundDrawable(SkinManager.getResourceManager().getPluginDrawable("liked_btn_bg_normal"));
}
});

animator.start();

}
}

结束语

经过上面这些总结学习以及项目的实践,对于动画的理解更深了一步。对于任何动画来说,无非都是View的属性改变后重新绘制出现的效果而已,只要找到动画适当的描述语言,实现起来其实也没有那么的复杂。这个月的学习就到这里,Keep Moving~