在 Android 开发中,Activity 的跳转是有默认的转场动画的,比如在国内的大多数 ROM 中,默认的 Activity 切换方式是从右往左进入,从左往右退出,这也是大多数 App 所使用的切换方式。

但是,当有时候原型设计出来之后,会发现这种方式并不那么合理,比如在『「ZXing」实现 Android 扫描二维码』一文中可以看到,我把扫描按钮放在了底部导航栏的中央,那么左右切换的方式就有些违和了,所以我改成了从底部向上弹入。

从底部进出

那么该如何实现这种切换的方式呢?

编写动画文件

虽然这个 Demo 是用 React Native 写的,但是由于扫描二维码的功能是使用原生 Android,所以这也是原生 Android 的实现方式。

首先在「res」目录下新建一个文件夹「anim」,不难看出这是一个用于存放动画文件的文件夹,也就是「Animation」的缩写,在「anim」文件夹下新建 Animation Resource File,每一个动画都要匹配一个动画文件。

比如我想要一个 Activity 的入场动画,我可以新建一个名为「anim_activity_open.xml」的动画文件,然后再在该文件内指定动画的效果。

先来看从底部弹入 Activity 的实现:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromYDelta="100%p"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toYDelta="0" />
</set>

解释一下这几个属性的作用:

  • android:duration:动画运行的时间,单位是毫秒。
  • android:fromYDelta:动画起始时,Y 坐标上的位置。
  • android:toYDelta:动画结束时,Y 坐标上的位置。
  • android:fromXDelta:动画起始时,X 坐标上的位置。
  • android:toYDelta:动画结束时,X 坐标上的位置。
  • android:interpolator:用来修饰动画效果,定义动画的变化率,可以使动画效果 accelerated(加速)、decelerated(减速)、repeated(重复)、bounced(弹跳)等。

不难理解,该动画实现的效果就是从 Y 轴 100%p 的位置开始,到 Y 轴 0 的位置结束,持续时间是 500 毫秒,并伴有加速效果。

解释一下轴,只需记住,以屏幕左上角为坐标原点,与之相连的两端屏幕则为坐标轴,往右是 X 轴,往下是 Y 轴。取值可以有多种写法:

  • 数值(如 50):表示 View 左上角坐标加上具体数值的像素。
  • 百分数(如 50%):表示在当前 View 左上角坐标加上 View 宽度的具体百分比。
  • 父百分数(如 50%p):表示在 View 左上角坐标加上父控件宽度的具体百分比。

同理,从底部退出:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromYDelta="0"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toYDelta="100%p" />
</set>

从右边进入:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="100%p"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toXDelta="0" />
</set>

其他就不一一详述了,参照修改即可。

接下来就是调用,只需在启动和销毁 Activity 时调用相应的动画即可。

比如,在调用 startActivity() 启动新的 Activity 时在后面添加一句代码即可:

startActivity(intent);
overridePendingTransition(R.anim.anim_activity_open, 0);

其中,第一个参数是要启动的 Activity 的弹入动画,第二个参数是当前 Activity 的退出动画,当参数为 0 时表示无动画。

startActivityForResult() 中也是同理:

startActivityForResult(intent, CODE);
overridePendingTransition(R.anim.anim_activity_open, 0);

当销毁 Activity 时,则在重写的 finish() 方法中调用:

@Override
public void finish() {
    super.finish();
    overridePendingTransition(0, R.anim.anim_activity_close);
}

但是我在项目中偶尔会遇到退出 Activity 时短暂黑屏的情况,直至该 Activity 完全退出才会正常显示,比如我在上方设置了动画持续时长为 0.5 秒,那么就会有 0.5 秒的黑屏,这是很不好的体验。

而后我发现新建一个动画可以避免这种情况,于是就有了「anim_activity_stay.xml」这个动画文件,虽说是“动画”,但实际上如其名所述,是一个“不动画”:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromYDelta="0"
        android:toYDelta="0"
        android:duration="@android:integer/config_mediumAnimTime" />
</set>

一目了然,起始位置与结束位置相同,即不产生移动。

在调用时,则使用:

overridePendingTransition(R.anim.activity_stay, R.anim.activity_close);

这样就能够产生相同的效果。

对了,Android 本身也自带了一些切换 Activity 的动画文件,可以让你省下造轮子的时间,如从屏幕左边切入,可以直接调用:

overridePendingTransition(android.R.anim.slide_in_left, android.R.anim.slide_out_right);

当然,官方自带的动画效果总是有限的,所以知道如何自定义也是十分重要的。

接下来看看还有哪些效果。

比如我们想实现 iOS 的缩放效果,进场动画可以这样写:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator">
    <scale
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXScale="2.0"
        android:fromYScale="2.0"
        android:pivotX="50%p"
        android:pivotY="50%p"
        android:toXScale="1.0"
        android:toYScale="1.0" />
</set>

<scale> 在这里就是用来实现缩放的,属性的作用如下:

  • android:fromXScale:起始 X 尺寸比例。
  • android:fromYScale:起始 Y 尺寸比例。
  • android:pivotX:缩放起点 X 轴坐标。
  • android:pivotY:缩放起点 Y 轴坐标。
  • android:toXScale:最终 X 尺寸比例。
  • android:toYScale:最终 Y 尺寸比例。

再来写个退场动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:zAdjustment="top">
    <scale
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:pivotX="50%p"
        android:pivotY="50%p"
        android:toXScale="0.5"
        android:toYScale="0.5" />
    <alpha
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromAlpha="1.0"
        android:toAlpha="0" />
</set>

相比进场动画,这里还增加了一个 <alpha> 标签,用于改变透明度,属性作用如下:

  • android:fromAlpha:动画开始的透明度。
  • android:toAlpha:动画结束的透明度。

