事情是这样的0-0,这个是我的一个好基友Anonymous___童鞋发给我的一个效果图,主要是练手一下最近研究的自定义view的各种技能,于是就搞了一下,最终实现其实很简单,但是中间也绕了几次弯,先上图,这是原图效果:
下面是代码实现效果:
可能在细节方面有些差异,但是重点是实现方式,是吧- -。
首先分析一下动画的变化过程,姑且把它看成是一段圆弧的变化吧,一共可以分为一下几点:
- 颜色变化,颜色会从青色变为蓝色;
- 圆弧线条粗细的变化,刚开始是整个填充的,然后旋转一小节之后,线条开始变细;
- 圆弧旋转一周之后,长度开始缩小,并在最后延长一部分
好了,动画分析的差不多了,那么就开始实现吧。
第一步,画圆弧
图中的圆弧大概长这个样子吧
这个简单,直接使用一个Path类的addArc(),然后用Canvas画出来就可以了,可是怎么画出圆弧的轮廓呢,这个当然你可以画两个圆弧加两个半圆去实现,但是太麻烦了,其实在Paint中有一个方法getFillPath (Path src, Path dst)专门可以获取到Canvas画出的path的实际的path,但是使用这个方法,记得关闭硬件加速!!!,关闭硬件加速请参考这里Android如何关闭硬件加速,关于Paint的Api详解可以到扔物线大大的HenCoder中详细了解,我就不做过多的赘述了。效果大概就是下面这个样子,第一个图是原Path画出的圆弧,第二个是获取到的轮廓:
第二步,执行动画
这里我大概想到了两种方式:
第一种方式
直接使用属性动画(ObjectAnimator),去动态的改变自定义View的属性值,比如我们可以在自定义View中定义一个变量progessColor
来记录颜色,并给这个变量定义setter方法,在setter方法中调用invalidate方法,这样就可以通过ObjectAnimator来动态的改变progessColor
的值,来一直刷新绘制方法onDraw,从而让view“动”起来;同样的方式可以改变绘制圆弧的起始角度startAngle
和绘制View的线条粗细progessWidth
,然后同时执行这三个动画,就可以让圆弧转起来了,不过这只是前半段动画,还有后半段动画,让圆弧的长度缩小并在最后突出一小截,这个问题可以给ObjectAnimator设置监听,在动画结束后,通过一个handler通知下一个动画执行,缩小圆弧长度可以控制绘制圆弧角度的sweepAngle
属性,让其不断的减小即可,对属性动画不熟悉的同学,同样可以去看抛物线大大的HenCoder,大致代码及效果如下:
public void setProgessColor(int progessColor) {
this.progessColor = progessColor;
invalidate();
}
... 省略部分代码 ...
ObjectAnimator objectAnimator = ObjectAnimator.ofInt(this, "progessColor", startColor, endColor);
objectAnimator.setEvaluator(new ArgbEvaluator()); // 使用ARGB求值器来改变颜色
objectAnimator.setDuration(2000);
ObjectAnimator objectAnimator1 = ObjectAnimator.ofFloat(this, "progessWidth", startWidth, endWidth);
objectAnimator1.setDuration(2000);
ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(this, "startAngle", startAngle, startAngle + 360);
objectAnimator2.setDuration(2000);
objectAnimator3 = objectAnimator.ofFloat(this, "sweepAngle", sweepAngle - 2, 10, 0, -10); // 需要在最后突出一截
objectAnimator3.setDuration(3000);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(objectAnimator, objectAnimator1, objectAnimator2); // 使用AnimatorSet同时执行动画
animatorSet.start();
这种方式大家应该也看出来了,有几个缺陷,第一,原动画的弧线是运动一段时间,线的宽度才开始减小的,而这个动画直接就开始减小;第二,中间动画衔接的时候会有一小点停顿的状态;第三,这个动画得倒回去啊。当然使用
setRepeatMode(ValueAnimator.REVERSE) 和 setRepeatCount(ValueAnimator.INFINITE)
Api可以让动画倒回去,但是还需要控制动画执行顺序,太麻烦,遂弃之。
第二种方式
既然使用ObjectAnimator
不好使,有限制,那么我们就换一个他的爹地,更加灵活的ValueAnimator
,它的玩法非常简单,只需要调用ofFloat()
,然后再添加addUpdateListener()
监听,在update的过程中,不断的根据自己设计的算法改变对应的值就ok了,当然关于颜色的变化还是需要使用ObjectAnimator的方法,因为颜色的变化需要ArgbEvaluator求值器来计算,具体的算法在下面的代码里做解释:
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
// 设置update监听,并在属性更新时写自己的算法
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取到当前动画的完成度,取值为0~1
float animatedFraction = animation.getAnimatedFraction();
// 用来计算线条宽度的变化,前动画的前0.2不改变初始线条宽度,从0.2~0.9,动态的将线条宽度变为最终的宽的endWidth
// 0.9~1阶段,宽度始终为最终宽度endWidth
progessWidth = startWidth - ((animatedFraction - 0.2f) / 0.7f) * startWidth;
// 前0.2的变化会进入这个判断
if (progessWidth >= startWidth) {
progessWidth = startWidth;
}
// 0.9~1时会进这个判断
if (progessWidth <= endWidth) {
progessWidth = endWidth;
}
// 用来计算弧线长度的变化,前0.7的变化为startAngle从50度逐渐变化为450,sweepAngle固定不变,即为固定的弧长旋转动画
if (animatedFraction <= 0.7f) {
startAngle = 50 + animatedFraction / 0.7f * 400f;
sweepAngle = -100;
}
// 从0.7~1的动画为startAngle不变,sweepAngle逐渐减小再增大的过程(这个是绘制长度的减小,符号代表绘制圆弧的方向)
// 即从-100变到20,从而达到长度开始缩小,并在最后延长一部分的动画
if (animatedFraction > 0.7f) {
startAngle = 450;
sweepAngle = -100 + ((animatedFraction - 0.7f) / 0.25f) * 100;
}
invalidate(); // 别忘了invalidate触发重绘onDraw
}
});
// 动态改变颜色
ObjectAnimator objectAnimator = ObjectAnimator.ofInt(this, "progessColor", startColor, endColor);
objectAnimator.setEvaluator(new ArgbEvaluator());
// 使用AnimatorSet同时执行两种动画
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(valueAnimator, objectAnimator);
animatorSet.start();
上面代码中,0.7f了、0.2f了这些值都是可以自己改的,不同的值效果也会不一样,大家可以自己修改试一下看看效果。先看看这个实现的效果图:
是不是觉得已经ok了呢,nonono,还有最后一步,这个loading动画是循环播放的呢,而且需要倒回去,所以:
第三步,REVERSE
这个就很简单了,给动画设置setRepeatMode
和setRepeatCount
就行了。
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
objectAnimator.setRepeatMode(ValueAnimator.REVERSE);
objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
效果就不贴了,就是本文开头的那个效果。代码虽然很简单,但是重要的还是思路,这东西就跟魔术一样,看到的时候感觉很神奇,但是知道了原理就发现原来就那么回事。
总结一下
这种动画主要是通过使用valueAnimator
在AnimatorUpdateListener
中动态的改变需要绘制的图形的属性值,并不断的通过invalidate
触发onDraw
,从而使得图形在感官上是“动”起来。这样你就可以根据你自己的算法做出各种动画了。我的好基友的系列文章也挺不错的,推荐大家一看。当然想要做出这些东西是需要一定的自定义View的基础的,首先你得知道各种Api的使用和细节吧,巧妇还难为无米之炊呢,下面我推荐几个网站:
- 我大Google的官方文档,Canvas,Path,Paint,PathMeasure,Camera等等一些类的Api总得玩儿转吧,什么?你说英语差?机翻会不。不过还是希望大家没事儿了多背背单词,不能一直靠机翻吧,多low(开玩笑~~~)
- 再次推荐抛物线大大的HenCoder教程,内容不多,但是是精品
- GcsSloop的魔法首页,写的非常细,配合HenCoder食用更加
就酱,代码很简单,贴下面咯,如果大家觉得ok,那么记得点赞哈,还有,强烈推荐看看我推荐的那几个网站噻。
使用下面的代码请记得关闭硬件加速!!!关闭硬件加速!!!关闭硬件加速!!!重要的事说三遍,在清单文件的application
中加android:hardwareAccelerated="false"
即可
package com.moonight.customview;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
/**
* Created by MooNight on 2017/9/11.
*/
public class CustomProgressView extends View {
private Paint mPaint;
private float left = -100;
private float top = -100;
private float right = 100;
private float bottom = 100;
private float startAngle = 90;
private float sweepAngle = -100;
private int mViewWidthHalf, mViewHeightHalf; // 获取view的宽高的一半
private Path arcSrcPath;
private Path arcDstPath;
private int startColor = 0xFF10D2DE;
private int endColor = 0xFF1039DD;
private int progessColor = startColor;
private float progessWidth;
private float startWidth = 70;
private float endWidth = 5;
private RectF rectF;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
objectAnimator3.start(); //当第阶段动画执行完毕后,执行第二阶段动画
}
};
private ObjectAnimator objectAnimator3;
private Animator.AnimatorListener animatorListener;
public CustomProgressView(Context context) {
this(context, null);
}
public CustomProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
// 提供setter方法,使用ObjectAnimator改变对应的属性值
public void setProgessColor(int progessColor) {
this.progessColor = progessColor;
invalidate();
}
public void setProgessWidth(float progessWidth) {
this.progessWidth = progessWidth;
}
public void setStartAngle(float startAngle) {
this.startAngle = startAngle;
}
public void setSweepAngle(float sweepAngle) {
this.sweepAngle = sweepAngle;
invalidate(); // 当属性值改变时,调用invalidate出发onDraw回调
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidthHalf = w / 2; // 在onSizeChanged中获取view的宽高值
mViewHeightHalf = h / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.BLACK); // 绘制背景
canvas.save();
canvas.translate(mViewWidthHalf, mViewHeightHalf); // 将绘制中心坐标(0,0)移动到View中心
arcSrcPath = new Path(); // 每次都绘制新的path
arcDstPath = new Path();
arcSrcPath.addArc(rectF, startAngle, sweepAngle);
mPaint.setStrokeWidth(startWidth);
mPaint.getFillPath(arcSrcPath, arcDstPath); // 获取实际弧线path的轮廓
canvas.clipPath(arcDstPath); // 只显示轮廓部分,如果不这么做,会使得出事弧线过宽
mPaint.setColor(progessColor);
mPaint.setStrokeWidth(progessWidth); // 动态改变paint的颜色跟线宽
canvas.drawPath(arcDstPath, mPaint);
canvas.restore();
}
private void init() {
initPaint();
initRect();
initAnimator();
}
private void initAnimator() {
// initAnimator1();
initAnimator2();
}
/**
* 第二种实现方式
*/
private void initAnimator2() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.setDuration(2200);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取到当前动画的完成度,取值为0~1
float animatedFraction = animation.getAnimatedFraction();
// 用来计算线条宽度的变化,前动画的前0.2不改变初始线条宽度,从0.2~0.9,动态的将线条宽度变为最终的宽的endWidth
// 0.9~1阶段,宽度始终为最终宽度endWidth
progessWidth = startWidth - ((animatedFraction - 0.2f) / 0.7f) * startWidth;
// 前0.2的变化会进入这个判断
if (progessWidth >= startWidth) {
progessWidth = startWidth;
}
// 0.9~1时会进这个判断
if (progessWidth <= endWidth) {
progessWidth = endWidth;
}
// 用来计算弧线长度的变化,前0.7的变化为startAngle从50度逐渐变化为450,sweepAngle固定不变,即为固定的弧长旋转动画
if (animatedFraction <= 0.7f) {
startAngle = 50 + animatedFraction / 0.7f * 400f;
sweepAngle = -100;
}
// 从0.7~1的动画为startAngle不变,sweepAngle逐渐减小再增大的过程(这个是绘制长度的减小,符号代表绘制圆弧的方向)
// 即从-100变到20,从而达到长度开始缩小,并在最后延长一部分的动画
if (animatedFraction >= 0.7f) {
startAngle = 450;
sweepAngle = -100 + ((animatedFraction - 0.7f) / 0.25f) * 100;
}
invalidate(); // 别忘了invalidate触发重绘onDraw
}
});
// 动态改变颜色
ObjectAnimator objectAnimator = ObjectAnimator.ofInt(this, "progessColor", startColor, endColor);
objectAnimator.setRepeatMode(ValueAnimator.REVERSE);
objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
objectAnimator.setEvaluator(new ArgbEvaluator());
objectAnimator.setDuration(2200);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(valueAnimator, objectAnimator);
animatorSet.start();
}
/**
* 第一种实现方式
*/
private void initAnimator1() {
initObjectAnimatorListener();
initObjectAnimator();
}
private void initObjectAnimatorListener() {
animatorListener = new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
handler.sendEmptyMessage(0);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
}
private void initObjectAnimator() {
ObjectAnimator objectAnimator = ObjectAnimator.ofInt(this, "progessColor", startColor, endColor);
// objectAnimator.setRepeatMode(ValueAnimator.REVERSE);
// objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
objectAnimator.setEvaluator(new ArgbEvaluator());
objectAnimator.addListener(animatorListener);
objectAnimator.setDuration(2000);
ObjectAnimator objectAnimator1 = ObjectAnimator.ofFloat(this, "progessWidth", startWidth, endWidth);
// objectAnimator1.setRepeatMode(ValueAnimator.REVERSE);
// objectAnimator1.setRepeatCount(ValueAnimator.INFINITE);
objectAnimator1.setDuration(2000);
ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(this, "startAngle", startAngle, startAngle + 360);
objectAnimator2.setInterpolator(new AccelerateDecelerateInterpolator());
// objectAnimator2.setRepeatMode(ValueAnimator.REVERSE);
// objectAnimator2.setRepeatCount(ValueAnimator.INFINITE);
objectAnimator2.setDuration(2000);
objectAnimator3 = objectAnimator.ofFloat(this, "sweepAngle", sweepAngle - 2, 10, 0, -10);
objectAnimator3.setDuration(3000);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(objectAnimator, objectAnimator1, objectAnimator2);
animatorSet.start();
}
private void initRect() {
rectF = new RectF(left, top, right, bottom);
}
private void initPaint() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND); // 线端点处设置会圆头
mPaint.setStrokeWidth(startWidth);
mPaint.setColor(startColor);
}
}