Android Animation运行原理详解

1. 前言

作为Android程序员,或者是想要去模仿一些酷炫的效果,或者是为了实现视觉的变态需求,或者是压抑不住内心的创造欲想要炫技,我们不可避免地需要做各种动画。Android中,动画主要分为帧动画、插间动画以及属性动画。帧动画最为简单,是用一系列的素材作为关键帧逐帧播放,常用于制作加载动画,其工作量主要在设计部分;插间动画与属性动画则更多地是需要开发通过控制各种动画参数来实现,只有系统地理解Android中动画运行的原理,才能创作出更出色的动画,属性动画在下一篇文章中分析,本文主要分享我在探索插间动画运行原理过程中的一些收获,包括:Matrix如何控制动画参数;动画中各参数具体起什么作用;透明度动画、缩放动画、平移动画以及旋转动画的运行逻辑;动画在View的绘制过程中如何被应用。

2. Matrix介绍

在Android中,Matrix是一个3 x 3的矩阵:

Matrix 3 x 3 矩阵

Matrix可将一个点映射到另一个点,矩阵中包含了处理缩放、透视以及平移的区域,从而可用于控制实现平移、缩放、旋转等动画效果。强烈建议阅读Android Matrix理论与应用详解以更深入地了解Matrix实现动画控制原理,这里仅摘录其中的关键信息:

结论一:设对给定的图像依次进行了基本变化F1、F2、F3…..、Fn,它们的变化矩阵分别为T1、T2、T3…..、Tn,图像复合变化的矩阵T可以表示为:T = TnTn-1…T1。

结论二:Preconcats matrix相当于右乘矩阵,Postconcats matrix相当于左乘矩阵。

Matrix还给我们提供了各种友好的接口来组合生成复杂的动画,举个例子:假如我们想要实现一个平移(a,b)之后旋转(c,d)的动画,那用Matrix的实现代码就是这样的:

    Matrix matrix = new Matrix();
    matrix.setTranslate(a, b);
    matrix.postScale(c, d);

3. Animation运行原理分析

(1)基本属性介绍

使用过Animation的同学对下述基本属性应该非常熟悉,这里为了文章完整性,特地赘述一下:

  • mStartTime:动画实际开始时间
  • mStartOffset:动画延迟时间
  • mFillEnabled:mFillBefore及mFillAfter是否使能
  • mFillBefore:动画结束之后是否需要进行应用动画
  • mFillAfter:动画开始之前是否需要进行应用动画
  • mDuration:单次动画运行时长
  • mRepeatMode:动画重复模式(RESTART、REVERSE)
  • mRepeatCount:动画重复次数(INFINITE,直接值)
  • mInterceptor:动画插间器
  • mListener:动画开始、结束、重复回调监听器

虽然大部分都知道上面这些属性怎么用,但是可能还是有一些人对这些字段为什么有这样的作用不甚明白,于是我们就来分析一下。

(2)计算动画数据

Animation在其getTransformation函数被调用时会计算一帧动画数据,而上面这些属性基本都是在计算动画数据时发光发热,我们先看看getTransformation函数的运行逻辑:

  1. startTimeSTART_ON_FIRST_FRAME(值为-1)时,将startTime设定为curTime
  2. 计算当前动画进度:
    normalizedTime = (curTime - (startTime + startOffset))/duration
  3. mFillEnabled==false:将normalisedTime夹逼至[0.0f, 1.0f]
  4. 判断是否需要计算动画数据:
    • normalisedTime在[0.0f, 1.0f],需计算动画数据
    • normalisedTime不在[0.0f, 1.0f]:
      • normalisedTime<0.0f, 仅当mFillBefore==true时才计算动画数据
      • normalisedTime>1.0f, 仅当mFillAfter==true时才计算动画数据
  5. 若需需要计算动画数据:
    • 若当前为第一帧动画,触发mListener.onAnimationStart
    • mFillEnabled==false:将normalisedTime夹逼至[0.0f, 1.0f]
    • 根据插间器mInterpolator调整动画进度:
      interpolatedTime = mInterpolator.getInterpolation(normalizedTime)
    • 若动画反转标志位mCycleFliptrue,则
      interpolatedTime = 1.0 - normalizedTime
    • 调用动画更新函数applyTransformation(interpolatedTime, transformation)计算出动画数据
  6. 若夹逼之前normalisedTime大于1.0f, 则判断是否需继续执行动画:
    • 已执行次数mRepeatCount等于需执行次数mRepeated
      • 若未触发mListener.onAnimationEnd,则触发之
    • 已执行次数mRepeatCount不等于需执行次数mRepeated
      • 自增mRepeatCount
      • 重置mStartTime为-1
      • mRepeatModeREVERSE,则取反mCycleFlip
      • 触发mListener.onAnimationRepeat

这一段是根据getTransformation源码分析出来的,建议有兴趣的同学可以直接查看源码。上面这段分析留了一个不小的悬念,那就是动画更新函数是什么鬼,这个函数在Animation这个抽象类中仅仅是个钩子函数,由其子类提供具体实现,于是自然而然地引出了我们的下一个主题:主流动画介绍。