这两个属性的取值为 0.0 ~ 1.00.0 表示完全透明,1.0 表示保持原有状态不变。

最外层的 <set> 标签我还增加了一个属性:

  • android:zAdjustment:允许在动画播放期间,调整播放内容在 Z 轴方向的顺序。

取值有三:

  • normal(0):正在播放的动画内容保持当前的 Z 轴顺序。
  • top(1):在动画播放期间,强制把当前播放的内容放到其他内容的上面。
  • bottom(-1):在动画播放期间,强制把当前播放的内容放到其他内容之下。

调用也是相同:

overridePendingTransition(R.anim.anim_zoom_in, R.anim.anim_zoom_out);

效果如下:

Zoom In & Zoom Out

Android L 的过渡效果

上面的切换动画基本上可以满足日常中对 Activity 跳转的需求了,但是总体来说还是相对比较平淡,符合用户习惯,但不惊艳。

Android 5.0 之后,Material Design 为 Android 注入了更加炫酷的过渡,通过其可以实现更加优秀的动画效果。

Explode

首先在「res」目录下新建一个文件夹「transition」,然后建一个 Transition Resource File,如「explode.xml」:

<?xml version="1.0" encoding="utf-8"?>
<explode xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000" />

太简单了就不多解释了,接下来在要启动的 Activity 中加入如下代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_explode);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Transition explode = TransitionInflater.from(this).inflateTransition(R.transition.explode);
        getWindow().setEnterTransition(explode);
    }
}

Transition 类初始化动画文件后调用 WindowsetEnterTransition() 方法设置进去即可。

启动 Activity 的方式稍微有点区别,如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(activityContext).toBundle());
} else {
    startActivity(intent);
}

startActivity() 的第二参数是用 makeSceneTransitionAnimation() 方法创建的 ActivityOptions 转成的 Bundle 对象,且 makeSceneTransitionAnimation() 方法的参数必须为 Activity,不能是其他 Context

另外,由于该过渡动画是从 API 21 引入的,所以如果项目的 minSdkVersion 小于 21 的话要加入上方的判断逻辑。

效果如下:

Explode

Slide

使用方法相似,建一个 Transition Resource File,如「slide.xml」:

<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:interpolator="@android:interpolator/decelerate_cubic"
    android:slideEdge="end">
    <targets>
        <target android:excludeId="@android:id/statusBarBackground" />
    </targets>
</slide>

解释一下属性:

  • android:interpolator:属性设置插值器,用来控制滑动速度。
  • android:slideEdge:滑动方向。有 4 个值:
    • end:根据地区使用习惯不同,中国一般在右侧。
    • start:根据地区使用习惯不同,中国一般在左侧。
    • top:顶部。
    • bottom:底部。

默认情况下顶部的状态栏也会执行动画,可以使用 <target> 标签忽略,android:excludeId 属性的值就是忽略的控件的 ID,如果需要忽略多个 View,则在 <targets> 组内继续添加即可。

调用方法也相似,不贴代码了,直接看效果:

Slide

Fade

使用方法相似,建一个 Transition Resource File,如「fade.xml」:

<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000" />

属性仅指定了动画持续时间,即用 1000 毫秒的时间把 View 从无到有,透明度从 01

调用方法也相似,不贴代码了,直接看效果:

Fade

Shared Element

该效果与前三种不同,可以在切换的时候指定元素过渡,使用这种切换动画,用户可能感觉不到开启了一个新的 Activity,可以说是我最喜欢的交互方案了,先来看效果吧:

Shared Element

实现方法与前三种不同,不需要创建 Transition resource file,只需为两个 Activity 需要共享动画的元素指定同一个 android:transitionName 即可。

如我为原 Activity 的头像指定为 profile,登录按钮指定为 button

<ImageView
    android:id="@+id/profile"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@mipmap/ic_launcher_round"
    android:transitionName="profile" />
    
<Button
    android:id="@+id/login_btn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="LOGIN"
    android:transitionName="button" />

那么在需要启动的 Activity 中选择需要共享的元素,也指定为相同的名称:

<ImageView
    android:layout_width="match_parent"
    android:layout_height="256dp"
    android:scaleType="centerCrop"
    android:transitionName="profile" />
    
<android.support.design.widget.FloatingActionButton
    android:id="@+id/edit"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="16dp"
    android:src="@drawable/ic_edit"
    android:transitionName="button" />

然后在原 Activity 中作如下配置:

ImageView profile = findViewById(R.id.profile);;
Button loginBtn = findViewById(R.id.login_btn);
loginBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Pair profilePair = new Pair<>(profile, ViewCompat.getTransitionName(profile));
        Pair btnPair = new Pair<>(loginBtn, ViewCompat.getTransitionName(loginBtn));
        ActivityOptionsCompat transitionActivityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(this, profilePair, btnPair);
        Intent intent = new Intent(MainActivity.this, SharedElementActivity.class);
        ActivityCompat.startActivity(this, intent, transitionActivityOptions.toBundle());
    }
});

将需要共享的元素构建成 Pair 对象,使用 ViewCompat.getTransitionName() 方法获取我们在布局文件中设置的 android:transitionName 的值。要注意,必须导入 android.support.v4.util.Pair 而不是 android.util.Pair,否则会出现问题。

通过 ActivityOptionsCompat.makeSceneTransitionAnimation() 静态方法得到 ActivityOptionsCompat 对象,该方法的第二个参数是一个数组,可以传入多个 Pair 对象。

最后再调用 ActivityCompat.startActivity() 启动 Intent 指定的 Activity,就可以看到上面的效果。