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

系列中其他文章:

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

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

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

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


前言 


动画有多么重要,相信大家都清楚。它可以让一个枯燥乏味的静态界面变成一个充满动力的动画世界,提高用户体验。它的用途有很多,例如:

  • 让原本突兀的过程变得缓和,例如UC浏览器点击“酷站”,如下图 

  • 当有一个逻辑复杂,需要时间的来做,可以用动画来表示体现,例如腾讯手机管家在屏幕中清理内存,如下图 

可见,动画是多么的重要。可是,在Android中,动画有很多种展示形式,有很多中方案实现,例如有View动画,属性动画,帧动画等,但你们会发现,仅仅用Animation或者Animator难以实现上面动图中的动画,那些动画又是如何实现呢?

这就是本系列文章的重点所在。其实只要理解动画的本质,就会很容易做出任何动画,无论是普通的平移缩放动画,还是复杂的酷炫动画。在系列后期的文章里会写一个实例来实现“高仿手机管家内存清理的动画”,就是上面动图的动画。

一些基础知识: 
如果对Android中的动画知识认知不多,可以先看看这文章:Android 动画基础


理解Android中动画实现的本质 


在理解Android中动画实现的本质之前,首先要理解动画实现的原理,估计这个大家都清楚。 
无论是电影,动画片,游戏还是我们Android中的动画,其原理都是利用人类眼睛的“视觉残留”的特性医学证明人类具有“视觉暂留”的特性,人的眼睛看到一幅画或一个物体后,在1/24秒内不会消失。利用这一原理,在一幅画还没有消失前播放下一幅画,就会给人造成一种流畅的视觉变化效果。

也就是说,只要一秒内有连续24帧的画面连贯出现,那么看起来就是动画了。这也是我们Android中展示动画的原理,那么具体是怎么实现呢?

如果要在Android中实现动画展示,那么就必须要有一个“动画驱动”每隔1/24秒去调用View的draw()方法,同时改变每一帧中View需要变化的元素,让这个View不断的绘制,这样一来,所有变化就是组合成一个流畅的动画。

上面就是“Android中动画实现的本质”,其关键就是要有一个“动画驱动”。回想下我们平时最常用的动画类Animation或者Animator,其实它们内部实现也是一个“动画驱动”,驱动View不断绘制。所以,我们完全可以不用Animation或者Animator去做动画,只要有一个“驱动”即可,例如Scroller是个不错的选择,甚至我们可以写一个我们自己实现的“动画驱动”


常用的“动画驱动”

1、 View本身

最简单的“动画驱动”就是View本身,其最简单的实现就是在onDraw()马上触发下一次重绘,也就是:

class MyView extends View {
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        invalidate();
    }
}

这样一来,View每次绘制都是触发下一次绘制,不过你不用担心它一秒会绘制上百帧,Andriod应该是做了优化,正常情况下,这样的实现方案一秒最多60帧,而60帧已经是非常流畅的一个帧数了(一般情况下24帧已经足够)。这种方案的“驱动”比较适合在有一定实现的View上用,并且动画的东西与View的实现有关,例如TextView做一个文字变动的动画等。

延伸阅读:为什么认为游戏帧数要到 60 帧每秒才流畅,而大部分电影帧数只有 24 帧每秒? 


2、View动画,属性动画(Animation/Animator) 
关于这点的知识网上有太多太多,而且总结得非常好,或者还是可以看看这篇文章:Android 动画基础


3、Scroller 
有接触过界面滑动,应该对Scroller也有一定的认知,它需要结合View的computeScroll()方法实现。

这个“驱动”如它名字所示的,比较适合滑动相关的操作,因为它启动动画的参数就是位置的值。当然,你要用它来做点别的什么动画,也是完全没问题的。


4、自己实现一个简易的“动画驱动” 
既然有些需求用原有的方法难以实现或者实现起来不太合适,这个时候我们就需要自己动手了。因此,我也写了一个简易的“动画驱动”,同时扩展了一些额外的动画属性,可以方便的实现各种需求,具体请看下文。

这种驱动最大的优点就是所以东西都可以自己控制,例如控制帧频,控制动画的时间流逝速度等等,你想怎样就怎样。


自定义简易的动画框架 

这也是本文的重点,也是后面实现“高仿手机管家内存清理的动画”的基础。最下面有源码下载地址。

很长很长,现在不看也没事,可以先看下一篇,第三篇文章(【Android进阶】如何写一个很屌的动画(3)---高仿腾讯手机管家火箭动画)会详细说说这个动画框架如何设计和实现。

这个框架,在“动画驱动”上,使用的是自己写的“驱动”,其原理也是不断让界面重绘,同时可以控制一些驱动的参数,例如帧频等;在绘制上,则尽量仿造现在View框架来写,接下来我将详细说明。

首先说说这个框架的用途:主要用于绘制一些纯动画的界面,例如上面手机管家的动图那些界面。

既然是纯动画,那这个动画的载体直接用View或者SurfaceView即可。我比倾向直接用View,因为SurfaceView不支持硬件加速,而开启了硬件加速的View绘制效率比SurfaceView要好。 
所以,框架的载体就是一个继承View的AnimView:

