Android 动画学习

Android动画

补间动画

  • 补间动画的作用对象是 View,它支持4种动画效果,分别是平移动画、缩放动画、旋转动画、透明度动画。补间动画这四种变化效果对应着Animation的四个子类:TranslateAnimation、SvaleAnimation、RotateAnimation、AlphaAnimation。这四种动画既可以通过XML来定义,也可以通过代码来创建。
  • 补间动画的特点,既可以是单个的动画,也可以由一系列动画组成。AnimationSet 是这一系列动画的容器。
  • interpolator 动画集合所采用的插值器,影响动画的速度(斜率)。

自定义补间动画

  • 自定义补间动画,派生一种新动画只需要继承Animation这个抽象类,然后重写它的initialize()applyThansformation(),在 initialize() 中做一些初始化工作,在 applyTransformation() 中进行相对的矩阵变化即可, 很多时候需要采用Camera来简化矩阵变化的过程。

帧动画

  • 帧动画是顺序播放一组预先定义好的图片,类似于电影播放。不同于补间动画,系统提供了一个另外一个类AnimationDrawable来使用帧动画。帧动画的使用比较容易引起OOM,所以在使用帧动画时尽量应避免使用大多尺寸较大的图片。

补间动画的特殊使用场景

如:在ViewGroup中可以控制子元素的出场效果,在Activity中可以实现不同Activity的之间的切换效果。

  • 子元素的出场效果: LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样它的子元素出现时都会具有这种动画效果。同时, 它既可以使用 xml 的形式实现,也可以通过 LayoutAnimationContoller 来实现。
  • Activity的切换效果:通过overridePendingTransition(int enterAnim, int exitAnim),这个方法必须在startActivity()或者finish()之后被调用才能生效。
  • Fragment的切换动画:我们可以通过 FragmentTransaction 中的 setCustomAniamtions()来添加切换动画。

属性动画

核心:在一个时间间隔内完成对象的一个属性,由一个属性值向另一个属性值的改变。

属性动画要求对象必须为该属性提供 set()get(),否则就会失败。

属性动画是 API 11 新加入的特性(nineoldandroids开源库实现向下兼容)。我们知道,补间动画只能作用于 View,而属性动画可以为任意对象添加动画;另一方面,属性动画支持更多的效果,而不限于补间动画的四则变换:平移,旋转,缩放,透明度.

比较常用的有 ValueAnimatorObjectAnimatorAnimatorSet。ObjectAnimator继承自ValueAnimation。AnimatorSet是属性动画的集合类。

  • 插值器和估值器

    • 时间插值器:TimeInterpolator,它的作用是:根据时间流逝的百分比以及所选差值器来计算出当前属性值改变的百分比。类比到现实中,就是斜率。
      • 线性插值器:匀速 LinearInterpolator
      • 加速插值器:匀加速 AccelerateInterpolator
      • 减速插值器:匀减速 DecelerateInterpolator
      • 变速插值器:先加速,后减速 AccelerateDecelerateInterpolator
    • 类型估值器:TypeEvaluator, 根据当前属性改变的百分比来计算改变后的属性值。
      • 针对整型属性 IntEvaluator
      • 针对浮点型属性 FloatEvaluator
      • 针对Color属性 ArgbEvaluator
  • 属性动画的监听器

    • 属性动画提供了监听器用于监听动画的播放过程,主要有两个API接口,ValueAnimator.AnimatorUpdateListenerAnimator.AnimatorListener
    • 我们知道动画是有许多帧组成的,如果我想在每一帧播放后都得到监听,则需要使用ValueAnimator.AnimatorUpdateListener,它会在每播放一帧onAnimationUpdate就会调用一次。注意:一帧动画的时间是非常短的,大约10ms,如果我们在onAnimationUpdate中创建了对象,可能会引起内存抖动。
  • 斜率

斜率表示一条直线(曲线的切线)与横坐标轴的夹角的正切值。平行于X轴的直线的斜率为0,平行于y轴的直线的斜率不存在。已知两个点(x1,y1)和(x2,y2)的直线,若x1!=x2, 则直线的斜率为 k=(y2-y1)/(x2-x1).