(3)主流动画分析

  • AlphaAnimation:透明度动画
    • 基本属性
      • mFromAlpha:起始透明度
      • mToAlpha:终止透明度
    • applyTransformation函数实现
      • transformation.setAlpha(mFromAlpha + ((mToAlpha - mFromAlpha) * interpolatedTime))
  • ScaleAnimation:缩放动画
    • 基本属性
      • mFromX:起始X值
      • mToX:终止X值
      • mFromY:起始Y值
      • mToY:终止Y值
      • mPivotX:缩放中心点X坐标
      • mPivotY:缩放中心点Y坐标
    • 属性计算逻辑
      • mFromX、mToX、mFromY、mToY计算
        • Float类型scale直接值
        • Faction类型相对值
          • 相对于自身(%):百分比转换为float直接值
          • 相对于父亲(%p):根据父亲size计算出size直接值,然后计算与本身size的百分比,最后转换为float直接值
        • Dimension类型size直接值:计算与本身size的百分比,然后转换为float直接值
      • mPivotX、mPivotY计算
        • ABSOLUTE类型直接值
        • RELATIVE_TO_SELF类型相对值:相对值乘以自身size得到直接值
        • RELATIVE_TO_PARENT类型相对值:相对值乘以父亲size得到直接值
    • applyTransformation函数实现
      • sx = mFromX + ((mToX - mFromX) * interpolatedTime)
      • sy = mFromY + ((mToY - mFromY) * interpolatedTime)
      • 是否设定缩放中心点:
        • 若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
        • 否则:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)
  • TranslateAnimation:平移动画
    • 基本属性
      • mFromXDelta
      • mToXDelta
      • mFromYDelta
      • mToYDelta
    • 属性计算逻辑
      • 同ScaleAnimation中mPivotX、mPivotY的计算逻辑
    • applyTransformation函数实现
      • dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime)
      • dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime)
      • transformation.getMatrix().setTranslate(dx, dy)
  • RotateAnimation:旋转动画
    • 基本属性
      • mFromDegrees
      • mToDegrees
      • mPivotX
      • mPivotY
    • 属性计算逻辑
      • mFromDegrees、mToDegrees均为角度(°)绝对值
      • mPivotX、mPivotY计算逻辑同ScaleAnimation
    • applyTransformation函数实现
      • 是否设定缩放中心点:
        • 若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
        • 否则:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)

透明度、缩放、平移以及旋转是最基本的动画,通过组合这些动画可以实现各种不一样的酷炫的效果,但是怎么才能实现这些动画的组合,这就不得不提到AnimationSet了。

(4) AnimationSet分析

  • AnimationSet是动画集合,用于组合运行多个动画,仅支持playTogether模式。
  • AnimationSet继承了Animation的字段,但是字段的应用有一些变化:
  • duration, repeatMode, fillBefore, fillAfter:这些属性会传递应用到所有的子Animation
  • repeatCount, fillEnabled:这些属性在AnimationSet中不被应用
  • startOffset, shareInterpolator:这些属性仅用于AnimationSet,不会传递至子Animation
  • 4.0以前在xml中设置duration, repeatMode, fillBefore, fillAfter, startOffset不会被应用,但是4.0之后再xml中设定这些属性跟运行时设定效果一致
  • 一些值的计算逻辑:
    • duration:
      • 缺省时,取所有子Animation中最长的duration;
      • 已设定时,返回mDuration
    • hasAlpha、willChangeTransformationMatrix、willChangeBounds:当有子Animation时,所有子Animation的值取“或”
    • startTime:取所有子Animation中最小的startTime
    • 子Animation中startOffset处理:
      • 保存子Animation的原始startOffset
      • 设置子Animation的startOffset为原始startOffset与AnimationSet的startOffset之和
      • 保存的原始startOffset在AnimationSet.clear是用于恢复各子Animation的startOffset
  • applyTransformation函数实现
    • 顺序调用子Animation的applyTransformation,然后利用Transformation.compose组合所有子Animation返回的Transformation作为该AnimationSet当前帧的变换状态
    • started及more值取所有子Animation对应值的“或”
    • ended值取所有子Animation对应值的“与”
    • 当started第一次为true时,调用AnimationSet的mListener.onAnimationStart
    • 当ended第一次为true(此时所有子Animation均结束)时,调用AnimationSet的mListener.onAnimationEnd

介绍完了主流动画以及组合动画,是不是Animation就介绍完了?其实不然,里面还漏掉了一个重要角色,那就是计算得到的动画数据是用什么存储的。实际上,Animation的动画函数getTransformation目的在于生成当前帧的一个Transformation,这个Transformation采用alpha以及Matrix存储了一帧动画的数据,Transformation包含两种模式:

  • alpha模式:用于支持透明度动画
  • matrix模式:用于支持缩放、平移以及旋转动画

