18、Android动画框架:View动画、属性动画、帧动画、Transition动画与共享元素

动画这东西,说白了就是让界面“活”起来。用户点个按钮,如果只是生硬地跳转,体验就很廉价。我做了这么多年Android,发现很多开发者对动画的理解还停留在“会动就行”的阶段。其实Android的动画框架远比你想象的要强大,而且坑也不少。

今天我们就来彻底捋一遍。从最基础的View动画,到最灵活的属性动画,再到过渡动画和共享元素。我会把我在项目中踩过的坑、积累的技巧,都抖出来。

18.1 View动画:最老派,但够用

View动画是Android最早提供的动画方案。它操作的是View的“绘制效果”,而不是View本身的属性。什么意思呢?就是动画结束后,View的实际位置其实没变,只是看起来变了。

核心要点:View动画只改变视觉效果,不改变真实坐标。点击事件还在原地。

我记得刚入行那会儿,有个需求是让一个按钮从屏幕外飞进来。我用TranslateAnimation做完,动画是挺炫,但用户点那个按钮就是没反应。调试了半天才发现,按钮的点击区域还在原来的位置。嗯,这就是View动画的经典坑。

View动画有四种基本类型:

  • TranslateAnimation:平移。从A点到B点。
  • ScaleAnimation:缩放。可以围绕某个中心点。
  • RotateAnimation:旋转。同样可以指定旋转中心。
  • AlphaAnimation:透明度。从0到1或者反过来。

你还可以用AnimationSet把它们组合起来。比如让一个View一边旋转一边淡出。

// 代码示例:组合动画
AnimationSet set = new AnimationSet(true);
set.addAnimation(new AlphaAnimation(1.0f, 0.0f));
set.addAnimation(new RotateAnimation(0, 360, 
    Animation.RELATIVE_TO_SELF, 0.5f, 
    Animation.RELATIVE_TO_SELF, 0.5f));
set.setDuration(1000);
view.startAnimation(set);

这里有个细节:AnimationSetshareInterpolator参数。如果设为true,所有子动画共享同一个插值器。我个人习惯设为false,这样每个动画可以独立控制节奏。

小技巧:如果你只是想让View消失或出现,用AlphaAnimation配合setFillAfter(true)。但别忘了在动画结束时把View的visibility设为GONE,否则它虽然透明了,但仍然占据布局空间。

18.2 属性动画:真正的“动”

属性动画是Android 3.0引入的。它直接改变View的真实属性,比如x、y、alpha、rotation等。动画结束后,View就在那个位置,点击事件也跟过去。

为什么会这样?因为属性动画是通过ValueAnimator不断回调,然后你手动或者通过ObjectAnimator自动去设置属性的值。View的属性变了,它的绘制位置自然就变了。

我最常用的是ObjectAnimator。它封装了大部分常见操作:

// 代码示例:属性动画
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f);
animator.setDuration(500);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();

你看,代码比View动画更直观。而且你可以对任何有setter方法的属性做动画,不限于View。比如自定义View里的某个progress变量。

我曾经在一个自定义进度条里,用ObjectAnimatorprogress属性做动画。效果非常丝滑,而且代码量很少。如果你还在用HandlerpostDelayed去模拟进度条动画,赶紧换掉吧。

避坑指南:我曾经在RecyclerView的item动画里大量使用属性动画,结果发现快速滑动时卡顿严重。原因是属性动画会频繁触发invalidate(),导致过度绘制。解决方案是使用ViewPropertyAnimator,它内部做了批量处理优化。

ViewPropertyAnimator是属性动画的“语法糖”:

// 代码示例:ViewPropertyAnimator
view.animate()
    .translationX(300f)
    .alpha(0.5f)
    .rotation(360f)
    .setDuration(500)
    .start();

一行链式调用,搞定多个属性。而且性能更好。我个人强烈推荐在简单动画场景下使用它。

18.3 帧动画:简单粗暴

帧动画就是一张一张地切换图片,像放电影一样。Android提供了AnimationDrawable来实现。

用法很简单:在drawable文件夹里放一个XML,列出每一帧的图片和持续时间:

<!-- res/drawable/frame_animation.xml -->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/frame_1" android:duration="100" />
    <item android:drawable="@drawable/frame_2" android:duration="100" />
    <item android:drawable="@drawable/frame_3" android:duration="100" />
</animation-list>

然后在代码里启动:

ImageView imageView = findViewById(R.id.iv_animation);
imageView.setImageResource(R.drawable.frame_animation);
AnimationDrawable anim = (AnimationDrawable) imageView.getDrawable();
anim.start();

帧动画的缺点很明显:内存占用大。如果你有几十张高清图片,那内存直接爆炸。我建议只用于小图标或者短时长的加载动画。而且不要在onCreate里直接start(),因为那时View还没完全显示。最好用post延迟启动。

优化建议:如果帧数较多,可以考虑用SurfaceView配合MediaPlayer播放视频序列帧,或者用Lottie加载矢量动画。帧动画只适合简单场景。

18.4 Transition动画:场景切换的艺术

Transition动画是Android 4.4引入的。它用于两个场景之间的过渡。什么叫场景?就是一组View的状态。比如一个布局从“展开”到“折叠”,或者从一个Activity到另一个Activity。

核心API是TransitionManagerScene。你定义好开始场景和结束场景,然后调用TransitionManager.go(),系统会自动计算View的变化并生成动画。

举个例子:一个布局里有个按钮,点击后布局高度变化,里面的文字也变化。传统做法你得手动写动画。用Transition,你只需要定义两个布局文件,然后:

// 代码示例:Transition动画
Scene scene = Scene.getSceneForLayout(rootView, R.layout.scene_end, this);
Transition transition = new AutoTransition();
transition.setDuration(500);
TransitionManager.go(scene, transition);

系统会自动对比两个场景中相同id的View,然后对它们的位置、大小、透明度等做动画。非常智能。

我记得在做一个电商App的筛选面板时,用了Transition动画。面板展开和收起的效果非常自然,而且代码量比用属性动画少了至少一半。你想想看,如果手动去计算每个View的起始位置和结束位置,那得多麻烦。

Transition框架内置了几种过渡效果:

  • AutoTransition:默认组合,包含淡入淡出、移动、缩放。
  • Fade:淡入淡出。
  • ChangeBounds:位置和大小变化。
  • ChangeImageTransform:ImageView的图片变换。

你也可以自定义Transition,继承Transition类,重写captureStartValuescreateAnimator方法。不过说实话,大部分场景内置的已经够用了。

18.5 共享元素:让页面跳转更丝滑

共享元素是Transition动画在Activity或Fragment跳转时的特殊应用。它让两个页面中的“共同元素”平滑过渡。比如一个图片列表,点击后图片放大到详情页。这种效果在Material Design里很常见。

实现方式分三步:

  1. 在两个页面的布局中,给共享的View设置相同的android:transitionName
  2. 在第一个页面启动Activity时,传入ActivityOptionsCompat
  3. 在第二个页面的onCreate中,设置getWindow().setEnterTransition()

代码示例:

// 第一个页面:启动Activity
Intent intent = new Intent(this, DetailActivity.class);
ActivityOptionsCompat options = ActivityOptionsCompat
    .makeSceneTransitionAnimation(this, sharedView, "shared_image");
startActivity(intent, options.toBundle());

// 第二个页面:设置进入动画
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().setEnterTransition(new Fade());
    setContentView(R.layout.activity_detail);
    // 注意:共享元素的transitionName必须和第一个页面一致
}

这里有个坑:共享元素动画对View的层级和布局有要求。如果两个页面中共享元素的父布局结构差异太大,动画会变形。我曾经遇到过一个情况,共享元素在第一个页面是CardView的子View,在第二个页面是CoordinatorLayout的子View,结果动画时位置偏移了。解决方案是尽量保持父布局结构一致,或者使用ChangeImageTransform来处理图片变换。

避坑指南:共享元素动画在低端机上可能掉帧。我建议在onCreate里延迟执行一些耗时操作,比如网络请求,等动画完成后再加载数据。另外,如果共享元素是图片,尽量使用相同尺寸的图片,避免动画过程中发生拉伸。

18.6 知识体系总览

说了这么多,我们来画一张图,把整个Android动画框架串起来。这样你心里就有个全局观了。

Android动画框架知识体系 Android动画框架 View动画 属性动画 帧动画 Transition动画 Translate Scale Rotate Alpha ObjectAnimator ValueAnimator ViewPropertyAnimator 插值器/估值器 AnimationDrawable 逐帧切换 内存敏感 场景切换 共享元素 Activity过渡 Fragment过渡 选择原则:简单动画用View动画,复杂交互用属性动画,页面跳转用Transition

这张图把整个动画框架分成了四大块。你从上往下看,每个分支都有对应的核心类和典型场景。我个人建议,日常开发中优先考虑属性动画和Transition动画。View动画虽然简单,但坑太多。帧动画只适合特定场景。

好了,关于Android动画框架,我就讲到这里。记住一句话:动画不是装饰,是用户体验的一部分。用好了,用户会觉得你的App很“跟手”;用不好,反而会让人觉得卡顿和廉价。


公众号:蓝海资料掘金营,微信deep3321