曲线上某点的斜率反映了此曲线的变量在此点处变化的快慢程度。

实例

属性动画默认时间间隔为 300ms,默认的帧率 10ms/帧

  1. 改变一个对象(mObject)的 translationY 属性,让其沿着Y轴向上平移一段距离
ObjectAnimator.ofFloat(mObject, "translationY", -mObject.getHeight()).start()
  1. 在3秒内改变View的背景色, 并将动画设置为无限循环且进行反转。
ValueAnimator valueAnim = ObjectAnimator.ofInt(mView, "backgroundColor", 0XFFFF8080, 0XFF8080FF),
valueAnim.setDuration(3000);
valueAnim.setEvaluator(new ArgbEvaluator());
valueAnim.setRepeatCount(ValueAnimator.INFINITE);
valueAnim.setRepeatMode(ValueAnimator.REVERSE);
valueAnim.start();
  1. 动画集合的使用
AnimatorSet set = new AnimatorSet();
set.playTogetheer(
    ObjectAnimator.ofFloat(mView, "rotationX", 0, 360),
    ObjectAnimator.ofFloat(mView, "rotationY", 0, 180),
    ObjectAnimator.ofFloat(mView, "rotation", 0, -90),
    ObjectAnimator.ofFloat(mView, "translationX", 0, 90),
    ObjectAnimator.ofFloat(mView, "translationY", 0, 90),
    ObjectAnimator.ofFloat(mView, "scaleX", 1, 1.5F),
    ObjectAnimator.ofFloat(mView, "scaleY", 1, 0.5F),
    ObjectAnimator.ofFloat(mView, "alpha", 1, 0.25F, 1)
);
set.setDuration(5000).start();

AnimatorSet 与 AnimationSet

  • AnimationSet

支持一组Animation同时播放。如果要实现动画的顺序播放,需要计算每个动画的延迟时间 startOffset。

// 继承至 Animation ,说明可以使用 view.startAnimation 启动一组动画
public class AnimationSet extends Animation {
    // 使用数组存储需要播放的 animation
    // 数组支持随机访问,在查找和更新方面的时间复杂度为O(1),而在删除和添加方面的时间复杂度为O(n),而这里经常使用到的是查找,所以使用 数组的数据结构
    private ArrayList<Animation> mAnimations = new ArrayList();
    
    private long[] mStoredOffsets;
    // duration + startOffset
    private long mLastEnd;
    
    // shareInterpolator一般为false,即每个 animation 使用自己的 插值器
    public AnimationSet(boolean shareInterpolator){
        setFlag(PROPERTY_SHARE_INTERPOLATOR_MASK, shareInterpolator);
        init()
    }
    // 设置重复模式, 与 repeatCount 联合使用。会覆盖每个 animation 的设置
    public void setRepeatMode(int repeatMode){
        mFlags |= PROPERTY_REPEAT_MODE_MASK;
        super.setRepeatMode(repeatMode);
    }
}

  • AnimatorSet

按执行顺序排列想要执行的动画,并支持同时与顺序播放该组动画。

