一、效果图
动画场景:该动画是在ImageView中显示,效果图中显示了三个动画:漂浮、左右招手,原动画是很流畅的,无丢帧,转成gif后大致能看出效果,不用在意卡顿。
二、实现原理
1.自定义Drawable,重写draw,根据onAnimationUpdate方法返回的动画进度,通过差值器计算出该帧drawable的状态,并绘制
2.调用Drawable.invalidateSelf触发View的重绘
三、代码分析
1.代码结构
2.MainActivity:测试动画Activity,无复杂逻辑
private void startAnimator(int type) { cancelAnimator(); switch (type) { case TYPE_START: mAnimator = new StartRobotAnimator(this.getApplicationContext()); break; case TYPE_WAVE_LEFT: mAnimator = new WaveRobotAnimator(this.getApplicationContext(), true); break; case TYPE_WAVE_RIGHT: mAnimator = new WaveRobotAnimator(this.getApplicationContext(), false); break; default: mAnimator = new StartRobotAnimator(this.getApplicationContext()); break; } mImageView.setImageDrawable(mAnimator.getDrawable()); mAnimator.start(); } private void cancelAnimator() { if (mAnimator != null) { mAnimator.cancel(); mAnimator = null; } }
提供两个方法:开启/关闭动画
3.StartRobotAnimator:动画实现类
继承ValueAnimator,定义内部类继承Drawable
上图为StartRobotAnimator类结构,内部有较多的内部类:RobotDrawable、Body、EyeLefe...
也容易理解,Robots动画内部是多个元素在动,眼睛、身体、头、左右手。每个元素对应一个内部类。这些内部类都继承Basic。Basic中保存这些元素相同的属性。
动画绘制流程:
从ValueAnimator.start-> onAnimationUpdate->
Drawable.update
Drawable.onDraw.invalidateSelf -> draw
在update时,将动画进度传给Drawable,代码片段如下:
@Override public void onAnimationUpdate(ValueAnimator animation) { mDrawable.update((int) (animation.getAnimatedValue())); mDrawable.invalidateSelf(); }
接着进入自定义Drawable的draw方法:
@Override public void draw(@NonNull Canvas canvas) { if (mWidth == 0 || mHeight == 0) { mWidth = canvas.getWidth(); mHeight = canvas.getHeight(); } canvas.save(); canvas.translate(mWidth / 2f - 126 / 2, mHeight / 2f - 164 / 2);//平移画布,保证绘制的robots在imageView中居中 float T0 = DURATION; float t = Math.min(mTime, DURATION); double translateY = (-30f * t / T0 + 30) * Math.sin(3.5 * Math.PI / T0 * t + Math.PI / 2f); canvas.translate(0, (float) translateY); //绘制头、嘴巴、左右手 mHead.draw(canvas); mMouth.draw(canvas, mTime); mHandLeft.draw(canvas, mTime); mHandRight.draw(canvas, mTime); //绘制身体、左右眼睛 mBody.draw(canvas); mEyeLeft.draw(canvas, mTime); mEyeRight.draw(canvas, mTime); canvas.restore(); }
draw方法和View内部逻辑关系比较大,原理很简单,可以自己理解。按照下面的思路理解可能更方便记忆:
对画布整体做动画使用canvas.translate/rotate/scale/alpha
对某一元素做动画使用Basic类中的Matrix。
对于Matrix,可认为有三个方法,set、pre、post。set:清空矩阵之前的设置,重置矩阵 pre:左乘 post:右乘
比如要实现先平移再旋转的效果,可以setTranslate+postRotate 也可以setRotate+preTranslte。
四、源码
附上MainActivity和自定义Animator源码
package com.zmh.animation.robots; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.ImageView; public class MainActivity extends Activity { private ImageView mImageView; private IRobotsAnimator mAnimator; private final static int TYPE_START = 0x1; private final static int TYPE_WAVE_LEFT = 0x2; private final static int TYPE_WAVE_RIGHT = 0x3; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mImageView = findViewById(R.id.img); mAnimator = new StartRobotAnimator(this.getApplicationContext()); mImageView.setImageDrawable(mAnimator.getDrawable()); findViewById(R.id.btn_wave_left).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAnimator(TYPE_WAVE_LEFT); } }); findViewById(R.id.btn_start).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAnimator(TYPE_START); } }); findViewById(R.id.btn_wave_right).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAnimator(TYPE_WAVE_RIGHT); } }); } private void startAnimator(int type) { cancelAnimator(); switch (type) { case TYPE_START: mAnimator = new StartRobotAnimator(this.getApplicationContext()); break; case TYPE_WAVE_LEFT: mAnimator = new WaveRobotAnimator(this.getApplicationContext(), true); break; case TYPE_WAVE_RIGHT: mAnimator = new WaveRobotAnimator(this.getApplicationContext(), false); break; default: mAnimator = new StartRobotAnimator(this.getApplicationContext()); break; } mImageView.setImageDrawable(mAnimator.getDrawable()); mAnimator.start(); } private void cancelAnimator() { if (mAnimator != null) { mAnimator.cancel(); mAnimator = null; } } @Override protected void onDestroy() { super.onDestroy(); cancelAnimator(); } }
package com.zmh.animation.robots; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; class StartRobotAnimator extends ValueAnimator implements IRobotsAnimator, ValueAnimator.AnimatorUpdateListener { private RobotDrawable mDrawable; private int DURATION = 4000; StartRobotAnimator(Context context) { this.setDuration(DURATION); this.setIntValues(0, DURATION); this.setInterpolator(new LinearInterpolator()); mDrawable = new RobotDrawable(context); addUpdateListener(this); } @Override public void onAnimationUpdate(ValueAnimator animation) { mDrawable.update((int) (animation.getAnimatedValue())); mDrawable.invalidateSelf(); } @Override public Drawable getDrawable() { return mDrawable; } private class RobotDrawable extends Drawable { private int mTime; private Body mBody; private EyeLeft mEyeLeft; private EyeRight mEyeRight; private HandLeft mHandLeft; private HandRight mHandRight; private Head mHead; private Mouth mMouth; private int mWidth; private int mHeight; RobotDrawable(Context context) { mBody = new Body(context); mEyeLeft = new EyeLeft(context); mEyeRight = new EyeRight(context); mHandLeft = new HandLeft(context); mHandRight = new HandRight(context); mHead = new Head(context); mMouth = new Mouth(context); } @Override public void setAlpha(int alpha) { } private void update(int time) { mTime = time; } @Override public void draw(@NonNull Canvas canvas) { if (mWidth == 0 || mHeight == 0) { mWidth = canvas.getWidth(); mHeight = canvas.getHeight(); } canvas.save(); canvas.translate(mWidth / 2f - 126 / 2, mHeight / 2f - 164 / 2); float T0 = DURATION; float t = Math.min(mTime, DURATION); double translateY = (-30f * t / T0 + 30) * Math.sin(3.5 * Math.PI / T0 * t + Math.PI / 2f); canvas.translate(0, (float) translateY); mHead.draw(canvas); mMouth.draw(canvas, mTime); mHandLeft.draw(canvas, mTime); mHandRight.draw(canvas, mTime); mBody.draw(canvas); mEyeLeft.draw(canvas, mTime); mEyeRight.draw(canvas, mTime); canvas.restore(); } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } } private class Body extends Basic { Body(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.body); mBitmap = Bitmap.createScaledBitmap(mBitmap, 57, 59, true); } private void draw(Canvas canvas) { mMatrix.setTranslate(34, 103); canvas.drawBitmap(mBitmap, mMatrix, null); } } private class EyeLeft extends Eye { EyeLeft(Context context) { mTransX = 28; mTransY = 57; mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.eye_left); mBitmap = Bitmap.createScaledBitmap(mBitmap, 18, 22, true); mBmpSmall = BitmapFactory.decodeResource(context.getResources(), R.drawable.small_eye_left); mBmpSmall = Bitmap.createScaledBitmap(mBmpSmall, 17, 5, true); } } private class EyeRight extends Eye { EyeRight(Context context) { mTransX = 80; mTransY = 57; mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.eye_right); mBitmap = Bitmap.createScaledBitmap(mBitmap, 18, 22, true); mBmpSmall = BitmapFactory.decodeResource(context.getResources(), R.drawable.small_eye_right); mBmpSmall = Bitmap.createScaledBitmap(mBmpSmall, 17, 5, true); } } class Eye extends Basic { Bitmap mBmpSmall; Matrix mSmall = new Matrix(); final float[] mTimes = {50f, 200f}; final float[] mProgress = {1f, 0.15f, 1f}; final Interpolator[] mInterpolator = { new PathInterpolator(0.33f, 0.00f, 0.67f, 1.00f), new PathInterpolator(0.33f, 0.00f, 0.67f, 1.00f) }; Interpolator mStartInterpolator = new PathInterpolator(0.03f, 0.59f, 0.49f, 1.00f); int mTransX; int mTransY; void draw(Canvas canvas, int time) { mMatrix.setTranslate(mTransX, mTransY); mSmall.setTranslate(mTransX + 1, mTransY + mBitmap.getHeight() / 2f + 1); if (time <= 350) { float progress = mStartInterpolator.getInterpolation(time / 350f); mMatrix.postScale(1, progress, mTransX + mBitmap.getWidth() / 2f, mTransY + mBitmap.getHeight() / 2f); canvas.drawBitmap(mBmpSmall, mSmall, null); } if (time >= 2350 && time <= 2600) { float scale = getProgress(time - 2350, mTimes, mProgress, mInterpolator); mMatrix.postScale(1, scale, mTransX + mBitmap.getWidth() / 2f, mTransY + mBitmap.getHeight() / 2f); } canvas.drawBitmap(mBitmap, mMatrix, null); } } private class HandLeft extends Basic { private Interpolator interpolator = new PathInterpolator(0.21f, 0.00f, 0.29f, 1.00f); private int DURATION = 750; HandLeft(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.hand_left); mBitmap = Bitmap.createScaledBitmap(mBitmap, 19, 27, true); } private void draw(Canvas canvas, int time) { time = Math.min(time, DURATION); float progress = interpolator.getInterpolation(time * 1f / DURATION) * 60f - 60f; mMatrix.setTranslate(16, 116); mMatrix.postRotate(progress, 37, 114); canvas.drawBitmap(mBitmap, mMatrix, null); } } private class HandRight extends Basic { private Interpolator interpolator = new PathInterpolator(0.21f, 0.00f, 0.29f, 1.00f); private int DURATION = 750; HandRight(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.hand_right); mBitmap = Bitmap.createScaledBitmap(mBitmap, 19, 27, true); } private void draw(Canvas canvas, int time) { time = Math.min(time, DURATION); float progress = 60f - interpolator.getInterpolation(time * 1f / DURATION) * 60f; mMatrix.setTranslate(90, 116); mMatrix.postRotate(progress, 88, 114); canvas.drawBitmap(mBitmap, mMatrix, null); } } private class Head extends Basic { Head(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.head); mBitmap = Bitmap.createScaledBitmap(mBitmap, 126, 103, true); } private void draw(Canvas canvas) { canvas.drawBitmap(mBitmap, mMatrix, null); } } private class Mouth extends Basic { private final Paint mPaint = new Paint(); Mouth(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.mouth); mBitmap = Bitmap.createScaledBitmap(mBitmap, 12, 8, true); } private void draw(Canvas canvas, int time) { time = Math.min(Math.max(0, time - 200), 150); mPaint.setAlpha(255 * time / 150); mMatrix.setTranslate(57, 81); canvas.drawBitmap(mBitmap, mMatrix, mPaint); } } private class Basic { Bitmap mBitmap; Matrix mMatrix = new Matrix(); float getProgress(int time, float[] times, float[] progresss, Interpolator[] interpolators) { float progress = 0f; float t; if (time <= times[0]) { t = 1f * time / times[0]; progress = progresss[0] + interpolators[0].getInterpolation(t) * (progresss[1] - progresss[0]); } else if (time <= times[0] + times[1]) { t = 1f * (time - times[0]) / times[1]; progress = progresss[1] + interpolators[1].getInterpolation(t) * (progresss[2] - progresss[1]); } else if (time <= times[0] + times[1] + times[2]) { t = 1f * (time - times[1] - times[0]) / times[2]; progress = progresss[2] + interpolators[2].getInterpolation(t) * (progresss[3] - progresss[2]); } else if (time <= times[0] + times[1] + times[2] + times[3]) { t = 1f * (time - times[2] - times[1] - times[0]) / times[3]; progress = progresss[3] + interpolators[3].getInterpolation(t) * (0 - progresss[3]); } return progress; } } }
package com.zmh.animation.robots; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; class WaveRobotAnimator extends ValueAnimator implements IRobotsAnimator, ValueAnimator.AnimatorUpdateListener { private RobotDrawable mDrawable; WaveRobotAnimator(Context context, boolean left) { this.setDuration(1700); this.setIntValues(0, 1700); this.setInterpolator(new LinearInterpolator()); mDrawable = new RobotDrawable(context, left); addUpdateListener(this); } @Override public void onAnimationUpdate(ValueAnimator animation) { mDrawable.update((int) (animation.getAnimatedValue())); mDrawable.invalidateSelf(); } @Override public Drawable getDrawable() { return mDrawable; } private class RobotDrawable extends Drawable { private int mTime; private Body mBody; private EyeLeft mEyeLeft; private EyeRight mEyeRight; private HandLeft mHandLeft; private HandRight mHandRight; private Head mHead; private Mouth mMouth; private int mWidth; private int mHeight; private boolean mIsLeft; RobotDrawable(Context context, boolean left) { mIsLeft = left; mBody = new Body(context); mEyeLeft = new EyeLeft(context); mEyeRight = new EyeRight(context); mHandLeft = new HandLeft(context); mHandRight = new HandRight(context); mHead = new Head(context); mMouth = new Mouth(context); } @Override public void setAlpha(int alpha) { } private void update(int time) { mTime = time; } @Override public void draw(@NonNull Canvas canvas) { if (mWidth == 0 || mHeight == 0) { mWidth = canvas.getWidth(); mHeight = canvas.getHeight(); } canvas.save(); if (mIsLeft) { canvas.scale(-1, 1, canvas.getWidth() / 2f, canvas.getHeight() / 2f); } canvas.translate(mWidth / 2f - 126 / 2, mHeight / 2f - 164 / 2); float bodyProgress = mBody.getProgress(mTime); canvas.rotate(bodyProgress, 63, 164); canvas.save(); float headProgress = mHead.getProgress(mTime); canvas.rotate(headProgress, 63, 103); mHead.draw(canvas); mEyeLeft.draw(canvas, mTime); mEyeRight.draw(canvas, mTime); mMouth.draw(canvas); canvas.restore(); mBody.draw(canvas); mHandLeft.draw(canvas, mTime); mHandRight.draw(canvas, mTime); canvas.restore(); } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } } private class Body extends Basic { private final Interpolator[] mInterpolator = { new PathInterpolator(0.48f, 0.06f, 0.52f, 0.94f), new PathInterpolator(0.47f, 0.00f, 0.77f, 0.63f), new PathInterpolator(0.34f, -0.68f, 0.55f, -1.14f), new PathInterpolator(0.48f, 0.00f, 0.52f, 1.00f) }; private final float[] mProgress = {0f, 18f, 17.7f, 18.0f}; Body(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.body); mBitmap = Bitmap.createScaledBitmap(mBitmap, 57, 59, true); } private void draw(Canvas canvas) { mMatrix.setTranslate(34, 103); canvas.drawBitmap(mBitmap, mMatrix, null); } float getProgress(int time) { return getProgress(time, mTimes, mProgress, mInterpolator); } } private class EyeLeft extends Eye { EyeLeft(Context context) { mTransX = 28; mTransY = 57; mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.eye_left); mBitmap = Bitmap.createScaledBitmap(mBitmap, 18, 22, true); } } private class EyeRight extends Eye { EyeRight(Context context) { mTransX = 80; mTransY = 57; mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.eye_right); mBitmap = Bitmap.createScaledBitmap(mBitmap, 18, 22, true); } } class Eye extends Basic { final float[] mTimes = {50f, 200f}; final float[] mProgress = {1f, 0.15f, 1f}; final Interpolator[] mInterpolator = { new PathInterpolator(0.33f, 0.00f, 0.67f, 1.00f), new PathInterpolator(0.33f, 0.00f, 0.67f, 1.00f) }; int mTransX; int mTransY; void draw(Canvas canvas, int time) { mMatrix.setTranslate(mTransX, mTransY); if (time >= 1000 && time <= 1250) { float scale = getProgress(time - 1000, mTimes, mProgress, mInterpolator); mMatrix.postScale(1, scale, mTransX + mBitmap.getWidth() / 2f, mTransY + mBitmap.getHeight() / 2f); } canvas.drawBitmap(mBitmap, mMatrix, null); } } private class HandLeft extends Basic { private final float[] mProgress = {0f, 116f, 80f, 116f}; private final Interpolator[] mInterpolator = { new PathInterpolator(0.44f, 0.20f, 0.71f, 1.00f), new PathInterpolator(0.48f, 0.00f, 0.44f, 1.00f), new PathInterpolator(0.53f, 0.00f, 0.61f, 1.00f), new PathInterpolator(0.17f, 0.00f, 0.52f, 1.00f) }; HandLeft(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.hand_left); mBitmap = Bitmap.createScaledBitmap(mBitmap, 19, 27, true); } private void draw(Canvas canvas, int time) { float progress = getProgress(time, mTimes, mProgress, mInterpolator); mMatrix.setTranslate(16, 116); mMatrix.postRotate(progress, 37, 114); canvas.drawBitmap(mBitmap, mMatrix, null); } } private class HandRight extends Basic { private final float[] mProgress = {0f, -25f, -24.6f, -25f}; private final Interpolator[] mInterpolator = { new PathInterpolator(0.48f, 0.06f, 0.52f, 0.94f), new PathInterpolator(0.47f, 0.00f, 0.77f, 0.63f), new PathInterpolator(0.34f, -0.68f, 0.55f, -1.14f), new PathInterpolator(0.48f, 0.00f, 0.52f, 1.00f) }; HandRight(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.hand_right); mBitmap = Bitmap.createScaledBitmap(mBitmap, 19, 27, true); } private void draw(Canvas canvas, int time) { float progress = getProgress(time, mTimes, mProgress, mInterpolator); mMatrix.setTranslate(90, 116); mMatrix.postRotate(progress, 88, 114); canvas.drawBitmap(mBitmap, mMatrix, null); } } private class Head extends Basic { private final float[] mProgress = {0f, 18f, 17.7f, 18f}; private final Interpolator[] mInterpolator = { new PathInterpolator(0.48f, 0.60f, 0.52f, 0.94f), new PathInterpolator(0.47f, 0.00f, 0.77f, 0.63f), new PathInterpolator(0.34f, -0.68f, 0.55f, -1.14f), new PathInterpolator(0.48f, 0.00f, 0.52f, 1.00f) }; Head(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.head); mBitmap = Bitmap.createScaledBitmap(mBitmap, 126, 103, true); } private void draw(Canvas canvas) { canvas.drawBitmap(mBitmap, mMatrix, null); } float getProgress(int time) { return getProgress(time, mTimes, mProgress, mInterpolator); } } private class Mouth extends Basic { Mouth(Context context) { mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.mouth); mBitmap = Bitmap.createScaledBitmap(mBitmap, 12, 8, true); } private void draw(Canvas canvas) { mMatrix.setTranslate(57, 81); canvas.drawBitmap(mBitmap, mMatrix, null); } } private class Basic { final float[] mTimes = {700f, 200f, 250f, 550f}; Bitmap mBitmap; Matrix mMatrix = new Matrix(); float getProgress(int time, float[] times, float[] progresss, Interpolator[] interpolators) { float progress = 0f; float t; if (time <= times[0]) { t = 1f * time / times[0]; progress = progresss[0] + interpolators[0].getInterpolation(t) * (progresss[1] - progresss[0]); } else if (time <= times[0] + times[1]) { t = 1f * (time - times[0]) / times[1]; progress = progresss[1] + interpolators[1].getInterpolation(t) * (progresss[2] - progresss[1]); } else if (time <= times[0] + times[1] + times[2]) { t = 1f * (time - times[1] - times[0]) / times[2]; progress = progresss[2] + interpolators[2].getInterpolation(t) * (progresss[3] - progresss[2]); } else if (time <= times[0] + times[1] + times[2] + times[3]) { t = 1f * (time - times[2] - times[1] - times[0]) / times[3]; progress = progresss[3] + interpolators[3].getInterpolation(t) * (0 - progresss[3]); } return progress; } } }
五、项目源码
github:https://github.com/ZhangMingHua001/Robots
csdn: