系列中其他文章:

【Android进阶】如何写一个很屌的动画(1)—先实现一个简易的自定义动画框架

【Android进阶】如何写一个很屌的动画(2)—动画的好帮手们

【Android进阶】如何写一个很屌的动画(3)—高仿腾讯手机管家火箭动画

文章中充满了很多很大的Gif图,请耐心等待加载或者刷新页面,谢谢~

前两节我介绍了一些写好一个动画的要素,这节我就用一个实例详细介绍如何一步一步写好一个动画。
本次实例是要高仿腾讯手机管家火箭的动画,就是下图:

这个动画不算复杂,但是实现起来也不简单,来练练手正好。
再看看我已经做好的效果:

感觉还不错吧,虽然有所差异,但是整体动画效果还是差不多的。具体可以自行下载手机管家和运行我的源码进行对比。

几点说明:
1、本实例所有图片素材来源都是从手机管家安装包解压出来的,我的用途仅仅用于学习交流;
2、此动画在手机管家里是用悬浮窗实现的,关于悬浮窗相关的知识可自行度娘;而我的实现只是用了一个透明主题的Activity,两者是存在一定差异的。不过由于本文主要介绍动画方面的知识,所以此差异可以无视;
3、手机管家中不仅仅只有这么少的动画,还有一些卫星等其他动画,这部分不是本文重点,所以可以无视;

实现复杂动画的要诀
任何复杂的动画都是由N个单一的简单动画组成的,所以在写一个复杂的动画的时候,先把动画拆分,然后一个一个小动画实现,积少成多,慢慢的就会组合成一个很屌的动画。

分析动画
为了更好地了解手机管家那个动画是如何设计的,可以自行到市场下载一个最新版,反复观看动画了解细节。
我们先来拆分下动画,整个动画由五个部分组成:1、火箭 2、下面的发射台 3、发射台上面的火花 4、起飞之后的雾霾 5、仔细看会发现雾霾是有一条小雾霾单独分开的

并且整个动画的逻辑是这样:
1、火箭是可拖动的;
2、发射台初始时比较暗,而且偏下;
3、当火箭拖动到离发射台比较近的位置时,发射台会变亮,并且有个上升的动画,同时火花出现;相反,如果原理发射台,发射台会变暗,会有一个下降的动画,同时火花消失就是下图这样:
4、当把火箭拖动到可以起飞的位置时,火花会加速转动;
5、当把火箭拖动到可以起飞的位置并释放手指时,飞机从中间起飞,同时发射台消失,出现雾霾;

整个流程大概就是这样,只要我们一步一步实现,实现这个动画也不难。既然目标明确了,那就动手开始写吧!

动画框架
实现这个动画我用的是在第一节所介绍的那个自定义动画框架。这个框架是整个动画实现的基础,所以这里再重新详细说说如何设计这个框架的,框架的源码最下面有下载地址,其中com.example.animdemo.anim包是整个主体框架。

动画载体:
因为动画是需要在一个Canvas上面绘制出来的,所以它需要一个载体。在这个框架中,动画的载体是一个直接继承View的AnimView或者是一个直接继承SurfaceView的AnimSurfaceView,两个都有实现,要用哪个自行选择,我推荐用AnimView,因为支持硬件加速的View绘图效率甚至比不支持硬件加速的SurfaceView要好。

/*** 用于动画绘图的View* * @author zhanghuijun* */
public class AnimView extends View implements IAnimFrameListener, IAnimView {public AnimView(Context context, AttributeSet attrs) {super(context, attrs);}public AnimView(Context context) {super(context);}/*** 绘制*/@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);}}

动画驱动:
在第一节已经分析过了,要让一个动画动起来,需要一个“动画驱动”驱动AnimView不断地绘制新的界面。框架中,这个“驱动”由AnimFrameController类负责,它的实现很简单,就是在AnimView一次绘制结束后,也就是onDraw的最后,计算下一帧的绘制时间,然后延迟这段时间去调用invalidate触发绘制。