同时,Transformation还提供了许多两个接口用于组合多个Transformation:

  • compose:前结合(alpha相乘、矩阵右乘、边界叠加)
  • postCompose:后结合(alpha相乘、矩阵左乘、边界叠加)

至此,Animation本身算介绍完整了,还差一个可用于从XML中构建动画以及插间器的AnimationUtils,这里就不做具体分析了,有兴趣的同学可以自行研究。但是,到现在为止,我们还没讲明白是:getTransformation这个函数究竟是在哪里调用的?计算得到的动画数据又是怎么被应用的?慌不要慌,待我娓娓道来,当这些问题揭秘之后,我们就知道为什么Animation这个包要放在android.view下面以及Animation完成之后为什么View本身的属性不会被改变,于是也就知道插间动画(Animation)跟属性动画(Animator)本质上的区别在哪了。

4. Animation的调用

要了解Animation的调用源头,要从Animation的基本使用View.startAnimation开始寻根溯源:

    public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
    }

通过invalidate(true)函数会触发View的重新绘制,由于View的绘制流程并不是本文的重点,因此这里仅说明从View.draw是怎么走到对Animation的处理函数的:

View.draw(Canvas)
—> ViewGroup.dispatchDraw(Canvas)
—> ViewGroup.drawChild(Canvas, View, long)
—> View.draw(Canvas, ViewGroup, long)
—> View.applyLegacyAnimation(ViewGroup, long, Animation, boolean)

View.applyLegacyAnimation就是Animation大显神通的舞台,其核心代码主要分三个部分:

  1. 初始化Animation(仅初始化一次)

    • 调用Animation.initialize(width, height, parentWidth, parentHeight),通过View及ParentView的Size来解析Animation中的相关数据;
    • 调用Animation.initializeInvalidateRegion(left, top, right, bottom)来设定动画的初始区域,并在fillBefore为true时计算Animation动画进度为0.0f的数据
  2. 调用getTransformation根据当前绘制事件生成Animation中对应帧的动画数据

  3. 根据动画数据设定重绘制区域

    • 若仅为Alpha动画,此时动画区域为View的当前区域,且不会产生变化
    • 若包含非Alpha动画,此时动画区域需要调用Animation.getInvalidateRegion进行计算,该函数会根据上述生成动画数据Thransformation中的Matrix进行计算,并与之前的动画区域执行unio操作,从而获取动画的完整区域
    • 调用ViewGroup.invalidate(int l, int t, int r, int b)设定绘制区域

View.applyLegacyAnimation调用完成之后,View此次绘制的动画数据就构建完成,之后便回到View.draw(Canvas, ViewGroup, long)应用动画数据对视图进行绘制刷新,其核心代码如下:

    if (transformToApply != null) {
        if (concatMatrix) {
            if (drawingWithRenderNode) {
                    // 应用动画数据
                renderNode.setAnimationMatrix(transformToApply.getMatrix());
            } else {
                canvas.translate(-transX, -transY);
                // 应用动画数据
                canvas.concat(transformToApply.getMatrix());
                canvas.translate(transX, transY);
            }
            parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
        }

        float transformAlpha = transformToApply.getAlpha();
        if (transformAlpha < 1) {
            // 应用动画数据
            alpha *= transformAlpha;
            parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
        }
    }

重点来了,大家看到Animation产生的动画数据实际并不是应用在View本身的,而是应用在RenderNode或者Canvas上的,这就是为什么Animation不会改变View的属性的根本所在。另一方面,我们知道Animation仅在View被绘制的时候才能发挥自己的价值,这也是为什么插间动画被放在Android.view包内,因为它跟View是真心相爱的。
文章到这,其实差不多可以结束了,但是创作动画过程中总是会被用到的一个神器还没出现,这让我有些不舍,尽管有太多人讲解这一神器,但是我还是毅然决然地决定抄一遍书,一来表示我对这一神器的爱,另一方面也是希望让文章更完整。

5. 插间器(Interpolator)

如果没有插间器,Animation应该按照时间来线性计算每一个时间点的动画帧数据;当时当加入插件器之后,我们计算动画帧数据时就可以更加的富有创造力,我可以随心所欲地计算任一时间点的动画帧数据,可以新加速在减速,也可以先减速在加速,总之一句话,我的地盘我做主。按照剧情的发展,接下来我应该介绍常用插间器了,但是作为一个有态度的程序员,我是不会按常理出牌的,想要了解常用插间器的实现原理,建议阅读Android Animations Tutorial 5: More on Interpolators

6. 后记

其实很早之前就看过Animation的源码,但是当时因为懒并没有写文章做笔记,这次因为项目需要优化动画,于是又重新撸了一遍,在此撰文为记,以备后用。当然,也希望这篇分享能给大家一些收获,非常感谢你的阅读,如果有浪费到你的时间,也就浪费了,权当看了一章凑字数的小说,233333~~~

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

推荐阅读更多精彩内容