一、前言
在Android系统中,总共有三种动画,其中有属性动画、Tween动画和帧动画,基于这三种动画可以演变出更多动画效果。本篇所说的动画是基于自定义View实现的,按原则的话应该属于帧动画,我们知道帧动画也是一种队列动画。所谓队列动画本质上就是有顺序的动画,第一个顺序位的动画总是不晚于下一个动画的出现,照这么说同时出现的也属于队列动画,显然这个结论是正确的,开源动画框架lottie 通过多层layer compose实现了这类顺序动画。
效果预览
(首尾平滑过渡类型)
(非首尾平滑过渡型)
二、顺序动画的原理
-
顺序执行 : 动画执行不存在超过前一帧的情况,但可以晚于前一顺序的时间(t >= 0)播放,也可以并行执行,但仍然保持顺序方法调用。
-
每个执行点位具备固定的时间: duration ,每一个点位执行的动画的时间应该是一样的
-
允许设置延迟间隔:可设置每一点位相对前一个位置的延迟开始时间
-
每个执行点位必须在duration之后才能发起下次动画
难点:首尾平滑过渡
实际上,原理很简单,但是难点确实首尾平滑过渡问题,对于不需要平滑过渡的动画,在点位动画duration执行后,再次发起即可,但是对于需要首尾平滑过渡的动画,这个难点在于,必须保证 每个动画较前一个动画duration/n的时间间隔,同时满足下次执行动画必须在(n -1)* (duration/n) 时间之后才能执行,总结如下。
-
开始时时间间隔:duration/n
-
下次执行时间:(n -1)* (duration/n)
-
下次无需考虑时间间隔,因为在(n -1)* (duration/n)时间后,就已经到到时间需要了开始动画了。
三、核心代码
3.1 首先来看非平滑过渡型
这种动画的优点是可随意修改duration和startDelayDuration,同时可以通过修改这两种参数调制出各种队列动画效果。
ini
if (items == null || items.isEmpty()) {
if (items == null) {
items = new ArrayList<>();
} else {
items.clear();
}
long millis = SystemClock.uptimeMillis();
for (int i = 0; i < mLineCount; i++) {
float centerX = -contentWidth / 2F + i * blockWidth + spanWidth / 2F;
float centerY = minSpanHeight / 2F;
Item item = new Item();
item.positionX = centerX;
item.positionY = centerY;
item.startTime = millis; //开始时间
item.startDelayTime = i * keyFrameStartDelayInterval; //延迟开始时间
item.duration = keyFrameDuration; //每一个点位的执行时间
item.spanHeightOffset = spanHeightOffset; //缩放范围
item.interpolator = interpolator; //插值器
item.color = argb(random.nextFloat(),random.nextFloat(),random.nextFloat());
items.add(item);
}
}
绘制逻辑
ini
public void draw(Canvas canvas, Paint paint, long clockTime, boolean isAllFinished) {
if (isAllFinished) {
//重制时间,开始下一次播放
isFinished = false;
startTime = clockTime;
}
long t = clockTime - startTime - startDelayTime;
float offsetHeight = 0;
if (t > 0 && !isFinished) {
float fraction = t * 1F / duration;
if (fraction > 1) {
//防止抖动
fraction = 1F;
}
float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction; //使用插值
// float degree = vfraction * 180;
// offsetHeight = (float) (spanHeightOffset * Math.sin(Math.toRadians(degree)));
// 用sin曲线,当然sin曲线带有速度,和差值有些冲突,正确的做法是分时间段
if (fraction <= 0.5f) {
float upFraction = vfraction * 2F; //拉伸时间
if (upFraction > 1) {
upFraction = 1F;
//因为使用过插值,因此必须保证不能超过1,避免引起抖动
}
offsetHeight = spanHeightOffset * upFraction;
} else {
float downFraction = (vfraction - 0.5f) * 2F; //拉伸时间
if (downFraction < 0F) {
downFraction = 0;
//因为使用过插值,因此必须保证不小于0,避免引起抖动
}
offsetHeight = spanHeightOffset * (1F - downFraction);
}
}
float centerX = positionX;
float centerY = positionY + offsetHeight / 2;
paint.setColor(color);
canvas.drawLine(centerX, centerY, centerX, -centerY, paint);
if (t >= duration) {
isFinished = true;
//本次绘制完成
}
}
代码里写了注释,注意,我们的缩放没有使用Sin函数,原因是Sin函数本身也是插值的一种,因此我们这里使用了拉伸比例时间的方式,设计的拉伸和缩放算法。
全部代码
ini
public class MatrixLearningView extends View {
private static final String TAG = "MatrixLearningView";
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
Matrix matrix = new Matrix();
private float[] vertex = new float[9];
private int mLineCount = 5;
private int spanWidth = 10;
private int padding = 10;
private int minSpanHeight = 50;
private int spanHeightOffset = 50;
private List<Item> items = new ArrayList<>();
private int contentWidth;
private int blockWidth;
private TimeInterpolator interpolator;
private long keyFrameStartDelayInterval = 100; //每个帧之间开始的时间间隔
private long keyFrameDuration = 350;
private Random random = new Random();
public MatrixLearningView(Context context) {
this(context, null);
}
public MatrixLearningView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MatrixLearningView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width <= 1 || height <= (minSpanHeight + spanHeightOffset) || mLineCount <= 0) {
return;
}
contentWidth = (spanWidth + padding) * mLineCount - padding;
if (contentWidth > width) {
return;
}
mCommonPaint.setStrokeWidth(spanWidth);
int count = canvas.save();
vertex[0] = 1;
vertex[1] = 0;
vertex[2] = width / 2F;
vertex[3] = 0;
vertex[4] = 1;
vertex[5] = height / 2F;
vertex[6] = 0;
vertex[7] = 0;
vertex[8] = 1;
matrix.reset();
matrix.setValues(vertex);
canvas.concat(matrix);
blockWidth = spanWidth + padding;
if (items == null || items.isEmpty()) {
if (items == null) {
items = new ArrayList<>();
} else {
items.clear();
}
long millis = SystemClock.uptimeMillis();
for (int i = 0; i < mLineCount; i++) {
float centerX = -contentWidth / 2F + i * blockWidth + spanWidth / 2F;
float centerY = minSpanHeight / 2F;
Item item = new Item();
item.positionX = centerX;
item.positionY = centerY;
item.startTime = millis; //开始时间
item.startDelayTime = i * keyFrameStartDelayInterval; //延迟开始时间
item.duration = keyFrameDuration; //每一个点位的执行时间
item.spanHeightOffset = spanHeightOffset; //缩放范围
item.interpolator = interpolator; //插值器
item.color = argb(random.nextFloat(),random.nextFloat(),random.nextFloat());
items.add(item);
}
}
int size = items.size();
if (size > 0) {
boolean isAllFinished = items.get(size - 1).isFinished;
long millis = SystemClock.uptimeMillis();
for (int j = 0; j < size; j++) {
Item item = items.get(j);
item.draw(canvas, mCommonPaint, millis, isAllFinished);
}
}
canvas.restoreToCount(count);
postInvalidateDelayed(32);
}
private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
interpolator = new AccelerateDecelerateInterpolator();
}
public void start() {
postInvalidateDelayed(16);
}
static class Item {
private long duration;
private long startDelayTime;
private long startTime;
private float positionX;
private float positionY;
private boolean isFinished = false;
private float spanHeightOffset;
private int color;
private TimeInterpolator interpolator;
public void draw(Canvas canvas, Paint paint, long clockTime, boolean isAllFinished) {
if (isAllFinished) {
//重制时间,开始下一次播放
isFinished = false;
startTime = clockTime;
}
long t = clockTime - startTime - startDelayTime;
float offsetHeight = 0;
if (t > 0 && !isFinished) {
float fraction = t * 1F / duration;
if (fraction > 1) {
//防止抖动
fraction = 1F;
}
float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction; //使用插值
// float degree = vfraction * 180;
// offsetHeight = (float) (spanHeightOffset * Math.sin(Math.toRadians(degree)));
// 用sin曲线,当然sin曲线带有速度,和差值有些冲突,正确的做法是分时间段
if (fraction <= 0.5f) {
float upFraction = vfraction * 2F; //拉伸时间
if (upFraction > 1) {
upFraction = 1F;
//因为使用过插值,因此必须保证不能超过1,避免引起抖动
}
offsetHeight = spanHeightOffset * upFraction;
} else {
float downFraction = (vfraction - 0.5f) * 2F; //拉伸时间
if (downFraction < 0F) {
downFraction = 0;
//因为使用过插值,因此必须保证不小于0,避免引起抖动
}
offsetHeight = spanHeightOffset * (1F - downFraction);
}
}
float centerX = positionX;
float centerY = positionY + offsetHeight / 2;
paint.setColor(color);
canvas.drawLine(centerX, centerY, centerX, -centerY, paint);
if (t >= duration) {
isFinished = true;
//重制绘制条件
}
}
}
public void setKeyFrameDuration(long keyFrameDuration) {
this.keyFrameDuration = keyFrameDuration;
items.clear();
}
public void setKeyFrameStartDelayInterval(long keyFrameStartDelayInterval) {
this.keyFrameStartDelayInterval = keyFrameStartDelayInterval;
items.clear();
}
public static int argb(float red, float green, float blue) {
return ((int) (1 * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}
}
3.2 接着看平滑过渡型
平滑过渡型的条件相对严格,不会出现多种构型效果,其duration和startDelayTime存在严格的关联关系。
ini
if (items == null || items.isEmpty()) {
if (items == null) {
items = new ArrayList<>();
} else {
items.clear();
}
long keyFrameStartDelayInterval = duration / mLineCount; //时间间隔
long time = clockTime;
//卡顿和线程休眠容易造成时间堆积问题,导致最终效果趋同,因此我们用自己的时钟
float degree = 360f / mLineCount;
for (int i = 0; i < mLineCount; i++) {
double radians = Math.toRadians(degree * i);
float centerX = (float) (radius * Math.cos(radians));
float centerY = (float) (radius * Math.sin(radians));
Item item = new Item();
item.positionX = centerX;
item.positionY = centerY;
item.startTime = time;
item.startDelayTime = i * keyFrameStartDelayInterval; //设置延迟时间
item.duration = duration; //设置每个点位的执行时间
item.spanHeightOffset = spanHeightOffset; //设置拉伸大小
item.interpolator = interpolator; //设置插值起
item.radians = radians; //设置角度
item.color = argb(random.nextFloat(),random.nextFloat(),random.nextFloat()); //设置颜色
items.add(item);
}
}
绘制逻辑
ini
public void draw(Canvas canvas, Paint paint, long clockTime) {
long t = getRunningTime(clockTime);
float offsetHeight = 0;
if (t > 0) {
float fraction = t * 1F / duration;
if (fraction > 1) {
//防止抖动
fraction = 1F;
}
float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction; //使用插值
if (fraction <= 0.5f) {
float upFraction = vfraction * 2F;
if (upFraction > 1) {
upFraction = 1F;
}
offsetHeight = spanHeightOffset * upFraction;
} else {
float downFraction = (vfraction - 0.5f) * 2F;
if (downFraction < 0F) {
downFraction = 0;
}
offsetHeight = spanHeightOffset * (1F - downFraction);
}
}
paint.setColor(color);
if(offsetHeight != 0) {
double xOffset = Math.cos(radians) * offsetHeight / 2F;
double yOffset = Math.sin(radians) * offsetHeight / 2F;
float startX = (float) (positionX + xOffset);
float startY = (float) (positionY + yOffset);
float stopX = (float) (positionX - xOffset);
float stopY = (float) (positionY - yOffset);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}else{
// offsetHeight 为0时,线条时看不见的,这样会出现闪烁问题,这里使用Point代替
canvas.drawPoint(positionX,positionY,paint);
}
if(t >= duration){
startTime = clockTime;
startDelayTime = 0;
// 这里是最大的区别,任何一个点,首次播放动画后下次,会一直延续首次的时间间隔,这里如果在使用时间间隔,将造成乱序和延迟
}
}
注意: t >= duration 时,startDelayTime本身在顺序位已经延迟过了,需要置为0才行。
另外如果要实现下面效果,修改缩放算法即可
ini
public void draw(Canvas canvas, Paint paint, long clockTime) {
long t = getRunningTime(clockTime);
float offsetHeight = 0;
if (t > 0) {
float fraction = t * 1F / duration;
if (fraction > 1) {
//防止抖动
fraction = 1F;
}
float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction; //使用插值
offsetHeight = spanHeightOffset * (1F - vfraction);
}
paint.setColor(color);
if(offsetHeight != 0) {
double xOffset = Math.cos(radians) * offsetHeight / 2F;
double yOffset = Math.sin(radians) * offsetHeight / 2F;
float startX = (float) (positionX + xOffset);
float startY = (float) (positionY + yOffset);
float stopX = (float) (positionX - xOffset);
float stopY = (float) (positionY - yOffset);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}else{
// offsetHeight 为0时,线条时看不见的,这样会出现闪烁问题,这里使用Point代替
canvas.drawPoint(positionX,positionY,paint);
}
if(t >= duration){
startTime = clockTime;
startDelayTime = 0;
// 这里是最大的区别,任何一个点,首次播放动画后下次,会一直延续首次的时间间隔,这里如果在使用时间间隔,将造成乱序和延迟
}
}
完整代码
ini
public class MatrixCircleLearningView extends View {
private static final String TAG = "MatrixLearningView";
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
Matrix matrix = new Matrix();
private float[] vertex = new float[9];
private int mLineCount = 20;
private int spanHeightOffset = 50;
private List<Item> items = new ArrayList<>();
private TimeInterpolator interpolator;
private long duration = 1500; //每个帧之间开始的时间间隔
private float padding = 20F;
private int spanWidth = 10;
private Random random = new Random();
//自定义时钟,防止因线程卡顿休眠引起的时间堆积问题
private long clockTime = 0L;
public MatrixCircleLearningView(Context context) {
this(context, null);
}
public MatrixCircleLearningView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MatrixCircleLearningView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width <= padding || height <= padding || mLineCount <= 0) {
return;
}
clockTime += 32;
float radius = Math.min(width, height) / 2F - padding * 2;
int count = canvas.save();
vertex[0] = 1;
vertex[1] = 0;
vertex[2] = width / 2F;
vertex[3] = 0;
vertex[4] = 1;
vertex[5] = height / 2F;
vertex[6] = 0;
vertex[7] = 0;
vertex[8] = 1;
matrix.reset();
matrix.setValues(vertex);
canvas.concat(matrix);
if (items == null || items.isEmpty()) {
if (items == null) {
items = new ArrayList<>();
} else {
items.clear();
}
long keyFrameStartDelayInterval = duration / mLineCount; //时间间隔
long time = clockTime;
//这类动画对时间敏感,卡顿和线程休眠容易造成时间堆积问题,导致最终效果趋同,因此我们用自己的时钟
float degree = 360f / mLineCount;
for (int i = 0; i < mLineCount; i++) {
double radians = Math.toRadians(degree * i);
float centerX = (float) (radius * Math.cos(radians));
float centerY = (float) (radius * Math.sin(radians));
Item item = new Item();
item.positionX = centerX;
item.positionY = centerY;
item.startTime = time;
item.startDelayTime = i * keyFrameStartDelayInterval; //设置延迟时间
item.duration = duration; //设置每个点位的执行时间
item.spanHeightOffset = spanHeightOffset; //设置拉伸大小
item.interpolator = interpolator; //设置插值起
item.radians = radians; //设置角度
item.color = argb(random.nextFloat(),random.nextFloat(),random.nextFloat()); //设置颜色
items.add(item);
}
}
mCommonPaint.setStrokeWidth(spanWidth);
int size = items.size();
if (size > 0) {
long clockTime = this.clockTime;
for (int j = 0; j < size; j++) {
Item item = items.get(j);
item.draw(canvas, mCommonPaint, clockTime);
}
}
canvas.restoreToCount(count);
postInvalidateDelayed(32);
}
private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
interpolator = new AccelerateDecelerateInterpolator();
}
public void start() {
postInvalidateDelayed(16);
}
static class Item {
private long duration;
private long startDelayTime;
private long startTime;
private float positionX;
private float positionY;
private float spanHeightOffset;
private double radians;
private TimeInterpolator interpolator;
private int color;
public void draw(Canvas canvas, Paint paint, long clockTime) {
long t = getRunningTime(clockTime);
float offsetHeight = 0;
if (t > 0) {
float fraction = t * 1F / duration;
if (fraction > 1) {
//防止抖动
fraction = 1F;
}
float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction; //使用插值
if (fraction <= 0.5f) {
float upFraction = vfraction * 2F;
if (upFraction > 1) {
upFraction = 1F;
}
offsetHeight = spanHeightOffset * upFraction;
} else {
float downFraction = (vfraction - 0.5f) * 2F;
if (downFraction < 0F) {
downFraction = 0;
}
offsetHeight = spanHeightOffset * (1F - downFraction);
}
}
paint.setColor(color);
if(offsetHeight != 0) {
double xOffset = Math.cos(radians) * offsetHeight / 2F;
double yOffset = Math.sin(radians) * offsetHeight / 2F;
float startX = (float) (positionX + xOffset);
float startY = (float) (positionY + yOffset);
float stopX = (float) (positionX - xOffset);
float stopY = (float) (positionY - yOffset);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}else{
// offsetHeight 为0时,线条时看不见的,这样会出现闪烁问题,这里使用Point代替
canvas.drawPoint(positionX,positionY,paint);
}
if(t >= duration){
startTime = clockTime;
startDelayTime = 0;
// 这里是最大的区别,任何一个点,首次播放动画后下次,会一直延续首次的时间间隔,这里如果在使用时间间隔,将造成乱序和延迟
}
}
private long getRunningTime(long clockTime) {
return clockTime - startTime - startDelayTime;
}
}
public static int argb(float red, float green, float blue) {
return ((int) (1 * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}
}
四、总结
其实,作为队列动画,其具备粒子动画的特点,我们任何时候定义粒子动画,不要忘记使用面向对象的编程思想,因此管理每一个粒子远比管理一群粒子简单的多。当然,上述动画也可以使用属性动画去实现,但是每一个点都有个Animator感觉似乎很奢侈。
下面有同感Animator实现的