/*** 用于动画绘图的View* * @author zhanghuijun* */
public class AnimView extends View implements IAnimFrameListener {/*** 是否已经测量完成*/protected boolean mHadSize = false;/*** 宽高*/protected int mWidth = 0;protected int mHeight = 0;/*** 动画帧控制器*/protected AnimFrameController mAnimFrameController = null;public AnimView(Context context, AttributeSet attrs) {super(context, attrs);init();}public AnimView(Context context) {super(context);init();}/*** 初始化*/protected void init() {// 获取主线程的Looper,即发送给该Handler的都在主线程执行mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper());}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mHadSize = true;mWidth = w;     // 其实就等于getMeasuredWidth()和getMeasuredHeight()mHeight = h;start();}@Overrideprotected void onWindowVisibilityChanged(int visibility) {super.onWindowVisibilityChanged(visibility);if (visibility == View.VISIBLE) {if (mHadSize) {start();}} else {stop();}}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();stop();}/*** 开始*/@Overridepublic void start() {mAnimFrameController.start();}/*** 停止*/@Overridepublic void stop() {mAnimFrameController.stop();}/*** 设置帧频*/@Overridepublic void setFtp(int ftp) {mAnimFrameController.setFtp(ftp);}/*** 绘制*/@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mAnimFrameController.updateFrame();}@Overridepublic void onUpdateFrame() {invalidate();}}/*** 控制动画帧,单独一个模块* @author zhanghuijun*/
public class AnimFrameController {public static final String TAG = "AnimDemo AnimFrameController";/*** 是否已经开始绘制*/private boolean mIsStart = false;/*** 绘制Handler,主线程的Handler*/private Handler mDrawHandler = null;/*** 上次绘制时间*/private long mLastDrawBeginTime = 0l;/*** 帧频,默认三十帧*/private int mFtp = 30;/*** 刷新帧时间,默认三十帧*/private long mIntervalTime = 1000 / 30;/*** 统计帧频所用*/private int mFrameCount = 0;private long mStartTime = 0l;/*** IAnimFrameCallback*/private IAnimFrameListener mListener = null;/*** 构造器*/public AnimFrameController(IAnimFrameListener listener, Looper threadLooper) {if (listener == null) {throw new RuntimeException("AnimFrameController 构造参数listener 不能为null");}mListener = listener;mDrawHandler = new Handler(threadLooper);}/*** 开始渲染绘制动画*/public void start() {if (!mIsStart) {mIsStart = true;mDrawHandler.post(mUpdateFrame);}}/*** 停止渲染绘制动画*/public void stop() {if (mIsStart) {mIsStart = false;}}/*** 设置帧频,理想值,一般没那么精准*/public void setFtp(int ftp) {if (ftp > 0) {mFtp = ftp;mIntervalTime = 1000 / mFtp;}}/*** 在每帧更新完毕时调用*/public void updateFrame() {// 计算需要延迟的时间long passTime = System.currentTimeMillis() - mLastDrawBeginTime;final long delayTime = mIntervalTime - passTime;// 延迟一定时间去绘制下一帧if (delayTime > 0) {mDrawHandler.postDelayed(mUpdateFrame, delayTime);} else {mDrawHandler.post(mUpdateFrame);}// 统计帧频,如是未开始计时, 或帧时间太长(可能是由于动画暂时停止了,需要忽略这次计数据)则重置开始if (mStartTime == 0 || System.currentTimeMillis() - mStartTime >= 1100) {mStartTime = System.currentTimeMillis();mFrameCount = 0;} else {mFrameCount++;if (System.currentTimeMillis() - mStartTime >= 1000) {Log.d(TAG, "帧频为 : " + mFrameCount + " 帧一秒 ");mStartTime = System.currentTimeMillis();;mFrameCount = 0;}}}/*** 刷新帧Runnable*/private final Runnable mUpdateFrame = new Runnable() {@Overridepublic void run() {if (!mIsStart) {return;}// 记录时间,每帧开始更新的时间mLastDrawBeginTime = System.currentTimeMillis();// 通知界面绘制帧mListener.onUpdateFrame();}};/*** 动画View要实现的接口*/public interface IAnimFrameListener {/*** 需要刷新帧*/public void onUpdateFrame();/*** 设置帧频*/public void setFtp(int ftp);}
}

在设计代码时,为了把功能更好的区分,降低耦合,所以驱动AnimFrameController类中,一点关于动画绘制的代码都没有,所有绘制代码都交给AnimView,而所有跟“动画驱动”的代码都由AnimFrameController负责,AnimView仅仅调用即可。

如果有研究过Scroller源码的朋友们,你会发现上面这个“驱动”与Scroller有异曲同工之处。Scroller是需要结合computeScroll()使用,computeScroll()是在draw()的时候调用,也就是每次重新绘制都会调用;而Scroller中计算偏移值的实现也是通过计算当前时间的偏移值来计算的,具体请看源码Scroller.computeScrollOffset()。这两者都没有用任何与定时器相关的代码来推算动画时间。

动画元素:
框架中,每个绘制的动画元素都需要继承一个基类AnimObject。哪些是动画元素呢?像手机管家那个动画中,那五个部分(火箭,发射台等)都是一个单一的动画元素。写看看AnimObject的定义:

/*** 动画绘制基础接口* @author zhanghuijun**/
public class AnimObject {/*** 是否需要绘制*/private boolean mIsNeedDraw = true;/*** 父AnimObject*/private AnimObjectGroup mParent = null;/*** 根AnimView*/private View mRootAnimView = null;/*** 整个动画场景的宽高*/private int mSceneWidth = 0;private int mSceneHeight = 0;/*** Context*/private Context mContext = null;public AnimObject(View mRootAnimView, Context mContext) {this.mRootAnimView = mRootAnimView;this.mContext = mContext;mSceneWidth = ((IAnimView) mRootAnimView).getAnimSceneWidth();mSceneHeight = ((IAnimView) mRootAnimView).getAnimSceneHeight();}/*** 绘制*/public void draw(Canvas canvas, int sceneWidth, int sceneHeight) {}/*** 逻辑*/public void logic(long animTime, long deltaTime) {}/*** 动画场景大小改变*/public void onSizeChange(int w, int h) {mSceneWidth = w;mSceneHeight = h;}
}

一般情况下,只要重写logic()和draw()方法即可,logic()方法会在计算逻辑,也就是绘制之前被调用,而draw()方法就是在AnimView绘制的时候调用。logic()里需要实现这个动画元素的业务逻辑部分,而draw()就是要把这个动画元素画出来。具体它们是怎样调用的,请往下看。

除了AnimObject,还有一个AnimObjectGroup的类,该类的作用就是统筹同一个Group里的所有动画元素,模块化区分。这两者的关系与View/ViewGruop有点类似,使用时参照使用即可。来看看AnimObjectGroup的定义:

/*** 负责绘制一组AnimObject* @author zhanghuijun**/
public class AnimObjectGroup extends AnimObject {/*** 一组AnimObject*/protected List<AnimObject> mAnimObjects = null;public AnimObjectGroup(View mRootAnimView, Context mContext) {super(mRootAnimView, mContext);mAnimObjects = new ArrayList<AnimObject>();}/*** 添加一个AnimObject*/public void addAnimObject(AnimObject object) {object.setParent(this);mAnimObjects.add(object);}/*** 移除一个AnimObject*/public void removeAnimObject(AnimObject object) {mAnimObjects.remove(object);}@Overridepublic void logic(long animTime, long deltaTime) {// 按顺序执行AnimObject的逻辑for (int i = 0; i < mAnimObjects.size(); i++) {mAnimObjects.get(i).logic(animTime, deltaTime);}}@Overridepublic void draw(Canvas canvas, int sceneWidth, int sceneHeight) {// 按顺序绘制AnimObjectfor (int i = 0; i < mAnimObjects.size(); i++) {mAnimObjects.get(i).draw(canvas, sceneWidth, sceneHeight);}}@Overridepublic void onSizeChange(int w, int h) {for (int i = 0; i < mAnimObjects.size(); i++) {mAnimObjects.get(i).onSizeChange(w, h);}}/*** 获取子Object*/public List<AnimObject> getAnimObjects() {return mAnimObjects;}
}

你会发现,AnimObjectGroup其实也是继承AnimObject,跟ViewGruop继承View很类似。同时用mAnimObjects来保存所有子AnimObject,也就是ViewGruop里面的mChildren变量。它也实现了logic()和draw()方法,具体的实现就是遍历所有子AnimObject,分别调用它们的logic()和draw()。这一点也跟ViewGruop很像。

那么AnimObjectGroup的logic()和draw()又是哪里调用呢?那就是根View—AnimView。我们把AnimView补充上对AnimObjectGroup的操作。

/*** 用于动画绘图的View* * @author zhanghuijun* */
public class AnimView extends View implements IAnimFrameListener, IAnimView {/*** 是否已经测量完成*/protected boolean mHadSize = false;/*** 宽高*/protected int mWidth = 0;protected int mHeight = 0;/*** 一组AnimObjectGroup*/protected List<AnimObjectGroup> mAnimObjectGroups = null;/*** 动画帧控制器*/protected AnimFrameController mAnimFrameController = null;/*** 动画时钟*/protected AnimClock mAnimClock = null;public AnimView(Context context, AttributeSet attrs) {super(context, attrs);init();}public AnimView(Context context) {super(context);init();}/*** 初始化*/protected void init() {// 获取主线程的Looper,即发送给该Handler的都在主线程执行mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper());mAnimObjectGroups = new ArrayList<AnimObjectGroup>();mAnimClock = new AnimClock();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mHadSize = true;mWidth = w;     // 其实就等于getMeasuredWidth()和getMeasuredHeight()mHeight = h;for (int i = 0; i < mAnimObjectGroups.size(); i++) {mAnimObjectGroups.get(i).onSizeChange(w, h);}start();}@Overrideprotected void onWindowVisibilityChanged(int visibility) {super.onWindowVisibilityChanged(visibility);if (visibility == View.VISIBLE) {if (mHadSize) {start();}} else {stop();}}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();stop();}/*** 开始*/@Overridepublic void start() {mAnimFrameController.start();mAnimClock.start();}/*** 停止*/@Overridepublic void stop() {mAnimFrameController.stop();}/*** 添加一个AnimObjectGroup*/@Overridepublic void addAnimObjectGroup(AnimObjectGroup group) {mAnimObjectGroups.add(group);}/*** 移除一个AnimObjectGroup*/@Overridepublic void removeAnimObjectGroup(AnimObjectGroup group) {mAnimObjectGroups.remove(group);}@Overridepublic int getAnimSceneWidth() {return mWidth;}@Overridepublic int getAnimSceneHeight() {return mHeight;}/*** 设置帧频*/@Overridepublic void setFtp(int ftp) {mAnimFrameController.setFtp(ftp);}/*** 绘制*/@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// 逻辑for (int i = 0; i < mAnimObjectGroups.size(); i++) {mAnimObjectGroups.get(i).logic(mAnimClock.getAnimTime(), mAnimClock.getDeltaTime());}// 绘制for (int i = 0; i < mAnimObjectGroups.size(); i++) {mAnimObjectGroups.get(i).draw(canvas, mWidth, mHeight);}mAnimFrameController.updateFrame();mAnimClock.updateFrame();}@Overridepublic void onUpdateFrame() {invalidate();}}

你会发现,AnimView里有一个mAnimObjectGroups变量保存这所有AnimObjectGroup,然后在onDraw()中,首先调用AnimObjectGroup的logic()方法,然后再在调用AnimObjectGroup的draw()方法,这样一来,所有动画元素的logic()和draw()都被串联起来了。

动画时间:
我还写了一个AnimClock的类,负责记录和管理整个动画过程流逝的时间。这个类有什么作用呢?
在动画框架中,因为自身已经提供一个“动画驱动”,所以想把系统Animation这种自身又有一个“驱动”的类很难结合一起用,但是我们可以抛弃Animation的“驱动”,用我们的驱动去驱动Animation,这样就可以在框架中使用Animation类。仔细阅读Animation的可以知道,它主要是通过时间的流逝来计算动画进行的程度,从而计算动画需要的数值;那么我们可以不经过它本身的流程来执行,也就是不用start,而是跳过start,直接用我们的动画时间去触发Animation的计算。这就是AnimClock具体就是这样:

