在 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.0
,0.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);
效果如下:
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
类初始化动画文件后调用 Window
的 setEnterTransition()
方法设置进去即可。
启动 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
的话要加入上方的判断逻辑。
效果如下:
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>
组内继续添加即可。
调用方法也相似,不贴代码了,直接看效果:
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
从无到有,透明度从 0
到 1
。
调用方法也相似,不贴代码了,直接看效果:
Shared Element
该效果与前三种不同,可以在切换的时候指定元素过渡,使用这种切换动画,用户可能感觉不到开启了一个新的 Activity
,可以说是我最喜欢的交互方案了,先来看效果吧:
实现方法与前三种不同,不需要创建 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
,就可以看到上面的效果。