public class AnimView extends View {
public AnimView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AnimView(Context context) {
        super(context);
    }
}

自定义的“动画驱动” 


有了载体,接下来需要的是我们的关键先生“动画驱动”,为了降低耦合和模块独立,这个驱动类不能做任何跟绘制相关的东西,仅仅做驱动的事情:

/**
 * 控制动画帧,单独一个模块
 * @author zhanghuijun
 */
public class AnimFrameController {

    public static final String TAG = "AnimDemo AnimFrameController";
    /**
     * 是否已经开始绘制
     */
    private boolean mIsStart = false;
    /**
     * 绘制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);
    }
}

上面的“驱动”主要控制了帧频和触发绘制,整个流程由这个驱动把关,结合View的实现看看框架的作用:

/**
 * 用于动画绘图的View
 * 
 * @author zhanghuijun
 * 
 */
public class AnimView extends View implements IAnimFrameListener, IAnimView {

    /**
     * 是否已经测量完成
     */
    protected boolean mHadSize = false;
    /**
     * 动画帧控制器
     */
    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();
    }
}

首先,初始化的是创建一个驱动:

mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper());

此处传了一个主线程的Looper过去,主要给AnimFrameController那个提供一个Looper,如果熟悉Handler的话,就会明白此处发送给该Looper的消息最终会在主线程执行。

然后,在View的onDraw()的结尾调用mAnimFrameController.updateFrame();,这样一来,所有要控制动画的东西都交给了AnimFrameController处理;

    /**
     * 在每帧更新完毕时调用
     */
    public void updateFrame() {
        // 计算需要延迟的时间
        long passTime = System.currentTimeMillis() - mLastDrawBeginTime;
        final long delayTime = mIntervalTime - passTime;
        // 延迟一定时间去绘制下一帧
        if (delayTime > 0) {
            mDrawHandler.postDelayed(mUpdateFrame, delayTime);
        } else {
            mDrawHandler.post(mUpdateFrame);
        }
        ...
    }

在updateFrame()中,按照一定时间去延时绘制下一帧,从而达到控制动画绘制的帧频。 
mUpdateFrame是一个Runnable:

    /**
     * 刷新帧Runnable
     */
    private final Runnable mUpdateFrame = new Runnable() {

        @Override
        public void run() {
            if (!mIsStart) {
                return;
            }
            // 记录时间,每帧开始更新的时间
            mLastDrawBeginTime = System.currentTimeMillis();
            // 通知界面绘制帧
            mListener.onUpdateFrame();
        }
    };

该Runnable的工作就是记录上一次绘制的时间,用来计算延迟时间;同时通知View去重新绘制,此处用了监听者模式,调用mListener.onUpdateFrame();就会回调到View去执行,从而将所有绘制操作交给View,AnimFrameController对于一概不管。

这样一来,“驱动”就完成了,这个“驱动”完全可以搬出去给其他有实现的View用。

动画时间 


动画时间与常规的时间不会完全一致符合,原因有很多,而且它也不应该完全符合。试想一下,如果动画由于某些原因中断暂停了,那么动画中流逝的时间肯定也得中断;又或者有一个需求,需要让当前动画加快到两三倍速度,那么动画中的时间必须比正常时间快两三倍才正确。因此,我们需要一个“动画时钟类”来单独管理这个动画时间。

/**
 * 动画时钟,可自行扩张更多功能,如快进时间等
 * @author zhanghuijun
 *
 */
public class AnimClock {

    /**
     * 相隔两帧之间的时间
     */
    private long mDeltaTime = 0l;
    /**
     * 上一帧的时间
     */
    private long mLastFrameTime = 0l;
    /**
     * 动画所经历的时间
     */
    private long mAnimTime = 0l;

    /**
     * 时钟启动,开始或者重新开始
     */
    public void start() {
        mLastFrameTime = System.currentTimeMillis();
    }

    /**
     * 刷新帧时调用
     */
    public void updateFrame() {
        long now = System.currentTimeMillis();
        mDeltaTime = now - mLastFrameTime;
        mAnimTime += mDeltaTime;
        mLastFrameTime = now;
    }

    /**
     * 获取相隔两帧之间的时间
     * @return
     */
    public long getDeltaTime() {
        return mDeltaTime;
    }

    /**
     * 获取动画总时间
     * @return
     */
    public long getAnimTime() {
        return mAnimTime;
    }

}

具体结合请看源码,在最下面。

绘制的动画物体类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接口。

因为有些动画元素在划分可能会有组的概念,所以会有一个AnimObjectGroup类负责管理自己组内的AnimObject,这样写的好处与ViewGroup、View的写法无异。

最后,AnimView则作为动画元素的根元素,统一筹划所有子动画元素,因此完整的AnimView就是这样:

/**
 * 用于动画绘图的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();
    }

}

简单实例 


尝试用上面的框架做一个计数器,非常简单,具体源码在下面的源码链接中,请看效果: 


这里写图片描述

声明 


该框架好多东西我还没有测试过,所以应该还存在挺多问题;同时它的功能实在薄弱,难以用在真正的项目上。写该框架的目的在于让更多的人明白如何写一个好动画,授人以渔。


源码下载

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

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值