// AnimtorSet 本身是一个 动画,可以接收来自硬件发出的VSYNC信号,及时作出刷新
public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback{

    // Node 为一组动画之间添加依赖关系
    private static class Node implements Cloneable {
        Animator mAnimation; // 当前节点播放的动画
        ArrayList<Node> mChildNodes = null; // 当前节点动画播放结束后的动画节点
        ArrayList<Node> mSiblings; // 与当前节点动画同时播放的动画节点
        ArrayList<Node> mParents; // 当前节点动画播放之前的动画节点
    }
    // Builder 生成一组按添加顺序排序的动画节点
    public class Buidler {
        private Node mCurrentNode;
        Buidler(Animator anim) {
            mCurrentNode = getNodeForAnimation(anim);
        }
        // 添加与当前节点动画同时播放的动画
        public Buidler with(Animator anim) {
            Node node = getNodeForAnimation(anim);
            mCurrentNode.addSibling(node);
            return this;
        }
        // 添加在当前节点播放完成后再播放的动画。即:A.befor(B)表示 A在B之前播放
        public Builder before(Animator anim) {
            Node node = getNodeForAnimation(anim);
            mCurrentNode.addChild(node);
            return this;
        }
        // 添加在当前节点播放之前播放的动画
        public Buidler after(Animator anim) {
            Node node = getNodeForAnimation(anim);
            mCurrentNode.addParent(node);
            return this;
        }
    }
    private Node getNodeForAnimation(Animator anim) {
        Node node = mNodeMap.get(anim);
        if (node == null) {
            node = new Node(anim);
            mNodeMap.put(anim, node); // 添加动画与节点的映射关系
            mNodes.add(node); // 添加动画节点
        }
        return node;
    }
    // 记录正在播放的动画
    private ArrayList<Node> mPlayingSet = new ArrayList<Node>();
    // 记录所有动画节点的开始和结束事件,且所有事件均有序
    private ArrayList<AnimationEvent> mEvents = new ArrayList<>();
    // 记录所有的动画节点
    private ArrayList<Node> mNodes = new ArrayList<Node>();

    // 同时播放 items
    public void playTogether(Animator... items) {
        if (items != null) {
            Builder builder = play(items[0]); // 生成Builder对象
            for (int i = 1; i < items.length; ++i) {
                builder.with(items[i]);
            }
        }
    }
    // 顺序播放 items
    public void playSequentially(Animator... items) {
        if (items != null) {
            if (items.length == 1) {
                play(items[0]);
            } else {
                for (int i = 0; i < items.length - 1; ++i) {
                    play(items[i]).before(items[i + 1]);
                }
            }
        }
    }
    // 开始播放动画
    private void start(boolean inReverse, boolean selfPulse) {
        // 动画必须在有Looper 的线程执行
        if(Looper.myLooper == null){
             throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mStarted = true;
        // 1. 所有动画都不能异步执行
        int size = mNodes.size();
        for (int i = 0; i < size; i++) {
            Node node = mNodes.get(i);
            node.mEnded = false;
            node.mAnimation.setAllowRunningAsynchronously(false);
        }
        // 2. 初始化 1. 使用插值器(interpolator);2. 设置运行时长(during);3. 创建动画节点的有向图
        initAnimation();
        boolean isEmptySet = isEmptySet(this);
        // 3. 开始动画
        if(!isEmptySet) startAnimation(); 
        if (mListeners != null) {
            ArrayList<AnimatorListener> tmpListeners =
                    (ArrayList<AnimatorListener>) mListeners.clone();
            int numListeners = tmpListeners.size();
            for (int i = 0; i < numListeners; ++i) {
                tmpListeners.get(i).onAnimationStart(this, inReverse); // 回调开始监听
            }
        }
        if(isEmptySet) end(); // 处理异常 
    }
    private void startAnimation() {
        addAnimationCallback(0); // 添加 Choreographer 处理动画的回调
        if (mShouldResetValuesAtStart) { // Android O 之前需要在开始动画前重置所有数据
            
        }
        // 开始执行动画流程
        handleAnimationEvents()
    }
    private void handleAnimationEvents(int startId, int latestId, long playTime) {
        // 省略了部分代码
        for (int i = startId + 1; i <= latestId; i++) {
            AnimationEvent event = mEvents.get(i);
            Node node = event.mNode;
            if (event.mEvent == AnimationEvent.ANIMATION_START) { // 处理开始动画事件
                mPlayingSet.add(event.mNode); // 向动画集合添加正在执行的动画
                if (node.mAnimation.isStarted()) {
                    // 如果动画正在执行,则cancel掉
                    node.mAnimation.cancel();
                }
                node.mEnded = false;
                node.mAnimation.startWithoutPulsing(false); // 递归执行 start(), 
                pulseFrame(node, 0); // 开始心跳,心脏是 AnimatorSet, 血液是 Animators。 首先执行的是 AnimationSet 的 doAnimationFrame
            } else if (event.mEvent == AnimationEvent.ANIMATION_END && !node.mEnded) { // 处理动画结束事件
                pulseFrame(node, getPlayTimeForNode(playTime, node)); // 当所有动画都执行完,duration超时后自动结束
            }
        }
    }
    // 接收VSYNC信号,执行动画
    @Override
    public boolean doAnimationFrame(long frameTime) {
        float durationScale = ValueAnimator.getDurationScale();
        if (durationScale == 0f) {
            forceToEnd();
            return true;
        }

        // 处理暂停与恢复
        if (mPaused) {
            mPauseTime = frameTime; // 记录在第几帧停止的
            removeAnimationCallback();
            return false;
        } else if (mPauseTime > 0) {
            mFirstFrame += (frameTime - mPauseTime); // 从暂停处恢复播放
            mPauseTime = -1; // 重置暂停帧
        }
        
        .....
        
        long unscaledPlayTime = (long) ((frameTime - mFirstFrame) / durationScale);
        mLastFrameTime = frameTime;

        int latestId = findLatestEventIdForTime(unscaledPlayTime);
        int startId = mLastEventId;
        // 执行 animator 的 start 方法
        handleAnimationEvents(startId, latestId, unscaledPlayTime);

        mLastEventId = latestId;

        // 计算当前帧是否需要停止
        for (int i = 0; i < mPlayingSet.size(); i++) {
            Node node = mPlayingSet.get(i);
            if (!node.mEnded) {
                pulseFrame(node, getPlayTimeForNode(unscaledPlayTime, node));
            }
        }

        // 从 plyingSet 集合中移除所有结束的动画节点
        for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
            if (mPlayingSet.get(i).mEnded) {
                mPlayingSet.remove(i);
            }
        }

        boolean finished = false;
        if (mReversing) {
            if (mPlayingSet.size() == 1 && mPlayingSet.get(0) == mRootNode) {
                // The only animation that is running is the delay animation.
                finished = true;
            } else if (mPlayingSet.isEmpty() && mLastEventId < 3) {
                // The only remaining animation is the delay animation
                finished = true;
            }
        } else {
            // 如果播放列表为空且事件执行到最后一个,则需要停止所有动画
            finished = mPlayingSet.isEmpty() && mLastEventId == mEvents.size() - 1;
        }

        if (finished) {
            endAnimation(); // 动画结束了
            return true;
        }
        return false;
    }
    private void endAnimation() {
        mStarted = false;
        mLastFrameTime = -1;
        mFirstFrame = -1;
        mLastEventId = -1;
        mPaused = false;
        mPauseTime = -1;
        mSeekState.reset();
        mPlayingSet.clear();

        // 移除监听硬件 VSYNC 信号的回调
        removeAnimationCallback();
        // Call end listener
        if (mListeners != null) {
            ArrayList<AnimatorListener> tmpListeners =
                    (ArrayList<AnimatorListener>) mListeners.clone();
            int numListeners = tmpListeners.size();
            for (int i = 0; i < numListeners; ++i) {
                tmpListeners.get(i).onAnimationEnd(this, mReversing); // 回调动画结束监听
            }
        }
        mSelfPulse = true;
        mReversing = false;
    }
    
    private void removeAnimationCallback() {
        if (!mSelfPulse) {
            return;
        }
        AnimationHandler handler = AnimationHandler.getInstance();
        handler.removeCallback(this);
    }

    private void addAnimationCallback(long delay) {
        if (!mSelfPulse) {
            return;
        }
        AnimationHandler handler = AnimationHandler.getInstance();
        handler.addAnimationFrameCallback(this, delay);
    }
}

总结:通过简单分析 AnimatorSet 的源码,可以得到以下几点:

  1. 属性动画必须在具有 Looper 的线程中执行,否则会抛出异常
  2. 属性动画集合通过 NOde 和 Builder 管理动画的执行顺序
  3. 动画的执行是在接收到硬件层的 VSYNC 信号后,才开始执行下一帧动画的。另外,不只是动画,view刷新,事件输入等,也是通过监听硬件发出的 VSYNC 信号,在回调中处理逻辑的。
  4. 如果我们为属性动画添加了 updateListener ,则一定要小心出现内存抖动。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容