一、前言
队列动画我们之前有篇文章专门写过,实际上每个动画结束后的状态处理是难点,队列动画的特定是
-
顺序执行 : 动画执行不存在超过前一帧的情况,但可以晚于前一顺序的时间(t >= 0)播放,也可以并行执行,但仍然保持顺序方法调用。
-
每个执行点位具备固定的时间: duration ,每一个点位执行的动画的时间应该是一样的
-
允许设置延迟间隔:可设置每一点位相对前一个位置的延迟开始时间
-
每个执行点位必须在duration之后才能发起下次动画
1.1 缓冲式效果
1.2 流体效果
1.3 扩散
二、原理
2.1 缓冲式动画原理
实际上,原理很简单,但是难点确实首尾平滑过渡问题,对于不需要平滑过渡的动画,在点位动画duration执行后,再次发起即可,但是对于需要首尾平滑过渡的动画,这个难点在于,必须保证 每个动画较前一个动画duration/n的时间间隔,同时满足下次执行动画必须在(n -1)* (duration/n) 时间之后才能执行,总结如下。
- 开始时时间间隔:duration/n
- 下次执行时间:(n -1)* (duration/n)
- 下次无需考虑时间间隔,因为在(n -1)* (duration/n)时间后,就已经到到时间需要了开始动画了。
2.2 流体动画原理
流体动画相对简单,只不过需要控制粒子的的随机效果
boolean update() {
this.randomSpeed.x = getRandomRange(-0.001f, 0.001f);
this.randomSpeed.y = getRandomRange(0.01f, 0.02f);
this.speed.x += this.randomSpeed.x;
this.speed.y += this.randomSpeed.y;
this.x += this.speed.x;
this.y += this.speed.y;
if (this.radius >= 0.01f) {
this.radius -= 0.2f;
this.alpha -= 0.001f;
return true;
}
this.radius = 0f;
this.alpha = 0f;
return false;
}
2.3 粒子扩散动画原理
本质上,此种动画基于2.2实现,因此原理和2.2一致
三、实现
3.1 缓冲式动画实现
之前的动画使用自己写的动画,这次我们使用Animator实现,使用先加速后减速的插值器实现快慢变化。
我们这里实现2种预览图种的效果,通过LINE_STYLE_DOT进行控制样式。
public class LoadingView extends View {
RectF arcBounds = new RectF();
private static final long ANIMATION_TIMEOUT = 1500;
private TextPaint mPaint;
private int mMaxRadius;
private int mDotWidth;
private int mColor = Color.TRANSPARENT;
private AnimatorSet mCircleDotAnimatorSet;
private float[] mDotAngles = null;
public final static int LINE_STYLE_DOT = 0;
public final static int LINE_STYLE_ARC = 1;
private int mLineStyle = LINE_STYLE_DOT;
public LoadingView(Context context) {
this(context, null);
}
public LoadingView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
public void setLineStyle(int mLineStyle) {
this.mLineStyle = mLineStyle;
if (mLineStyle == LINE_STYLE_DOT) {
mDotWidth = (int) dip2px(8);
} else {
mDotWidth = (int) dip2px(3);
}
}
public float dip2px(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
private void initPaint() {
// 实例化画笔并打开抗锯齿
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mPaint.setPathEffect(new CornerPathEffect(10)); //设置线条类型
mPaint.setStrokeWidth(dip2px(1));
mPaint.setTextSize(dip2px((12)));
mPaint.setStyle(Paint.Style.STROKE);
mDotWidth = (int) dip2px(8);
mColor = argb((float) Math.random(), (float) Math.random(), (float) Math.random());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mDotAngles = null;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
width = (int) dip2px(100);
}
if (heightMode != MeasureSpec.EXACTLY) {
height = (int) dip2px(100);
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width == 0 || height == 0) return;
int diameter = Math.min(width, height) / 2;
mMaxRadius = diameter - mDotWidth / 2;
initPoints();
int storeId = canvas.save();
canvas.translate(width / 2F, height / 2F);
if (mLineStyle == LINE_STYLE_DOT) {
drawDotCircle(canvas);
} else {
drawArcCircle(canvas);
}
canvas.restoreToCount(storeId);
}
private void drawArcCircle(Canvas canvas) {
if (mDotAngles == null || mDotAngles.length == 0) return;
int firstPointY = (int) (Math.sin(Math.toRadians(mDotAngles[0])) * mMaxRadius);
int firstPointX = (int) (Math.cos(Math.toRadians(mDotAngles[0])) * mMaxRadius);
int lastPointY = (int) (Math.sin(Math.toRadians(mDotAngles[mDotAngles.length - 1])) * mMaxRadius);
int lastPointX = (int) (Math.cos(Math.toRadians(mDotAngles[mDotAngles.length - 1])) * mMaxRadius);
double lineLenghtPower = Math.pow(firstPointY - lastPointY, 2) + Math.pow(firstPointX - lastPointX, 2);
double length = Math.sqrt(lineLenghtPower) / 2f;
float degrees = (float) Math.toDegrees(Math.asin(length / mMaxRadius)) * 2;
drawArcSport(canvas, mDotAngles[mDotAngles.length - 1], degrees);
}
private void drawDotCircle(Canvas canvas) {
if (mDotAngles == null) return;
int color = mPaint.getColor();
float strokeWidth = mPaint.getStrokeWidth();
Paint.Cap strokeCap = mPaint.getStrokeCap();
mPaint.setStrokeWidth(mDotWidth);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setColor(mColor);
for (int i = 0; i < mDotAngles.length; i++) {
double dotAngle = mDotAngles[i];
int pointY = (int) (Math.sin(Math.toRadians(dotAngle)) * mMaxRadius);
int pointX = (int) (Math.cos(Math.toRadians(dotAngle)) * mMaxRadius);
canvas.drawPoint(pointX, pointY, mPaint);
}
mPaint.setColor(color);
mPaint.setStrokeWidth(strokeWidth);
mPaint.setStrokeCap(strokeCap);
}
private void initPoints() {
if (mDotAngles != null && mDotAngles.length == 5) return;
mDotAngles = new float[5];
float dotRadius = mDotWidth / 2f;
float dotAngle = (float) Math.toDegrees(Math.atan(dotRadius / mMaxRadius) * 2);
for (int i = 0; i < mDotAngles.length; i++) {
float angle = 360f - (dotAngle + 8) * (i + 1);
mDotAngles[i] = angle;
}
if (mCircleDotAnimatorSet != null) {
mCircleDotAnimatorSet.cancel();
}
mCircleDotAnimatorSet = buildDotAnimationSet();
mCircleDotAnimatorSet.start();
}
private AnimatorSet buildDotAnimationSet() {
AnimatorSet animatorSet = new AnimatorSet();
Animator[] animators = new Animator[mDotAngles.length];
for (int i = 0; i < mDotAngles.length; i++) {
Animator animator = buildDotAnimation(mDotAngles[i], i);
animator.setStartDelay(100 * i);
animators[i] = animator;
}
animatorSet.playTogether(animators);
return animatorSet;
}
private Animator buildDotAnimation(final float startAngle, final int i) {
ValueAnimator animatorTimer = ValueAnimator.ofFloat(0, 1);
animatorTimer.setDuration(ANIMATION_TIMEOUT);
animatorTimer.setRepeatCount(ValueAnimator.INFINITE);
animatorTimer.setInterpolator(new AccelerateDecelerateInterpolator());
animatorTimer.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
float angle = (float) (startAngle + fraction * 360);
if (mDotAngles != null) {
mDotAngles[i] = angle;
}
if (i == 0) {
postInvalidate();
}
}
});
return animatorTimer;
}
public static int argb(
@IntRange(from = 0, to = 255) int alpha,
@IntRange(from = 0, to = 255) int red,
@IntRange(from = 0, to = 255) int green,
@IntRange(from = 0, to = 255) int blue) {
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
try {
if (mCircleDotAnimatorSet != null) {
mCircleDotAnimatorSet.cancel();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void drawArcSport(Canvas canvas, float currentAngle, float sweepAngle) {
int oldColor = mPaint.getColor();
Paint.Style style = mPaint.getStyle();
float strokeWidth = mPaint.getStrokeWidth();
mPaint.setStrokeWidth(mDotWidth);
mPaint.setColor(mColor);
mPaint.setStyle(Paint.Style.STROKE);
arcBounds.set(-mMaxRadius, -mMaxRadius, mMaxRadius, mMaxRadius);
canvas.drawArc(arcBounds, currentAngle, sweepAngle, false, mPaint);
mPaint.setColor(oldColor);
mPaint.setStyle(style);
mPaint.setStrokeWidth(strokeWidth);
}
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 流体式动画效果实现
这部分主要是通过粒子效果来实现,基本是围绕圆运动的过程更新和产生新的粒子,不过这里要注意的一点是,粒子是y轴方向是向下运动,因此,速度必须小于0.
public class FluidLoadingView extends View {
private TextPaint mPaint;
public FluidLoadingView(Context context) {
super(context);
}
public FluidLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FluidLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
{
initPaint();
}
private void initPaint() {
//否则提供给外部纹理绘制
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(sp2px(14));
mPaint.setStrokeWidth(10);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
List<Particle> particles = new ArrayList<>();
float angle = (float) (45);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int count = canvas.save();
final float radius = Math.min(width,height) / 5f ;
canvas.translate(width/2f,height/2f);
if(particles.size() < 100){
Particle particle = createParticle(angle,radius);
particles.add(particle);
}
for (int j = 0; j < particles.size(); j++) {
Particle p = particles.get(j);
boolean update = p.update();
p.draw(canvas,mPaint);
if(!update){
double radians = Math.toRadians(angle);
float x = (float) (radius * Math.sin(radians));
float y = (float) (radius * Math.cos(radians));
p.init(x,y);
}
}
canvas.restoreToCount(count);
if (angle <= 360f) {
angle += 2.5f;
} else {
angle = angle - 360f;
}
postInvalidate();
}
private Particle createParticle(float angle,float radius) {
double radians = Math.toRadians(angle);
float x = (float) (radius * Math.sin(radians));
float y = (float) (radius * Math.cos(radians));
Particle p = new Particle();
p.init(x,y);
return p;
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, getResources().getDisplayMetrics());
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
static float getRandomRange(float min, float max) {
return (float) (min + Math.floor(Math.random() * (max - min + 1)));
}
static class Particle {
float radius;
float x;
float y;
PointF speed = null;
PointF randomSpeed = null;
float alpha = 1f;
Particle(){
this.speed = new PointF();
this.randomSpeed = new PointF();
}
private void init(float x, float y) {
this.x = x;
this.y = y;
this.radius = getRandomRange(10, 16);
this.speed.x = 0;
this.speed.y = 0;
this.randomSpeed.x = 0;
this.randomSpeed.y = 0;
this.alpha = 1f;
}
boolean update() {
this.randomSpeed.x = getRandomRange(-0.001f, 0.001f);
this.randomSpeed.y = getRandomRange(0.01f, 0.02f);
this.speed.x += this.randomSpeed.x;
this.speed.y += this.randomSpeed.y;
this.x += this.speed.x;
this.y += this.speed.y;
if (this.radius >= 0.01f) {
this.radius -= 0.2f;
this.alpha -= 0.001f;
return true;
}
this.radius = 0f;
this.alpha = 0f;
return false;
}
void draw(Canvas canvas,Paint paint) {
paint.setColor(argb(1,1,1,this.alpha));
canvas.drawCircle(this.x, this.y, this.radius, paint);
}
}
public static int argb(float alpha, float red, float green, float blue) {
return ((int) (alpha * 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.3 粒子扩散动画
扩散动画主要核心逻辑和3.2类似,但是不同点四,这里使用2000多个粒子,同时使用Shader来渲染
public class ParticleShaderView extends View {
private TextPaint mPaint;
private TextPaint mParticlePaint;
private final List<Particle> particles = new ArrayList<>();
private final List<Particle> recycleParticles = new ArrayList<>();
private final List<Particle> aliveParticles = new ArrayList<>();
private final Matrix matrix = new Matrix();
private float angle = 45;
private float imageAngle = 45;
private final float particleCircleRadius = 4.5f;
private final int MAX_NUM = 2048;
private boolean isResume = true;
private int primaryColor = Color.TRANSPARENT;
private float particleTrackWidth;
private boolean isShouldCreate = false;
private final String TAG = "DeepMusicPlayingView";
private TextPaint mBitmapPaint;
private int secondaryColor;
private Matrix particleMatrix = new Matrix();
private int particleColor = 0xFFFF1493;
public ParticleShaderView(Context context) {
super(context);
}
public ParticleShaderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ParticleShaderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private Bitmap primaryImageBitmap;
private Shader primaryBitmapShader;
private Shader particleShader;
private final int threshold;
{
initPaint();
threshold = MAX_NUM - 50;
}
private void initPaint() {
//否则提供给外部纹理绘制
this.mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
this.mPaint.setAntiAlias(true);
this.mPaint.setDither(true);
this.mPaint.setStrokeCap(Paint.Cap.ROUND);
this.mPaint.setStyle(Paint.Style.FILL);
this.mPaint.setTextSize(sp2px(14));
this.mPaint.setStrokeWidth(5);
this.mParticlePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
this.mParticlePaint.setAntiAlias(true);
this.mParticlePaint.setDither(true);
this.mParticlePaint.setStrokeCap(Paint.Cap.ROUND);
this.mParticlePaint.setStyle(Paint.Style.FILL);
this.mBitmapPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
this.mBitmapPaint.setAntiAlias(true);
this.mBitmapPaint.setDither(true);
this.mBitmapPaint.setStyle(Paint.Style.FILL);
this.particleTrackWidth = dp2px(30);
}
public void setPrimaryColor(int primaryColor) {
this.primaryColor = primaryColor;
this.primaryBitmapShader = null;
postInvalidate();
}
public void pause() {
this.isResume = false;
invalidate();
}
public void resume() {
this.isResume = true;
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
boolean isResume = this.isResume;
float radius = Math.min(width, height) / 2f;
int save = canvas.save();
canvas.translate(width / 2f, height / 2f);
float distance = radius - particleTrackWidth *2/3f ;
float textureRadius = radius - particleTrackWidth;
if(this.particleShader == null && this.particleColor != Color.TRANSPARENT){
this.mParticlePaint.setColor(this.particleColor);
this.particleShader = new RadialGradient(0, 0, particleCircleRadius, new int[]{this.particleColor, Color.TRANSPARENT}, new float[]{
0, 0.85f
}, Shader.TileMode.CLAMP);
this.mParticlePaint.setShader(this.particleShader);
}
mPaint.setColor(this.primaryColor);
canvas.drawCircle(0, 0, textureRadius, mPaint);
if (this.secondaryColor != Color.TRANSPARENT) {
mPaint.setColor(this.secondaryColor);
canvas.drawCircle(0, 0, radius * 3 / 5f, mPaint);
}
drawPrimaryBitmap(canvas, radius * 3 / 5f - this.particleTrackWidth/2f);
double preRadian = Math.toRadians(angle);
float lx = (float) (distance * Math.cos(preRadian));
float ly = (float) (distance * Math.sin(preRadian));
mPaint.setColor(this.primaryColor);
canvas.drawCircle(lx, ly, particleCircleRadius, mPaint);
aliveParticles.clear();
for (int i = 0; i < particles.size(); i++) {
Particle next = particles.get(i);
next.draw(canvas, mParticlePaint, particleMatrix);
if(isResume) {
next.update();
if (next.alpha >= 0.05f) {
aliveParticles.add(next);
} else {
recycleParticles.add(next);
}
}
}
canvas.restoreToCount(save);
if (isResume) {
particles.clear();
particles.addAll(aliveParticles);
int particleCount = particles.size();
if (!isShouldCreate && particleCount < threshold) {
isShouldCreate = true;
}
if (isShouldCreate) {
final float speed = 1.5f;
if (MAX_NUM - particleCount >= 20) {
int count = (int) (1 + Math.random() * 19);
for (int i = 0; i < count; i++) {
Particle particle = createParticle(lx, ly, 1, speed, speed);
particles.add(particle);
}
} else {
Particle particle = createParticle(lx, ly, 1, speed, speed);
particles.add(particle);
}
}
if (particles.size() >= MAX_NUM - 50) {
isShouldCreate = false;
}
if (angle <= 360f) {
angle += 1.5f;
} else {
angle = angle - 360f;
}
if (imageAngle <= 360f) {
imageAngle += 0.5f;
} else {
imageAngle = imageAngle - 360f;
}
postInvalidateDelayed(16);
}
}
private void drawPrimaryBitmap(Canvas canvas, float textureRadius) {
if (this.primaryImageBitmap != null && !this.primaryImageBitmap.isRecycled()) {
if (primaryBitmapShader == null) {
primaryBitmapShader = new BitmapShader(this.primaryImageBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
}
drawRotateBitmap(canvas, this.primaryImageBitmap, textureRadius, primaryBitmapShader, mBitmapPaint);
}
}
private void drawRotateBitmap(Canvas canvas, Bitmap primaryImageBitmap, float textureRadius, Shader primaryBitmapShader, Paint bitmapPaint) {
matrix.reset();
int bitmapWidth = primaryImageBitmap.getWidth();
matrix.postTranslate(-bitmapWidth / 2f, -bitmapWidth / 2f);
matrix.postScale(textureRadius * 2 / bitmapWidth, textureRadius * 2 / bitmapWidth);
matrix.postRotate(imageAngle);
primaryBitmapShader.setLocalMatrix(matrix);
bitmapPaint.setShader(primaryBitmapShader);
canvas.drawCircle(0, 0, textureRadius, bitmapPaint);
}
private Particle createParticle(float lx, float ly, int alpha, float spx, float spy) {
if(!recycleParticles.isEmpty()){
int i = recycleParticles.size() - 1;
Particle particle = recycleParticles.remove(i);
particle.reset(lx,ly,particleCircleRadius,alpha,spx,spy);
return particle;
}
return new Particle(lx, ly, particleCircleRadius, alpha, spx, spy);
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, getResources().getDisplayMetrics());
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
static float getRandomRange(float min, float max) {
return (float) (min + Math.floor(Math.random() * (max - min + 1)));
}
public void setSecondaryColor(int secondaryColor) {
this.secondaryColor = secondaryColor;
postInvalidate();
}
public void setPrimaryImage(Bitmap bitmap) {
this.primaryImageBitmap = bitmap;
postInvalidate();
}
public void setParticleColor(int particleColor) {
this.particleColor = particleColor;
this.particleShader = null;
if(this.mParticlePaint != null){
this.mParticlePaint.setShader(null);
}
postInvalidate();
}
static class Particle {
private float alpha;
private float radius;
private int color = Color.TRANSPARENT;
private float x;
private float y;
private float speedx;
private float speedy;
private double alphaTransformer;
Particle(float x, float y, float radius, float alpha, float spx, float spy) {
init(x, y, radius, alpha, spx, spy);
}
private void init(float x, float y, float radius, float alpha, float spx, float spy) {
this.x = x;
this.y = y;
this.speedx = (float) ((Math.random() - 0.5) * spx);
this.speedy = (float) ((Math.random() - 0.5) * spy);
this.radius = radius;
this.alpha = alpha;
this.alphaTransformer = 0.005 + 0.005 * Math.random();
// this.color = argb(1f, (float) Math.random(), (float) Math.random(), (float) Math.random());
}
void update() {
this.alpha -= alphaTransformer;
this.x += this.speedx;
this.y += this.speedy;
}
void draw(Canvas canvas, Paint paint, Matrix matrix) {
Shader shader = paint.getShader();
if(shader == null){
paint.setColor(color);
}else {
if(matrix == null){
matrix = new Matrix();
}else{
matrix.reset();
}
matrix.postTranslate(this.x,this.y);
shader.setLocalMatrix(matrix);
}
paint.setAlpha((int) (this.alpha * 255));
canvas.drawCircle(this.x, this.y, this.radius, paint);
}
public void reset(float x, float y, float radius, float alpha, float spx, float spy) {
init(x, y, radius, alpha, spx, spy);
}
}
}
四、总结
之前我们的一篇是 《Android 自定义队列动画》,专门从细节点和公式上处理了循环问题,这次使用Animator就不需要考虑这种问题了,相比来说更加简单直观。