首页 / 资讯中心 / 文章详情

Android属性动画解析:从补间动画缺陷到ObjectAnimator实战与原理

Android属性动画解析:从补间动画缺陷到ObjectAnimator实战与原理 ★ FEATURED ARTICLE
做Android开发这些年我最早接触的是补间动画一个AnimationSet往上一套平移、旋转、透明度都有现成接口看起来挺方便。但真正在复杂交互里遇到按钮“飞过去但点不到”的问题之后我才重新理解了属性动画的价值。而ObjectAnimator作为属性动画里最常用的入口不仅写法简单背后思路也足够优雅值得每个Android开发者把它吃透。这篇内容不打算照抄官方文档我会把原理、实操、组合用法和踩坑经验一次讲清楚适合刚入门不久或者用了很久却一直停留在“套API”层面的开发者。1. 为什么我从补间动画转投属性动画1.1 补间动画的“伪移动”陷阱补间动画View Animation最典型的用法是TranslateAnimation、RotateAnimation、AlphaAnimation这些配合AnimationSet把一组动作叠在一起。问题在于它们只改变View在绘制阶段的矩阵变换并没有改变View自身的属性。说得直白一点按钮的图形从左边移动到了右边但按钮这个对象在ViewGroup里的坐标、点击区域、touch事件命中范围全部还停留在原来的位置。我在某个项目里做个卡片移除效果卡片从列表中间滑动到屏幕外看起来是“删掉了”结果用户点原来位置还是能触发点击事件而且卡片明明已经飞到屏幕边缘屏幕中间的某个按钮却被卡片挡住点不了。排查了很久才反应过来问题不在事件分发代码而在动画类型选错了。补间动画产生的位移本质上只是画面上的“幻觉”底层Layout位置没有任何变化所以一旦交互要跟着视觉走它就露馅了。1.2 属性动画把“值的变化”交给对象本身属性动画的思路完全不同它直接操作对象的真实属性通过不断调用属性的setter方法把某个值从起始值一点一点改到目标值。比如ObjectAnimator.ofFloat(view, translationX, 0f, 500f)它在动画的每一帧里都在执行view.setTranslationX(当前值)最终View的translationX属性真的变成了500点击区域、触摸命中、事件分发全都跟着一起移过去。这也是“属性”两个字的由来动画的本质变成了“属性值随时间变化”而不是“画面随时间变化”。对View来说常用的translationX、rotation、alpha、scaleX这类属性都有对应的getter和setter所以ObjectAnimator可以直接拿来用。对于普通Java对象只要类里有setter理论上也能做动画后面我会专门讲这个。1.3 什么场景下两者该怎么选如果你做的纯是一次性视觉展示比如欢迎页的Logo旋转、背景透明度过渡不需要用户去点它也不依赖动画结束后的位置判断补间动画其实也够用。但从工程规范角度看我现在的项目里基本全面切到了属性动画补间动画只会在极少数遗留代码里出现。选择标准很简单动画结束之后这个View还要不要和用户交互要不要和其他View做位置碰撞要不要根据它的新状态读取坐标。只要答案是“要”就老老实实用属性动画。ObjectAnimator作为属性动画里最常用的一个类绝大多数View位移旋转场景都能覆盖这也是它值得单独写一篇细讲的原因。2. ObjectAnimator的内功心法一帧动画是怎么算出来的2.1 ObjectAnimator与ValueAnimator的父子关系ObjectAnimator继承自ValueAnimator。ValueAnimator本身不绑定任何View它只负责“在指定时间内从一个值变化到另一个值”并且把每一帧计算出来的新值通过回调通知出来。你可以把它理解成一个纯粹的数值发生器。ObjectAnimator在ValueAnimator的基础上加了一个target对象和propertyName属性名。目标对象有了、属性名有了ObjectAnimator就能在每一帧拿到新值后自动调用target.setXxx(newValue)。比如属性名是alpha它就调用target.setAlpha()属性名是rotation它就调用target.setRotation()。所以你发现没有ObjectAnimator真正依赖的并不是View而是“某个对象上存在对应的setter方法”。这个设计带来的最大好处是解耦动画框架不关心你动画的是什么只关心你能不能提供一个可写入的属性。坏处也很直接一旦属性名拼错或者对象没有对应setter动画就会失效或者抛异常这个我在第5章会专门讲。2.2 每一帧都要经历的三步换算ObjectAnimator内部跑起来后本质上是一个基于系统VSync信号的帧循环每一帧动画都会执行以下三步换算根据已经播放的时间算出当前时间进度fraction。比如duration是1000毫秒已经播了300毫秒那fraction就是0.3。把时间进度交给TimeInterpolator时间插值器换算成实际的插值进度。如果是LinearInterpolator插值进度还是0.3如果是DecelerateInterpolator实际进度会大于0.3效果就是“先快后慢”。把插值进度交给TypeEvaluator类型估值器根据起始值和结束值算出当前具体的属性值。比如从0f到100f进度0.3时计算出的值就是30f。最后这个值会被传给对象的setter。理解这三步非常关键因为很多自定义动画的坑都出在这里你以为你改的是属性值其实你改的是时间曲线你以为你改的是动画时长其实最后呈现的是插值器叠加估值器的综合结果。2.3 插值器与估值器分工明确的黄金搭档插值器和估值器是两个很容易混淆的概念我见过很多人以为Interpolator负责计算中间值其实它只负责“时间变化曲线”不负责具体值。一个管“时间怎么流逝”一个管“数值怎么映射”两者正交可以随意组合。插值器特点常见场景LinearInterpolator匀速进度条、匀速旋转DecelerateInterpolator先快后慢进场动画、卡片滑入AccelerateInterpolator先慢后快退场动画、物体重力加速AccelerateDecelerateInterpolator两头慢中间快对称往返动画OvershootInterpolator冲到终点再回弹一点弹窗弹出、金额变动BounceInterpolator落地多次弹跳物体掉落、水滴滴落估值器方面Android内置了IntEvaluator、FloatEvaluator、ArgbEvaluator这几种常用实现。ArgbEvaluator值得一提它支持在两种颜色之间平滑过渡做主题切换或者状态强调动画时很实用。ObjectAnimator colorAnim ObjectAnimator.ofInt( view, backgroundColor, 0xFF00FF00, 0xFFFF0000 ); colorAnim.setEvaluator(new ArgbEvaluator()); colorAnim.setDuration(600); colorAnim.start();3. ObjectAnimator实操手册最常用的几种写法3.1 从一行代码开始ofFloat与常见属性ObjectAnimator最简单的用法是ofFloat()括号里依次传目标对象、属性名、可变参数values。注意values的两种语义如果传两个值前一个是起始值后一个是结束值如果只传一个值起始值会通过getter读取比如view.getTranslationX()。所以想从当前状态动画到目标状态只传结束值就够了。ObjectAnimator animator ObjectAnimator.ofFloat(view, translationX, 500f); animator.setDuration(600); animator.setInterpolator(new DecelerateInterpolator()); animator.start();常用属性整理成一张表方便对照属性作用settertranslationX / translationY相对原始位置的水平/垂直偏移setTranslationXrotation绕Z轴旋转平面旋转setRotationrotationX / rotationY绕X轴/Y轴旋转3D透视效果setRotationXscaleX / scaleY水平/垂直缩放setScaleXalpha透明度setAlphax / y绝对位置等于left加上translationXsetXpivotX / pivotY旋转和缩放的中心点setPivotX一个细节translationX和x是两个不同维度的属性x是getLeft()和translationX叠加后的结果。如果你先动画x又同时动画translationX两个目标值会互相覆盖很容易出现位置漂移。我平时的原则是平移优先用translationX不要同时去动x避免“两个开关控制一盏灯”的混乱。3.2 多个属性一起动PropertyValuesHolder的用法一个动画同时改变多个属性比如卡片既平移又旋转还要淡出最简单的做法是开三个ObjectAnimator同时播放但更推荐用PropertyValuesHolder把多个属性打包到一个ObjectAnimator里。PropertyValuesHolder translation PropertyValuesHolder.ofFloat(translationX, 0f, 400f); PropertyValuesHolder rotation PropertyValuesHolder.ofFloat(rotation, 0f, 360f); PropertyValuesHolder alpha PropertyValuesHolder.ofFloat(alpha, 1f, 0f); ObjectAnimator animator ObjectAnimator.ofPropertyValuesHolder(view, translation, rotation, alpha); animator.setDuration(800); animator.setInterpolator(new AccelerateDecelerateInterpolator()); animator.start();这样做的好处是只有一个Animator对象在跑生命周期管理和监听器都简单很多多个属性的起始时间天然同步不容易出现“位移做完了透明度才开始”的割裂感。我实测下来属性比较多的时候代码的可读性也比散开写三个动画好很多。3.3 用XML声明动画与AnimatorInflater除了Java/Kotlin代码属性动画也可以用XML定义放在res/animator目录下。这样做主要方便把动画参数交给设计师或者快速调整代码里只需要加载并setTarget。XML示例res/animator/slide_animator.xml?xml version1.0 encodingutf-8? objectAnimator xmlns:androidhttp://schemas.android.com/apk/res/android android:propertyNametranslationX android:duration600 android:valueFrom0 android:valueTo500 android:repeatCount1 android:repeatModereverse android:startOffset200 android:interpolatorandroid:interpolator/decelerate_cubic /代码中加载Animator animator AnimatorInflater.loadAnimator(context, R.animator.slide_animator); animator.setTarget(view); animator.start();需要注意loadAnimator只是加载了动画定义不会自动绑定Viewstart之前必须先setTarget。XML里声明的valueFrom和valueTo对应代码里ofFloat的两个参数如果省略valueFrom系统也会尝试通过getter拿起始值。3.4 关于fillAfter、repeat和持续时长的细节用补间动画的人都知道setFillAfter(true)可以保留动画结束状态这个习惯在属性动画里是多余且不存在的。因为属性动画最终直接把属性set成了目标值View本身就停在结束状态不需要“填充”这种概念。如果你用AnimatorSet结束之后发现View又跳回了原位多半是后面还有别的动画在改同一个属性而不是fillAfter的锅。repeatCount和repeatMode配合可以实现循环动画比如indeterminate加载动画。repeatCount设为INFINITE代表无限循环repeatMode有restart和reverse两种restart是每次从起始值重新开始reverse是一来一回。无限循环动画一定要记得在不需要时cancel否则会持续持有目标对象引用带来内存问题。4. 监听器、AnimatorSet与自定义动画属性进阶4.1 用监听器把动画状态接进业务逻辑动画很多时候不只是视觉变化还需要在动画开始、结束、取消、重复这些时间点做业务处理。监听器主要分两类AnimatorListener负责状态监听animator.addListener(new AnimatorListenerAdapter() { Override public void onAnimationStart(Animator animation) { // 动画开始比如禁用点击防止误触 } Override public void onAnimationEnd(Animator animation) { // 动画结束比如恢复点击、移除View } Override public void onAnimationCancel(Animator animation) { // 动画被取消 } });AnimatorUpdateListener负责每一帧的值监听适合把动画值同步给其他View或者非View对象animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { Override public void onAnimationUpdate(ValueAnimator animation) { float animatedValue (float) animation.getAnimatedValue(); progressView.setCurrentProgress(animatedValue); } });这里有个细节onAnimationCancel被调用时onAnimationEnd不会继续回调。如果你在end里面写了“动画完成后把列表项删掉”的逻辑结果用户切后台或者代码里主动cancel了动画删除逻辑就不会执行容易造成状态不一致。所以业务上要区分“正常播完”和“被取消”这两种结束方式。4.2 AnimatorSet组合动画编排一个完整的交互流程单个动画搞不定的时候用AnimatorSet把多个动画按顺序编排。它的核心API是play()、with()、after()、before()理解起来就像搭积木先拿到一个动画的句柄然后让别的动画跟它同时、在它之前或者在它之后执行。ObjectAnimator slideUp ObjectAnimator.ofFloat(view, translationY, 0f, -200f); ObjectAnimator fadeOut ObjectAnimator.ofFloat(view, alpha, 1f, 0f); ObjectAnimator rotate ObjectAnimator.ofFloat(view, rotation, 0f, 30f); AnimatorSet set new AnimatorSet(); set.play(rotate).with(fadeOut).after(slideUp); set.setDuration(500); set.start();组合动画要注意一个很容易踩的坑AnimatorSet的duration只会作用在没有单独设置duration的子动画上。如果你给某个子ObjectAnimator单独设了500毫秒又给AnimatorSet设了800毫秒那个子动画还是会按自己的时长跑整体节奏会乱。我平时习惯要么全部用AnimatorSet统一时长要么每个子动画都明确设置不混合使用。AnimatorSet还经常用来做“入场之后停顿一下再退场”这种带间隔的流程可以在子动画上setStartOffset也可以把betweenActions用好。4.3 自定义TypeEvaluator让动画值按你要的曲线走内置估值器只能处理简单的数值插值当你要动画一个PointF、Rect或者自定义结构体的时候就需要自己实现TypeEvaluator了。比如让一个View按两点之间的直线移动自定义一个PointF估值器public class PointFEvaluator implements TypeEvaluatorPointF { Override public PointF evaluate(float fraction, PointF startValue, PointF endValue) { return new PointF( startValue.x (endValue.x - startValue.x) * fraction, startValue.y (endValue.y - startValue.y) * fraction ); } }使用方式ObjectAnimator animator ObjectAnimator.ofObject( view, position, new PointFEvaluator(), new PointF(0, 0), new PointF(300, 600) ); animator.setDuration(1000); animator.start();前提是你的View里要有setPosition(PointF point)方法然后在里面把自己移动到对应位置并invalidate。这种做法的价值在于动画的中间值完全可以由你定义不受限于int和float自由度很高。想做一个沿贝塞尔曲线飞行的效果本质也是自定义估值器去算曲线上的点。4.4 属性动画不限于View给任意对象做动画这一点很多人会忽略ObjectAnimator不关心目标是不是View。只要你有一个Java对象有对应的setter和getter它就敢给你做动画。比如某个自定义的数据展示对象内部有一个progress字段控制图表绘制层的进度public class ProgressModel { private float progress; public float getProgress() { return progress; } public void setProgress(float progress) { this.progress progress; } }然后写ProgressModel model new ProgressModel(); ObjectAnimator animator ObjectAnimator.ofFloat(model, progress, 0f, 100f); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { Override public void onAnimationUpdate(ValueAnimator animation) { chartView.invalidate(); } }); animator.start();结合ValueAnimator的update监听就能用同一个模型驱动多个View同步绘制。这样动画逻辑就从具体的View里抽离出来了做复杂大屏可视化的时候特别好用。5. 我踩过的七个ObjectAnimator的坑5.1 目标对象没有对应setter动画直接失效ObjectAnimator通过属性名反推setter方法属性名rotationX会去找setRotationX()。如果View没有这个方法或者属性名拼错动画不会弹窗提示常见的表现是View纹丝不动或者运行到一半直接抛异常。我在某个项目里想动画自定义View里的progress属性类里只有updateProgress()方法没有setProgress()结果动画静默失败排查了好一阵。解决方案也很简单写自定义属性之前先把getter和setter都补齐命名严格按照setProgress/getProgress的驼峰规则。如果不想因为动画暴露setter可以直接用ValueAnimator加updateListener手动赋值。5.2 忘记cancel导致的内存泄漏属性动画会持有目标对象的引用如果目标对象是Activity或者View而动画又是无限循环或者还没播完时页面就被销毁了泄漏就发生了。因为动画框架的宿主是Choreographer它会持续回调引用链条一直断不开。所以只要在页面里启动了无限循环动画最稳妥的方式是在onDestroy里主动cancelOverride protected void onDestroy() { super.onDestroy(); if (animator ! null) { animator.cancel(); } }即使是有限时长动画也不要完全依赖它自动结束。如果动画时长是800毫秒而用户在200毫秒时退出了页面这期间泄漏窗口一样存在。5.3 不要在onCreate里急着startonCreate阶段View还没完成measure和layout很多布局相关的尺寸拿不到比如getWidth()返回0。此时你按宽度算一个平移距离算出来的目标值往往是0动画看起来就是“闪了一下”。正确做法是把动画start放到视图树准备好的时机view.post(new Runnable() { Override public void run() { animator.start(); } });或者用ViewTreeObserver的OnPreDrawListener。总之需要用到布局尺寸的动画都要等第一帧绘制快结束时再启动。5.4 同一个Animator实例的复用问题一个Animator实例在播放过程中再次调用start()状态会很混乱。动画内部会尝试重置当前时间但监听器、更新器等状态可能会重复触发。我通常的准则是每个动画播放场景创建新的Animator实例而不是缓存起来复用。如果确实要复用等它完全结束之后再重新设置值并start不要边播边重新start。如果是View常见的移动、缩放、透明效果其实有更轻量的选择ViewPropertyAnimator它每次调用都会基于当前值继续动画天然规避了复用问题。5.5 pivotX和pivotY决定缩放旋转中心旋转和缩放默认围绕View的中心点但不是所有场景都想要中心。比如左上角弹出的面板做scale动画如果围绕中心缩放面板会从中心往外扩大视觉上是从中间“长出来”而不是从左上角伸展。很多时候需要把缩放中心改到左边缘或者上边缘view.setPivotX(0f); view.setPivotY(0f);注意pivotX和pivotY也是普通属性也可以用ObjectAnimator动画它们比如让旋转中心从中心平滑移到角落能做出很特别的卡片翻转效果但同时也增加了位置判断的复杂度不建议在列表里高频使用。5.6 onAnimationEnd与onAnimationCancel的关系前面提到过动画被cancel之后不会回调onAnimationEnd而是回调onAnimationCancel。很多人的end回调里写着“动画结束就把数据同步到服务器”“删除这条列表数据”这些一等一的业务逻辑一旦cancel导致逻辑不执行数据就会错乱。我的习惯是如果业务结束逻辑在cancel时也要执行就在cancel里显式调用同一个处理方法或者干脆把业务逻辑放到finally这种保证执行的区域。说到底动画只是交互链条的一环不能让它承担太多业务完整性责任。5.7 更新回调里不要做耗时操作AnimatorUpdateListener每一帧都会触发帧率一般是60fps也就是一秒回调60次。如果在里面做文件读写、网络请求、复杂布局UI线程很容易卡顿动画就会掉帧看起来一卡一卡的。这个更新回调只适合做轻量操作更新progress、设置View属性、改颜色、算个简单坐标。复杂计算提前算好或者放到动画外完成不要把动画帧当成万能的任务分发器。6. 性能对比与我的最后建议6.1 ViewPropertyAnimator、ObjectAnimator、ValueAnimator怎么选总有人问我这几个类到底有什么区别选哪个好。我用一个表格直接点明方案代码复杂度灵活性性能推荐场景ViewPropertyAnimator极简低只支持View常用属性高内部针对性优化普通View位移、透明、缩放ObjectAnimator中高支持任意对象任意属性中通过反射调用setter业务联动、自定义属性、组合动画ValueAnimator较高极高不依赖setter较高驱动非View对象、自定义绘制动画如果一个需求只是让某个View平移一下、淡入淡出我用ViewPropertyAnimator更多因为链式调用非常直观view.animate() .translationX(400f) .alpha(0.5f) .setDuration(600) .start();但它只覆盖View的常见属性一旦涉及到自定义View属性、普通对象动画、多动画编排就轮到ObjectAnimator和AnimatorSet出场了。ValueAnimator则是我在写自定义控件时用得最多的因为它不绑定setter自由度最高适合把动画值直接用在onDraw里面的绘图逻辑。6.2 动画时长、插值器和代码结构上的建议动画时长不要贪心。系统默认的300毫秒左右是参考基准复杂一点的编排可以拉到500到800毫秒超过1秒就会明显感觉拖沓。我在项目里会统一维护一个时长常量类把短动画、中动画、长动画的时长集中管理避免每个页面随手写数字后面统一调节奏时非常痛苦。插值器的选择也不要只盯着Bounce和Overshoot这些抢眼的。我个人经验是进场动画用DecelerateInterpolator退场动画用AccelerateInterpolator往返用AccelerateDecelerateInterpolator这三套已经覆盖了绝大多数场景花哨的回弹和弹跳用在关键提示上才有记忆点。代码结构上最好把动画封装成方法或者独立的动画工厂类不要散落在Activity的各个回调里。一个页面动辄十几个动画每个动画都有duration、interpolator、监听器堆在一起根本没法维护。我习惯是每一种交互动向对应一个方法比如showCardEntranceAnimation()、hidePanelExitAnimation()参数只暴露View和可选延时内部细节全部收拢。最后分享一点个人体会动画不是装饰品而是在向用户解释交互逻辑。好的动画让用户知道这个元素从哪里来、要到哪里去、当前是什么状态。我见过很多实现属性动画本身用得没有问题但就是因为过度追求效果弹窗打转、列表跳来跳去反而让页面显得廉价。掌握ObjectAnimator不是终点想清楚一个动效在界面上承担什么职责比背再多API都重要。这也是我近几年做动画最大的一个转变。
阅读完成 · 觉得有帮助?
咨询建站