Android 动画总结(1) - 概述
Android 动画总结(2) - 帧动画
Android 动画总结(3) - 补间动画
Android 动画总结(4) - 插值器
Android 动画总结(5) - 属性动画
Android 动画总结(6) - 估值器
Android 动画总结(7) - ViewGroup 子元素间的动画
Android 动画总结(9) - 过渡动画
对于 Activity,在 startActivity 或 finish 后调用
overridePendingTransition(R.anim.activity_in, R.anim.activity_out)
对于 Fragment:
supportFragmentManager.beginTransaction().setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)
ActivityOptions
从 Android 5.0 之后,可以用 ActivityOptions 来实现,ActivityOptionsCompat 是 support v4 的兼容实现,可以支持到 4.1(SDK 16),它有几个 make 开头的方法
- makeCustomAnimation(Context context, int enterResId, int exitResId)
- makeScaleUpAnimation(View source, int startX, int startY, int startWidth, int startHeight)
- makeThumbnailScaleUpAnimation(View source, Bitmap thumbnail, int startX, int startY)
- makeClipRevealAnimation(View source, int startX, int startY, int width, int height)
- makeSceneTransitionAnimation(Activity activity, View sharedElement, String sharedElementName)
- makeSceneTransitionAnimation(Activity activity, Pair<View, String>... sharedElements)
makeCustomAnimation
custom.onClick {
val compat = ActivityOptionsCompat.makeCustomAnimation(ctx, R.anim.activity_in, R.anim.activity_out)
start(it!!, compat)
}
private fun start(view: View, compat: ActivityOptionsCompat) {
val intent = Intent(ctx, OptionAfterActivity::class.java)
intent.putExtra("from", (view as Button).text)
// SDK 16 以下会忽略 compat.toBundle()
ActivityCompat.startActivity(ctx, intent, compat.toBundle())
}
最普通的,效果和过去的 overridePendingTransition 一样。
OptionAfterActivity 的布局就只有一个 TextView
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".OptionAfterActivity"
android:background="#5500f2f0">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="18sp"
android:layout_centerInParent="true"/>
</RelativeLayout>
class OptionAfterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_option_after)
// 接收前一个方法传过来的名字显示出来
tv.text = intent.extras["from"].toString()
}
override fun onBackPressed() {
ActivityCompat.finishAfterTransition(this)
}
}
发现 ActivityCompat.finishAfterTransition(this)
并没什么用。看源码
public static void finishAfterTransition(Activity activity) {
if (Build.VERSION.SDK_INT >= 21) {
activity.finishAfterTransition();
} else {
activity.finish();
}
}
好吧,ActivityCompat.startActivity
是 SDK >= 16
就生效,这退出的必须 >= 21
才行。再继续往下走
public void finishAfterTransition() {
if (!mActivityTransitionState.startExitBackTransition(this)) {
finish();
}
}
public boolean startExitBackTransition(final Activity activity) {
if (mEnteringNames == null || mCalledExitCoordinator != null) {
return false;
} else {
if (!mHasExited) {
// 能判断对这里才真正执行页面返回的动画
}
return true;
}
}
而这个 mEnteringNames 的定义是:
The shared elements that the calling Activity has said that they transferred to this
很可惜,此时的 mEnteringNames 是 null,直接返回 false 调用 finish 了。下面会讲到什么是 shared elements
。
makeScaleUpAnimation
参照 Activity 上的某个 View,新 Activity 从指定大小放大到最大显示。
动画速度太快了,也没找到可以控制时间的地方,查了许多资料,包括看源码注释,其实还不是很明白这个 View 到底有没有放大。
使用的一个场景是可能点击一个小 View,然后第二个页面某个位置显示的放大版的,这样看着好像是点击放大到另一个页面似的。
scaleUp.onClick {
val compat = ActivityOptionsCompat.makeScaleUpAnimation(image, image.width / 2, image.height / 2, 0, 0)
start(it!!, compat)
}
看下它的 5 个参数:
- View source - 参照物
- int startX - 相对于 source,新 Activity 开始的位置
- int startY - 同 startX,只不过这是 Y 轴方向上的
- int startWidth - 第二个 Activity 在做放大动画前一开始的初始宽度
- int startHeight - 这当然就是初始高度了
makeThumbnailScaleUpAnimation
和 makeScaleUpAnimation 的区别是,不再是放大页面上的一个 View,而是指定一张图,在转场时,放大这张图片。不过也许是太快了,根本看不见,也不知理解是否正确。
thumbnailScaleUp.onClick {
val compat = ActivityOptionsCompat.makeThumbnailScaleUpAnimation(text, BitmapFactory.decodeResource(resources, R.drawable.timg), 0, 0)
start(it!!, compat)
}
参数:
- View source - 图片放大的参照物
- Bitmap thumbnail - 要放大的图片
- int startX - 相对于参照物 source,这张图片开始放大的 X 轴位置
- int startY - 图片开始放大相对于 source 的 Y 轴位置
makeClipRevealAnimation
说是一个点圆形渐变到全部显示,参数含义和 makeScaleUpAnimation 的一样。也是太快,没看出什么特别的。
clipReveal.onClick {
val compat = ActivityOptionsCompat.makeClipRevealAnimation(text, text.getWidth() / 2, text.getHeight() / 2, 0, 0)
start(it!!, compat)
}
makeSceneTransitionAnimation 单个 View
Scene 就是场景,两个 Activity 中的某些 View 协同完成过渡动画。
在两个 Activity 的布局文件中,要协同做动画的 View 要有一个属性 android:transitionName
并将值设为一样的。
第一个 Activity 的 xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
...
<ImageView
android:id="@+id/image1"
android:layout_width="230dp"
android:layout_height="78dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/timg"
android:transitionName="image_name" />
...
</LinearLayout>
要跳转的新 Activity 的 xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="@+id/image2"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@drawable/timg"
android:layout_alignParentBottom="true"
android:transitionName="image_name" />
</LinearLayout>
两个要协同的 ImageView 都有一句 android:transitionName="image_name"
。
sceneTransitionSingleView.onClick {
val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this@OptionActivity, image1, "image_name")
start(it!!, compat)
}
参数:
- Activity activity - 当前所在 Activity
- View sharedElement - 要协同过渡的 View,就是
共享元素
- String sharedElementName - 就是 xml 里定义的 transitionName
上面说到 ActivityCompat.finishAfterTransition(this)
没生效是在一处判断 shared elements
为 null 就返回执行普通的 finish 了,现在这里有 shared elements
了,发现返回原来页面确实也有动画效果了。
这种协同过渡用同类型甚至内容都差不多的 View 来做看着效果好,但就算让两个完全不一样的 View 做协同过渡,也是可以的,如第一个 Activity 的一个 Button,点击就跳转到新 Activity,就让这个 Button 和新 Activity 里的一个 TextView 做过渡,也是可以,效果还好,就是返回时有个突变。
<Button
android:id="@+id/sceneTransitionDifferent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="makeSceneTransitionAnimation(不同类型View过渡)"
android:textAllCaps="false"
android:transitionName="transition" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#ff0000"
android:textSize="12sp"
android:background="#ffffff"
android:padding="10dp"
android:layout_margin="10dp"
android:text="共享元素"
android:transitionName="transition" />
sceneTransitionDifferent.onClick {
val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this@OptionActivity, sceneTransitionDifferent, "transition")
start(it!!, compat)
}
makeSceneTransitionAnimation 多个 View
修改 xml 再给其它 View 也加上 android:transitionName。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
...
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:background="#669812"
android:text="我欲乘风归去"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:textSize="14sp"
android:transitionName="text_name" />
<ImageView
android:id="@+id/image1"
android:layout_width="230dp"
android:layout_height="78dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/timg"
android:transitionName="image_name" />
...
</LinearLayout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:background="#669812"
android:text="我欲乘风归去"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:textSize="26sp"
android:transitionName="text_name" />
<ImageView
android:id="@+id/image2"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@drawable/timg"
android:layout_alignParentBottom="true"
android:transitionName="image_name" />
</LinearLayout>
sceneTransitionMultiView.onClick {
val textPair = android.support.v4.util.Pair<View, String>(text, "text_name")
val imagePair = android.support.v4.util.Pair<View, String>(image1, "image_name")
val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this@OptionActivity, textPair, imagePair)
start(it!!, compat)
}
有共享元素时的动画效果
使用共享元素时 Activity 的效果
有三种:
- explode - 爆裂,从场景中间移动视图进入或者退出屏幕
- slide - 滑动,视图从场景的一个边缘进入或者退出屏幕
- android:slideEdge 属性控制滑动方向,取值可以是
LEFT, TOP, RIGHT, BOTTOM, START, END
- android:slideEdge 属性控制滑动方向,取值可以是
- fade - 淡入淡出,从场景添加或者移除一个视图时改变他的透明
可以指定 target,只在某个 View 或排除某个 View 上做动画。
主题中可以配置
- android:windowEnterTransition - 当 A start B 时,B 页面进入场景的 transition
- android:windowExitTransition - 当 A start B 时,A 页面退出场景的 transition
- android:windowReturnTransition - 当 B 返回 A 时,B 页面退出场景的 transition
- android:windowReenterTransition - 当 B 返回 A 时,A 页面进入场景的 transition
如果不在主题配置,在 Activity 的代码设置,如 getWindow().setEnterTransition(new Explode());
,那么
- setEnterTransition - B 中设置
- setExitTransition() - A 中设置
- setReturnTransition() - B 中设置
- setReenterTransition() - A 中设置
在 res/transiton
目录创建两个文件,可以定义其时间和插值器
transition_slide.xml
<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:slideEdge="right" />
transition_set.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together" >
<explode
android:duration="2000"
android:interpolator="@android:interpolator/accelerate_decelerate" />
<fade
android:duration="2000" />
</transitionSet>
然后配置主题
<item name="android:windowEnterTransition">@transition/transition_set</item>
<item name="android:windowReturnTransition">@transition/transition_slide</item>
如果不配置主题,也可以在 B 页面(不是跳转前的 A)代码设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val set = TransitionInflater.from(ctx).inflateTransition(R.transition.transition_set)
val slide = TransitionInflater.from(ctx).inflateTransition(R.transition.transition_slide)
window.enterTransition = set
window.returnTransition = slide
}
也可以不用 xml,全部代码配置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val set = TransitionSet()
set.ordering = TransitionSet.ORDERING_TOGETHER
val explode = Explode()
explode.duration = 2000
explode.interpolator = AccelerateInterpolator()
val fade = Fade()
fade.duration = 2000
fade.interpolator = AccelerateInterpolator()
set.addTransition(explode)
set.addTransition(fade)
window.enterTransition = set
val slide = Slide()
slide.duration = 2000
slide.slideEdge = Gravity.RIGHT
window.returnTransition = slide
}
共享元素间的效果
- changeBounds - 改变目标视图的布局边界
- android:resizeClip
- changeClipBounds - 裁剪目标视图边界
- changeTransform - 改变目标视图的缩放比例和旋转角度
android:reparent 是否追踪父容器的变化
-
android:reparentWithOverlay 默认 true。
When the parent change doesn't use an overlay, it affects the transforms of the child
- changeImageTransform - 改变目标图片的大小和缩放比例
- ChangeScroll
在 res/transition
目录下创建 transition_elements_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<changeBounds xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:resizeClip="true" />
创建 transition_elements_return_set.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together">
<changeTransform xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000" />
<changeClipBounds android:duration="2000"
android:resizeClip="true" />
</transitionSet>
配置主题
<item name="android:windowSharedElementEnterTransition">@transition/transition_elements_enter</item>
<item name="android:windowSharedElementReturnTransition">@transition/transition_elements_return_set</item>
也可以在 Activity B 中代码设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// val set = TransitionInflater.from(ctx).inflateTransition(R.transition.transition_elements_return_set)
// val enter = TransitionInflater.from(ctx).inflateTransition(R.transition.transition_elements_enter)
val set = TransitionSet()
set.ordering = TransitionSet.ORDERING_TOGETHER
val changeTransform = ChangeTransform()
changeTransform.duration = 2000
val changeClipBounds = ChangeClipBounds()
changeClipBounds.duration = 2000
set.addTransition(changeTransform)
set.addTransition(changeClipBounds)
//
val enter = ChangeBounds()
enter.duration = 2000
enter.resizeClip = true
window.sharedElementEnterTransition = enter
window.sharedElementReturnTransition = set
}
Overlap
To start an enter transition as soon as possible, use the Window.setAllowEnterTransitionOverlap() method on the called activity. This lets you have more dramatic enter transitions.
更鲜活的动画效果?没看出什么来。在 Activity B 中设置
window.allowEnterTransitionOverlap = true
window.allowReturnTransitionOverlap = true
或者配置主题
<item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:windowAllowReturnTransitionOverlap">true</item>
关于主题
看网上有些文章说必须在主题里设置 <item name="android:windowContentTransitions">true</item>
或者代码里在 setContentView()
之前 getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
,可我没设置什么问题都没有,都生效了啊。然后去看官方文档 https://developer.android.com/training/transitions/start-activity.html,又出现一个新的配置 <item name="android:windowActivityTransitions">true</item>
:
First, enable window content transitions with the android:windowActivityTransitions attribute when you define a style that inherits from the material theme.
这意思是说用 material theme 时才需要设置这个属性吗?而我 Demo 用的主题是 Theme.AppCompat.Light.DarkActionBar
。不太清楚到底是不是这个原因。