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

系列中其他文章:

【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);
        }
    }
}

然后,整个动画就完成了:
这里写图片描述

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

源码下载

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值