系列中其他文章:
【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);
}
/**
* 绘制
*/
@Override
protected 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());
}
@Override
protected 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();
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == View.VISIBLE) {
if (mHadSize) {
start();
}
} else {
stop();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stop();
}
/**
* 开始
*/
@Override
public void start() {
mAnimFrameController.start();
}
/**
* 停止
*/
@Override
public void stop() {
mAnimFrameController.stop();
}
/**
* 设置帧频
*/
@Override
public void setFtp(int ftp) {
mAnimFrameController.setFtp(ftp);
}
/**
* 绘制
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mAnimFrameController.updateFrame();
}
@Override
public 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() {
@Override
public 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);
}
@Override
public void logic(long animTime, long deltaTime) {
// 按顺序执行AnimObject的逻辑
for (int i = 0; i < mAnimObjects.size(); i++) {
mAnimObjects.get(i).logic(animTime, deltaTime);
}
}
@Override
public void draw(Canvas canvas, int sceneWidth, int sceneHeight) {
// 按顺序绘制AnimObject
for (int i = 0; i < mAnimObjects.size(); i++) {
mAnimObjects.get(i).draw(canvas, sceneWidth, sceneHeight);
}
}
@Override
public 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();
}
@Override
protected 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();
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == View.VISIBLE) {
if (mHadSize) {
start();
}
} else {
stop();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stop();
}
/**
* 开始
*/
@Override
public void start() {
mAnimFrameController.start();
mAnimClock.start();
}
/**
* 停止
*/
@Override
public void stop() {
mAnimFrameController.stop();
}
/**
* 添加一个AnimObjectGroup
*/
@Override
public void addAnimObjectGroup(AnimObjectGroup group) {
mAnimObjectGroups.add(group);
}
/**
* 移除一个AnimObjectGroup
*/
@Override
public void removeAnimObjectGroup(AnimObjectGroup group) {
mAnimObjectGroups.remove(group);
}
@Override
public int getAnimSceneWidth() {
return mWidth;
}
@Override
public int getAnimSceneHeight() {
return mHeight;
}
/**
* 设置帧频
*/
@Override
public void setFtp(int ftp) {
mAnimFrameController.setFtp(ftp);
}
/**
* 绘制
*/
@Override
protected 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();
}
@Override
public 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();
@Override
public 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;
@Override
protected 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();
}
}
@Override
public 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.2
mHeight = 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;
}
}
@Override
public 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;
}
}
@Override
public 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;
@Override
public 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();
}
}
@Override
public 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));
// 使图片变暗的ColorMatrix
ColorMatrix 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;
}
}
@Override
public 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,把暗色过滤出来。
我这里用的实现方法是后者,代码上面也有,再写一遍:
// 使图片变暗的ColorMatrix
ColorMatrix 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);
@Override
public 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);
}
@Override
public 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;
@Override
public 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;
@Override
protected 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 {
。。。
@Override
public 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);
}
}
@Override
public 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;
@Override
protected void applyTransformation(float interpolatedTime,
Transformation t) {
// 改变真实的位置坐标
mX = (int) (mStartX + (mEndX - mStartX) * interpolatedTime);
mY = (int) (mStartY + (mEndY - mStartY) * interpolatedTime);
}
}
}
然后,整个动画就完成了:
结语
实现这种动画的时候,只要把动画拆分开来,理解好整个动画的流程与逻辑,一个一个小动画完成,最后就可以完成一个很屌的动画了。
源码下载