    /*** 坐标*/private int mX, mY;private MyTransAnimation mTransAnimation = new MyTransAnimation();@Overridepublic void logic(long animTime, long deltaTime) {if (!mTransAnimation.hasStarted() || mTransAnimation.hasEnded()) {// 创建新的动画mTransAnimation.mStartX = mX;mTransAnimation.mStartY = mY;mTransAnimation.mEndX = 1000;mTransAnimation.mEndY = 1000;mTransAnimation.setStartTime(Animation.START_ON_FIRST_FRAME);mTransAnimation.setDuration(600);// 开始动画mTransAnimation.getTransformation(animTime, null);} else {// 已经在动画中mTransAnimation.getTransformation(animTime, null);}}/*** 平移动画*/class MyTransAnimation extends Animation {/*** 起始位置*/public int mStartX = 0;public int mStartY = 0;/*** 预期结束位置*/public int mEndX = 0;public int mEndY = 0;@Overrideprotected void applyTransformation(float interpolatedTime,Transformation t) {// 改变真实的位置坐标mX = (int) (mStartX + (mEndX - mStartX) * interpolatedTime);mY = (int) (mStartY + (mEndY - mStartY) * interpolatedTime);}}

首先,我们先定义一个所需要的Animation,然后在合适的时机初始化Animation,并且在每一帧的logic()实现中,通过主动调用mTransAnimation.getTransformation(animTime, null);传入我们的动画时间来触发Animation的计算。这样以来,Animation的进度就会和我们动画的进度一致,因为它的时间就是我们的动画时间。

以上,我们就把我们的动画框架搭建好了。下面会以这个框架为基础,来实现高仿手机管家火箭的动画。当然你也可以不需要用这个框架,只要你能很好的管理所有动画的元素就行。

实现火箭的点击和拖动
第一个难题来了!在动画中,火箭是直接绘制出来,它不是一个单独的View,所以它是没有权获取点击事件的。在动画框架中,有权获取到点击事件的只有AnimView,所以就需要在AnimView把点击事件分发下去。而不巧的是,框架一开始的设计没有点击事件分发部分,所以首先要把这块逻辑补充上。如果有时间可以完全模拟ViewGroup/View的事件分发逻辑,把几乎整套逻辑实现在这个框架上,这是最好的做法。但是这个做起来实在不简单,所以我写了一套比较简单的分发逻辑,也已经足够用很多动画场景了。(注意,该实现没有怎么测试过)具体实现可以看看本文的源码,com.example.animdemo.anim是原框架的代码,com.example.animdemo.anim.touch是扩展点击事件分发功能的代码。扩展后,需要点击事件的动画元素需要继承TouchAnimObject,并且它的Group要继承TouchAnimObjectGroup。

现在,我们就着手写代码吧,首先要画一个火箭(所有图片素材都是手机管家安装包解压出来的):

/*** 火箭* @author zhanghuijun**/
public class RocketObject extends TouchAnimObject {/*** 宽高*/private float mWidth = 0;private float mHeight = 0;/*** 坐标*/private int mX = 0;private int mY = 0;/*** 是否已经初始化*/private boolean mHasInit = false;/*** 图*/private Bitmap[] mRocketBmp = null;/*** 当前哪张图*/private int mBitmapIndex = 0;/*** 绘制Rect*/private Rect mSrcRect = null;private Rect mDstRect = null;/*** Paint*/private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);/*** 上一次动画时间*/private long mLastLogicAnimTime = 0l;public RocketObject(View mRootAnimView, Context mContext) {super(mRootAnimView, mContext);if (getAnimSceneWidth() != 0 && getAnimSceneHeight() != 0) {init();}}@Overridepublic void onSizeChange(int w, int h) {super.onSizeChange(w, h);init();}/*** 初始化*/private void init() {if (!mHasInit) {mRocketBmp = new Bitmap[] {BitmapFactory.decodeResource(getContext().getResources(), R.drawable.rocket_1),BitmapFactory.decodeResource(getContext().getResources(), R.drawable.rocket_2),};int mSceneWidth = getAnimSceneWidth();int mSceneHeight = getAnimSceneHeight();mWidth = mSceneWidth * 0.2f;    // 宽度是场景宽度的0.2mHeight = mWidth * 2.41f;       // 根据图片计算合理的高度mX = (int) ((mSceneWidth - mWidth) / 2);mY = mSceneHeight * 1 / 6;mSrcRect = new Rect(0, 0, mRocketBmp[mBitmapIndex].getWidth(), mRocketBmp[mBitmapIndex].getHeight());mDstRect = new Rect(mX, mY, (int) (mWidth + mX), (int) (mHeight + mY));mHasInit = true;}}@Overridepublic void logic(long animTime, long deltaTime) {if (mLastLogicAnimTime == 0l) {mLastLogicAnimTime = animTime;}// 每一定时间换一次图,产生动的效果long now = System.currentTimeMillis();if (now - mLastLogicAnimTime > 100) {mBitmapIndex++;mBitmapIndex = mBitmapIndex % mRocketBmp.length;mLastLogicAnimTime = now;}}@Overridepublic void draw(Canvas canvas, int sceneWidth, int sceneHeight) {if (mHasInit) {canvas.drawBitmap(mRocketBmp[mBitmapIndex], mSrcRect, mDstRect, mPaint);}}}

上面代码做出了一个火箭的动画元素,它每隔一定时间就会切换火箭图片素材,这些图片素材快速切换时会产生火箭动的效果,请看下图:

看到那个撩人的火焰小尾巴没?那就是用这两张图片快速切换做出来的动画效果。

现在火箭画出来了,但是它需要拖动,那么就要加上事件分发的代码:

    /*** 是否正在被点击中*/private boolean mIsInTouch = false;/*** 点击处的距离*/private int mTouchDisX = 0;private int mTouchDisY = 0;@Overridepublic boolean onTouch(MotionEvent event) {int action = event.getAction();int x = (int) event.getX();int y = (int) event.getY();switch (action) {case MotionEvent.ACTION_DOWN:if (isTouchIn(x, y)) {mTouchDisX = mX - x;mTouchDisY = mY - y;mIsInTouch = true;return true;}break;case MotionEvent.ACTION_MOVE:if (mIsInTouch) {mX = x + mTouchDisX;mY = y + mTouchDisY;mDstRect.set(mX, mY, (int) (mWidth + mX), (int) (mHeight + mY));return true;}break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:if (mIsInTouch) {mIsInTouch = false;return true;}break;default:break;}return false;}/*** 是否点击中了*/private boolean isTouchIn(int x , int y) {if (x >= mX && x <= mX + mWidth&& y >= mY && x <= mY + mHeight) {return true;}return false;}

在设计上跟普通View的事件分发一样,return true代表消耗该事件,同时用了mIsInTouch记录点击状态。另外,用了两个变量mTouchDisX和mTouchDisY来记录手指与火箭的偏移量,然后在拖动的时候同样加上这个偏移量,否则拖动的时候,火箭的位置会不正确。加上点击事件,请看效果:

OK,火箭部分暂时就这样。

绘制发射台
同样的,先给发射台创建一个动画类:

/*** 发射台*/
public class LaunchPadObject extends AnimObject {/*** 宽高*/private float mWidth = 0;private float mHeight = 0;/*** 坐标*/private int mX = 0;private int mY = 0;/*** 是否已经初始化*/private boolean mHasInit = false;/*** 图*/private Bitmap mBitmap = null;/*** 绘制Rect*/private Rect mSrcRect = null;private Rect mDstRect = null;/*** Paint*/private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);/*** ColorMatrixColorFilter*/private ColorMatrixColorFilter mColorMatrixColorFilter = null;/*** 火箭的Object*/private RocketObject mRocketObject = null;/*** 是否要高亮*/private boolean mIsHighLight = false;public LaunchPadObject(View mRootAnimView, Context mContext) {super(mRootAnimView, mContext);if (getAnimSceneWidth() != 0 && getAnimSceneHeight() != 0) {init();}}@Overridepublic void onSizeChange(int w, int h) {super.onSizeChange(w, h);init();}/*** 初始化*/private void init() {if (!mHasInit) {mBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.launch_pad);int mSceneWidth = getAnimSceneWidth();int mSceneHeight = getAnimSceneHeight();mWidth = mSceneWidth;   // 宽度mHeight = mWidth * 0.342f;      // 根据图片计算合理的高度mX = 0;         // 定义初始位置mY = (int) (mSceneHeight - mHeight * 0.845f);mSrcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());mDstRect = new Rect(mX, mY, (int) (mWidth + mX), (int) (mHeight + mY));// 使图片变暗的ColorMatrixColorMatrix colorMatrix = new ColorMatrix(new float[]{  0.5F, 0, 0, 0, 0,  0, 0.5F, 0, 0, 0,  0, 0, 0.5F, 0, 0,  0, 0, 0, 1, 0,  }); mColorMatrixColorFilter = new ColorMatrixColorFilter(colorMatrix);mHasInit = true;}}@Overridepublic void draw(Canvas canvas, int sceneWidth, int sceneHeight) {if (mHasInit) {if (mIsHighLight) {mPaint.setColorFilter(null);} else {mPaint.setColorFilter(mColorMatrixColorFilter);}canvas.drawBitmap(mBitmap, mSrcRect, mDstRect, mPaint);}}public void setRocketObject(RocketObject object) {mRocketObject = object;}}

这里的难点是发射台初始的时候颜色会比较暗色,那么问题来了,该怎么实现呢?
可能你会觉得,这个还不容易,直接在其上面画一层半透明黑色的遮罩不就行了吗?真的可以吗?下图就是这样做的效果:

你会发现,发射台所有部分都会被半透明黑色罩着,包括图片本身的透明部分。而真正想要的效果是,透明的继续透明,非透明的变暗黑色。这。。这不是强人所难吗?其实并不难,还记得上一节介绍的动画好帮手吗?里面有不少帮手可以做到这点。第一,我们可以用混合图像Xfermode,这个场景非常符合;第二,我们可以直接用ColorFilter,把暗色过滤出来。
我这里用的实现方法是后者,代码上面也有,再写一遍:

// 使图片变暗的ColorMatrixColorMatrix colorMatrix = new ColorMatrix(new float[]{  0.5F, 0, 0, 0, 0,  0, 0.5F, 0, 0, 0,  0, 0, 0.5F, 0, 0,  0, 0, 0, 1, 0,  }); mColorMatrixColorFilter = new ColorMatrixColorFilter(colorMatrix);@Overridepublic void draw(Canvas canvas, int sceneWidth, int sceneHeight) {if (mHasInit) {if (mIsHighLight) {mPaint.setColorFilter(null);} else {mPaint.setColorFilter(mColorMatrixColorFilter);}canvas.drawBitmap(mBitmap, mSrcRect, mDstRect, mPaint);}}

最后的效果如下:

嗯,完美!

接下来要做的是实现:当火箭拖动到离发射台比较近的位置时,发射台会变亮,并且有个上升的动画,同时火花出现;相反,如果远离发射台,发射台会变暗,会有一个下降的动画,同时火花消失。

“当火箭拖动到离发射台比较近的位置时,发射台会变亮,并且有个上升的动画”这些逻辑比较简单,只要发射台能与火箭交流下位置信息,很容易就可以做到,这里不细说,具体看源码。

不过需要注意的是此处有个难点:两个动画的快速切换。现在我们需要是实现的是:火箭的位置有个临界值,当大于这个值时,发射台做上升动画;当小于这个值时,发射台要做下降的动画。那么,请细想一种很容易出现的情况,假如我在临界值附近不断来回,那么上升动画做到一半就要下降,下降动画做到一半就要上升。如果动画没有控制好,火箭是会抽筋的,很容易出现问题,而且也不容易实现。
我们需要实现的效果如下:

要处理好这类两个动画中不断来回切换的问题,有个诀窍:记录好当前状态,所有动画从当前状态开始!什么意思呢?就例如上面的,上升动画做到一半时,要强制做下降动画了,这时记录当前的火箭的位置,从这个位置开始做下降动画。说起来很容易,想起来也是很容易,但是到实现起来就莫名无从入手了。
下面我用代码实现说明下,我用mX,mY来记录当前的火箭位置,mX,mY需要时刻更新最新的状态值。那么如果要做上升动画,首先取消下降动画,然后从当前的mX,mY开始做动画,反之也一样:

if (rocketBottom > getAnimSceneHeight() * 2 / 3) {mIsHighLight = true;// 如果火箭的位置超过屏幕的 2/3,发射台变亮并上升if (!mUpTransAnimation.hasStarted() || mUpTransAnimation.hasEnded()) {mDownTransAnimation.cancel();       // 取消下降的动画// 创建新的动画mUpTransAnimation.mStartX = mX;mUpTransAnimation.mStartY = mY;mUpTransAnimation.mEndX = mX;mUpTransAnimation.mEndY = (int) (getAnimSceneHeight() - mHeight);mUpTransAnimation.setStartTime(Animation.START_ON_FIRST_FRAME);mUpTransAnimation.setDuration(600);mUpTransAnimation.setInterpolator(new EaseOutBounceInterpolator());// 开始动画mUpTransAnimation.getTransformation(animTime, null);} else {// 已经在动画中mUpTransAnimation.getTransformation(animTime, null);}} else {mIsHighLight = false;// 如果火箭的位置没有超过屏幕的 2/3,发射台恢复一开始的状态int initStartY = (int) (getAnimSceneHeight() - mHeight * 0.845f);if (mY < initStartY) {if (!mDownTransAnimation.hasStarted() || mDownTransAnimation.hasEnded()) {mUpTransAnimation.cancel();     // 取消上升的动画// 创建新的动画mDownTransAnimation.mStartX = mX;mDownTransAnimation.mStartY = mY;mDownTransAnimation.mEndX = mX;mDownTransAnimation.mEndY = initStartY;mDownTransAnimation.setStartTime(Animation.START_ON_FIRST_FRAME);mDownTransAnimation.setDuration(600);// 开始动画mDownTransAnimation.getTransformation(animTime, null);} else {// 已经在动画中mDownTransAnimation.getTransformation(animTime, null);}}}

最终我做出来的效果如下:

挺好的!继续下一步。

绘制火花
如果前面都顺利做好了,相信绘制火花也不难了,唯一要注意的是,火花的位置不容易确定,要从图片中计算出来,其他没什么难的。
注意:正常来说,火花应该也是单独继承一个AnimObject才对,但是这里我把它跟发射台的AnimObject写在一起了,问题也不大,但是最好还是分开写吧。

绘制起飞阶段的动画
终于来到最后一步了!激动!
首先要实现的就是触发火箭起飞,具体就是当火箭拖动到火花上面并释放手指的时候,火箭就会起飞。
直接交给AnimObjectGroup来处理就好,在分发点击事件的时候,如果收到UP时间,就判断火箭的位置是不是在火花上,如果是,就切换到起飞动画阶段,同时把发射台给移除,加入雾霾的动画元素,代码如下:

public class RocketAnimGroup extends TouchAnimObjectGroup {/*** 火箭*/private RocketObject mRocketObject = null;/*** 发射台*/private LaunchPadObject mLaunchPadObject = null;/*** 雾霾*/private FogObject mFogObject = null;/*** 小雾霾*/private SmallFogObject mSmallFogObject = null;/*** 动画阶段* 第一阶段,准备发射* 第二阶段,起飞*/public static final int ANIM_STAGE_READY = 1;public static final int ANIM_STAGE_LAUNCH = 2;private int mAnimStage = ANIM_STAGE_READY;/*** 是否可点击*/private boolean mCanTouch = true;public RocketAnimGroup(View mRootAnimView, Context mContext) {super(mRootAnimView, mContext);mRocketObject = new RocketObject(mRootAnimView, mContext);mLaunchPadObject = new LaunchPadObject(mRootAnimView, mContext);mLaunchPadObject.setRocketObject(mRocketObject);// 添加到处理队列中addAnimObject(mLaunchPadObject);addAnimObject(mRocketObject);}@Overridepublic boolean onTouch(MotionEvent event) {if (!mCanTouch) {return true;}if (event.getAction() == MotionEvent.ACTION_UP) {if (mLaunchPadObject.isReadyOnLaunch()) {gotoAnimSecStage();}}return super.onTouch(event);}/*** 切换到动画第二阶段*/private void gotoAnimSecStage() {mAnimStage = ANIM_STAGE_LAUNCH;// 禁止点击事件mCanTouch = false;// 不再画发射台removeAnimObject(mLaunchPadObject);// 通知火箭起飞,设置动画阶段mRocketObject.setAnimStage(mAnimStage);// 加入雾霾removeAnimObject(mRocketObject);mFogObject = new FogObject(getRootAnimView(), getContext());addAnimObject(mFogObject);mSmallFogObject = new SmallFogObject(getRootAnimView(), getContext());mSmallFogObject.setFogObject(mFogObject);addAnimObject(mSmallFogObject);addAnimObject(mRocketObject);   // 调整绘制顺序}
}

接着画火箭飞行的动画,其实就是一个加速的平移动画,简单:

/*** 动画阶段* 第一阶段,准备发射* 第二阶段,起飞*/private int mAnimStage = 1;/*** 起飞动画*/private RocketTransAnimation mLaunchAnimation = null;
@Overridepublic void logic(long animTime, long deltaTime) {if (mLastLogicAnimTime == 0l) {mLastLogicAnimTime = animTime;}// 每一定时间换一次图,产生动的效果long now = System.currentTimeMillis();if (now - mLastLogicAnimTime > 100) {mBitmapIndex++;mBitmapIndex = mBitmapIndex % mRocketBmp.length;mLastLogicAnimTime = now;}if (mAnimStage == RocketAnimGroup.ANIM_STAGE_LAUNCH) {// 处于第二阶段,起飞if (mLaunchAnimation == null) {mLaunchAnimation = new RocketTransAnimation();// 直接从当前位置开始起飞mLaunchAnimation.mStartX = mX;mLaunchAnimation.mStartY = mY;// 下面两行是让火箭从正中间开始起飞
//              mLaunchAnimation.mStartX = (int) ((getAnimSceneWidth() - mWidth) / 2);
//              mLaunchAnimation.mStartY = (int) (getAnimSceneHeight() - mHeight);mLaunchAnimation.mEndX = mX;mLaunchAnimation.mEndY = (int) (-2 * mHeight);mLaunchAnimation.setDuration(1000);mLaunchAnimation.setInterpolator(new AccelerateInterpolator());     // 加速插值}mLaunchAnimation.getTransformation(animTime, null);mDstRect.set(mX, mY, (int) (mWidth + mX), (int) (mHeight + mY));}}
/*** 设置动画阶段*/public void setAnimStage(int stage) {mAnimStage = stage;}/*** 火箭的平移动画*/class RocketTransAnimation extends Animation {/*** 起始位置*/public int mStartX = 0;public int mStartY = 0;/*** 预期结束位置*/public int mEndX = 0;public int mEndY = 0;@Overrideprotected void applyTransformation(float interpolatedTime,Transformation t) {// 改变真实的位置坐标mX = (int) (mStartX + (mEndX - mStartX) * interpolatedTime);mY = (int) (mStartY + (mEndY - mStartY) * interpolatedTime);}}

这样一来,火箭就会直直的往上加速飞,飞离屏幕范围;
最后,再把那个小长条的雾霾做一个拉长的动画,就可以实现整个效果了:

/*** 小雾霾* @author zhanghuijun**/
public class SmallFogObject extends AnimObject {。。。@Overridepublic void logic(long animTime, long deltaTime) {if (mFogObject != null) {int fogHeight = mFogObject.getFogRect().height();int fogTop = mFogObject.getFogRect().top;if (mUpAnimation == null) {mUpAnimation = new SmallFogTracAnimation();mUpAnimation.mStartX = mX;mUpAnimation.mStartY = mY;mUpAnimation.mEndX = mX;mUpAnimation.mEndY = (int) (getAnimSceneHeight() - mHeight - fogHeight);mUpAnimation.setDuration(500);}mUpAnimation.getTransformation(animTime, null);mDstRect.set(mX, mY, (int) (mWidth + mX), fogTop);}}@Overridepublic void draw(Canvas canvas, int sceneWidth, int sceneHeight) {if (mHasInit) {canvas.drawBitmap(mBitmap, mSrcRect, mDstRect, mPaint);}}/*** 拉伸动画*/class SmallFogTracAnimation extends Animation {/*** 起始拉伸位置*/public int mStartX = 0;public int mStartY = 0;/*** 预期拉伸位置*/public int mEndX = 0;public int mEndY = 0;@Overrideprotected void applyTransformation(float interpolatedTime,Transformation t) {// 改变真实的位置坐标mX = (int) (mStartX + (mEndX - mStartX) * interpolatedTime);mY = (int) (mStartY + (mEndY - mStartY) * interpolatedTime);}}
}

然后,整个动画就完成了:

结语
实现这种动画的时候,只要把动画拆分开来,理解好整个动画的流程与逻辑,一个一个小动画完成,最后就可以完成一个很屌的动画了。

源码下载

http://download.csdn.net/detail/scnuxisan225/9389464

【Android进阶】如何写一个很屌的动画(3)---高仿腾讯手机管家火箭动画相关推荐

  1. Android仿腾讯手机管家实现桌面悬浮窗小火箭发射的动画效果

    功能分析:  1.小火箭游离在activity之外,不依附于任何activity,不管activity是否开启,不影响小火箭的代码逻辑,所以小火箭的代码逻辑是要写在服务中:  2.小火箭挂载在手机窗体 ...

  2. java软件屏幕大小转换,android 用java写一个自动适配各种屏幕大小的工具

    android 用java写一个自动适配各种屏幕大小的工具,假设我当前机器屏幕宽高为1920*1116,我页面某个组件的宽度为100px,那么在1280*800的机器上,按道理将应该是100/1920 ...

  3. 在Android studio里写一个Flutter程序

    在Android studio里写一个Flutter程序 前言 前言 新建一个Flutter工程,选择Flutter Application 选择Flutter 同时配置Flutter sdk路径 n ...

  4. C语言写一个很好玩的皇帝的后宫小游戏

    C语言写一个很好玩的皇帝的后宫小游戏 前言 先演示一下 上源码 总结 前言 只是单纯喜欢C语言,闲着无事把以前学习的时候的案例编了一下,都是很基础的代码,for,swich,if这些,基础好的看完后完 ...

  5. [html] 写一个搜索框,聚焦时搜索框向左拉长并有动画效果

    [html] 写一个搜索框,聚焦时搜索框向左拉长并有动画效果 为啥直接粘贴html发布以后就没有了呢 个人简介 我是歌谣,欢迎和大家一起交流前后端知识.放弃很容易, 但坚持一定很酷.欢迎大家一起讨论 ...

  6. 腾讯手机管家(pc版) for android,腾讯手机管家(PC版)for Android小技巧

    前几天看到添翼圈爆出腾讯应用助手泄漏版,作为一个老魔乐软件用户,抱着试试看的心情下载下来试了一下.之前一直也有用应用助手beta版,感觉新版界面挺清新的,立体感挺强的,比之前平面的界面感觉洋气很多.功 ...

  7. 类似腾讯手机管家应用android源码

    类似腾讯手机管家应用源码,也是自己写的android手机管家,内附源代码,应用实现了手机防功能,通信卫士功能,软件管家,进程管理,手机杀毒等功能,里面的图片部分用到了腾讯手机管家的图片,所以跟腾讯手机 ...

  8. Android用户分类管理,腾讯手机管家Android 7.10上线,微信整理助手实现智能精准分类...

    如今,大家习惯打开微信,或查看好友朋友圈动态,或浏览热点新闻.微信作为高频应用,带来了更加智能.便捷的生活,同时也让大家开始思考:当微信接收的信息越来越多,如何实现高效管理? 近日,腾讯手机管家上线了 ...

  9. 电脑管理android手机版下载失败怎么办,腾讯手机管家PC版轻松解决Android文件管理难题...

    智能手机使用时间越来越长,照片.视频.音乐.应用软件等不断增多.起初,还能把各类文件管理的有条不紊,但随着不断的拷贝.新建.删除,眼看着手机中的文件夹越来越多.越来越复杂,手机中的文件资料也开始像杂货 ...

最新文章

  1. cocoahttpserver 载入本地html,利用CocoaHttpServer搭建手机本地服务器
  2. python【数据结构与算法】从一个例子引入动态规划❤️
  3. [POJ](3268)Silver Cow Party ---最短路径(图)
  4. 数据集标注工具_数据标注分享9个数据标注工具
  5. GIT入门笔记(11)- 多种撤销修改场景和对策--实战练习
  6. saltstack计划任务工具和其他命令
  7. 负载均衡策略_高负荷小区负载均衡策略建议
  8. 一个模拟抛硬币的游戏
  9. International Obfuscated C Code Contest(IOCCC)
  10. 【Kay】MySQL必会常用函数
  11. K_MEANS 聚类
  12. debian9.6安装virtualbox
  13. 【oracle报错】ORA-01722:无效数字
  14. 智能红外遥控器(五):手机蓝牙控制格力空调
  15. 嵌入式Linux驱动笔记(十四)------详解clock时钟(CCF)框架及clk_get函数
  16. RAID区别和特点(全)
  17. 5G NR标准 第4章 LTE概述
  18. windows2012 r2 安装sqlserver 2000问题的解决方法
  19. .dat数据文件怎么打开_CAXA线切割版,打开CAD的DWG, DXF文件的终极绝招
  20. 零钱通项目(两个版本)含思路详解

热门文章

  1. python中的鸭子类型
  2. Andorid中修改密码
  3. APP极光消息推送无法接收,部分手机设置
  4. html5 实现ios原生控件,vue.js实现仿原生ios时间选择组件开发经验
  5. 用 ipdb 调试 Python 程序
  6. 读 THIN(谭振林) 《中国哲学(一)》有感
  7. 论文研究 | 基于视觉的汽车线束绑扎胶套检测与测量系统
  8. 《Ajax基础教程》读书笔记
  9. JavaWeb思维导图(一周目版)
  10. Linux教程 - 在Shell脚本中声明和使用布尔变量示例