文章目录
前言
Android 5.0 以来, 我们可以方便的使用系统内置的转场动画. 平移, 淡入, 爆炸, 共享元素等. 当然大神也可以自定义复杂动画. 经过排列组合, 可以做出炫丽的转场效果
提示:以下是本篇文章正文内容,下面案例可供参考
一、Activity 转场:
无图无真相, 上图
1.三种常用转场
Explode
爆炸出入, 控件由屏幕中心 向四周扩散或汇聚.
// TransOneActivity 用双参的 startActivity()
startActivity(
Intent(this, TransTwoActivity::class.java),
ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle()
)
// TransTwoActivity 的 onCreate() 函数中:
window.enterTransition = Explode().setDuration(800)
// 如果退场动画跟进场动画一样, returnTransition 可以不设置; 这里可以注释掉
window.returnTransition = Explode().setDuration(800)
// 布局文件? 要什么布局, 随便写几个 View 就完事了;
重点:
系统会自动遍历屏幕上所有的 View, 并做对应动画处理;
enterTransition: 进场动画
returnTransition: 退场动画. 如果跟进场动画一样, 则可以不设置 (进场扩散, 则退场汇聚)
Slide
滑动平移, 默认是 向上滑入, 向下滑出
// TransOneActivity, 启动方法没变;
startActivity(
Intent(this, TransTwoActivity::class.java),
ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle()
)
// TransTwoActivity 的 onCreate() 函数中:
window.enterTransition = Slide(Gravity.START) // 左滑入
window.returnTransition = Slide(Gravity.END) // 右滑出
// 如果手动关闭页面, 则用 finishAfterTransition 替代 finish();
ActivityCompat.finishAfterTransition(this)
Slide 构造函数, 可以传一个 Gravity 常量; 可以从对应方向入场. 控件分步入场(带时间差);
如果不传参数, 则页面整体 向上滑入, 向下滑出;
- Gravity.LEFT, Gravity.START: 左侧出入;
- Gravity.TOP: 上方出入;
- Gravity.RIGHT, Gravity.END: 右侧出入;
- Gravity.BOTTOM: 底部出入;
Fade
整页面, 淡入淡出
// TransTwoActivity
window.enterTransition = Fade()
2.前页动画:
上面几个例子, 都是 被启动页 动画. 前页也可以有动画
假设 Activity A 转场启动 Activity B:
函数 | 意义 | 触发时机 |
---|---|---|
setExitTransition() | A的 前进关闭(离场)动画 | 启动B时执行, 早于B的入场动画 |
setEnterTransition( ) | B的 启动入场动画 | B被启动时 |
setReturnTransition() | B的 后退离场动画 | B关闭时 |
setReenterTransition() | A的 返场动画 | B关闭时, 回到A后执行 |
代码示例:
// TransOneActivity
window.reenterTransition = Slide(Gravity.START)
window.exitTransition = Slide(Gravity.END)
window.allowEnterTransitionOverlap = false // 允许进入时过度重叠
window.allowReturnTransitionOverlap = false // 允许返回时过度重叠
// TransTwoActivity 的 onCreate() 函数中:
window.enterTransition = Explode()
window.returnTransition = Explode()
这里就不贴动图了, 有兴趣的小伙伴可以自己试一下; 我们可以看到, 前页动画执行完毕后, 后页的动画才开始执行; 当下面两个属性设为 false 时, 才会这样;
参数 | 意义 |
---|---|
allowEnterTransitionOverlap | 默认true; 前后页面的动画, 几乎同时执行 |
allowReturnTransitionOverlap | 默认true; 返场时动画是否同时执行 |
3.单元素共享过度
说是元素共享. 实际上根本就不共享. 只是在元素前后页的位置上做了个渐变动画!
1.第一步: 设置 transitionName
xml中写死:
android:transitionName="trans_view"
代码指定:
ViewCompat.setTransitionName(view, "trans_view")
注意: 前后页的共享控件都要设置该属性;
2.第二步: 启动Activity 指定共享元素
startActivity(
Intent(this, TransTwoActivity::class.java),
ActivityOptionsCompat.makeSceneTransitionAnimation(
this, transView, "trans_view").toBundle()
)
使用三参的 makeSceneTransitionAnimation() 函数, 传入 View 和 transitionName;
3.第三步: …
确保: transitionName 设置好了
退出页面时, 使用: ActivityCompat.finishAfterTransition(this); back键 可以不管;
有时候, 新的页面可能 View还没准备好. 这时候我们想让动画稍晚一点执行:
- ActivityCompat.postponeEnterTransition(this): 暂停转场动画
- ActivityCompat.startPostponedEnterTransition(this): 开始转场动画
// 例如
ActivityCompat.postponeEnterTransition(this)
// 准备 view 的代码; 例如 ViewPager
...
ActivityCompat.startPostponedEnterTransition(this)
ActivityCompat 就是一个兼容类, 判断了一下 Android 版本;
细心的小伙伴有没有发现, 我上面的 gif 图中, 单图转场时的 scaleType 丝滑切换. 这是Android 自动帮我们处理的.
4.多元素共享:
多元素, 单元素 一样. 只区别在写法上. 需要借助 androidx.core.util.Pair 类
// 例如:
startActivity(
Intent(this, TransTwoActivity::class.java),
ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
androidx.core.util.Pair.create(binding.tvFour,"four_trans"),
androidx.core.util.Pair.create(binding.tvFive, "five_trans")
).toBundle()
)
二、页内控件转场动画
好, 上图:
1.示例代码
1. ChangeBounds
第一张图的动画效果, 检测view的 位置边界 创建移动和缩放动画;
val trans = ChangeBounds() // 并未使用, 默认貌似就是这种动画;
TransitionManager.beginDelayedTransition(binding.clRoot)
val layoutParams = binding.imgOne.layoutParams as ConstraintLayout.LayoutParams
if (type == 0) {
type = 1
layoutParams.endToEnd = ConstraintSet.PARENT_ID
layoutParams.startToStart = -1
layoutParams.width = 500
layoutParams.height = 280
binding.imgOne.layoutParams = layoutParams
} else {
type = 0
layoutParams.startToStart = ConstraintSet.PARENT_ID
layoutParams.endToEnd = -1
layoutParams.width = ConstraintSet.WRAP_CONTENT
layoutParams.height = ConstraintSet.WRAP_CONTENT
binding.imgOne.layoutParams = layoutParams
}
讲解:
- imgOne: 执行动画的View
- clRoot: 页面的 根ViewGroup
- 这里用了type来判断状态. 然后改变了View的 尺寸 和 位置
- 关键代码: TransitionManager.beginDelayedTransition(binding.clRoot);
动画执行只需要两步:
- 调用 beginDelayedTransition() 指定根布局, 指定动画;
- 改变 view 状态;
2.ChangeImageTransform
第二张图的动画效果, 检测 ImageView 的 ScaleType 属性变化, 创建动画;
// 这里用动画集合, 联合 ChangeBounds 同时变化位置和大小
val trans = TransitionSet()
trans.addTransition(ChangeBounds())
trans.addTransition(ChangeImageTransform())
TransitionManager.beginDelayedTransition(binding.clRoot, trans)
val layoutParams = binding.imgTwo.layoutParams as ConstraintLayout.LayoutParams
if (type == 0) {
type = 1
... //改变位置和大小的代码省略, 可参照 ChangeBounds 的示例;
binding.imgTwo.scaleType = ImageView.ScaleType.FIT_CENTER
} else {
type = 0
... //改变位置和大小的代码省略, 可参照 ChangeBounds 的示例;
binding.imgTwo.scaleType = ImageView.ScaleType.FIT_XY
}
3.Slide
第三张图的动画效果, 控制显示隐藏时, 让控件向对应方向(Gravity.XXX) 飞出;
val trans = Slide(Gravity.END)
TransitionManager.beginDelayedTransition(binding.clRoot, trans)
if(type == 0){
type = 1
binding.imgThree.visibility = View.GONE
binding.btnBack.visibility = View.VISIBLE
}else{
type = 0
binding.imgThree.visibility = View.VISIBLE
binding.btnBack.visibility = View.GONE
}
4.ChangeTransform
第四张图的动画效果, View 缩放 和 旋转;
val trans = TransitionSet()
trans.addTransition(ChangeBounds())
trans.addTransition(ChangeTransform())
TransitionManager.beginDelayedTransition(binding.clRoot, trans)
val layoutParams = binding.imgFour.layoutParams as ConstraintLayout.LayoutParams
if (type == 0) {
type = 1
layoutParams.endToEnd = ConstraintSet.PARENT_ID
layoutParams.startToStart = -1
binding.imgFour.layoutParams = layoutParams
binding.imgFour.scaleX = 0.6f
binding.imgFour.scaleY = 0.6f
binding.imgFour.rotation = 90f
} else {
type = 0
layoutParams.startToStart = ConstraintSet.PARENT_ID
layoutParams.endToEnd = -1
binding.imgFour.layoutParams = layoutParams
binding.imgFour.scaleX = 1f
binding.imgFour.scaleY = 1f
binding.imgFour.rotation = 0f
}
总结
用法来说比较简单, 先 beginDelayedTransition(root, trans), 然后改变view属性即可;
这些动画都继承自 Transition 类; 通过浏览它的子类,可以查看其它动画类型
3.RecycleView+ViewPager 图片转场
先上图:
我们先来分析一下:
- 已知, 图片转场时(前后都是ImageView), scaleType会丝滑过度, 像是 ChangeImageTransform 动画;
- 使用 ViewPager 需要等它内部 View 准备好
- ViewPager 滑动后, 返回时, 前页的转场View也得跟着切换
1.准备 Grid
就是类似于GridView 的图片 RecycleView
val mAdapter = SimpleAdapter(
R.layout.item_img_grid,
object : Handler<MultipleEntity.ImageEntity>() {
override fun onClick(view: View, info: MultipleEntity.ImageEntity) {
val index = imgRes.indexOf(info)
startActivityForResult(
Intent(applicationContext, TransThreeActivity::class.java).putExtra("index",index), 1,
ActivityOptionsCompat.makeSceneTransitionAnimation(this@TransOneActivity,view,"iv_trans_one").toBundle()
)
}
})
binding.rvRecycle.let {
it.layoutManager = GridLayoutManager(this, 3)
it.adapter = mAdapter
}
mAdapter.submitList(imgRes)
布局如下:
<!-- item_img_grid.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="item"
type="com.example.kotlinmvpframe.yiyou.entity.MultipleEntity.ImageEntity" />
<variable
name="handler"
type="com.example.kotlinmvpframe.test.testtwo.Handler" />
</data>
<ImageView
android:id="@+id/iv_img_item"
style="@style/img_wrap"
android:layout_width="match_parent"
android:layout_height="120dp"
android:scaleType="centerCrop"
android:onClick="@{v -> handler.onClick(v, item)}"
android:tag="@{item.res}"
app:imgRes="@{item.res}"/>
</layout>
博主这里使用的封装好的 ListAdapter; 采用的数据绑定模式; 想了解的点这里
反正就是准备 GridLayoutManager 的 RecycleView; 然后图片点击事件如下:
startActivityForResult(
// 跳转大图预览, index 为图片索引, 用于默认显示第几张图;
// 这里博主前后页资源一样, 就没传图片集合数据;
Intent(applicationContext, TransThreeActivity::class.java).putExtra("index",index),
// requestCode, 后页返回时, 前页需变更 转场View
1,
// view 是RecycleView中当前点击的图片;
ActivityOptionsCompat.makeSceneTransitionAnimation(this@TransOneActivity,view,"iv_trans_one").toBundle()
)
2.准备 ViewPager
在 onCreate 中:
override fun onCreate(savedInstanceState: Bundle?) {
...
postponeEnterTransition() // 暂停动画, 可以使用兼容类
initViewPager() // 初始化ViewPager
}
private fun initViewPager() {
// 又是 ListAdapter, MVVM模式
val mAdapter = SimpleAdapter(R.layout.item_img_pager, object : Handler<ImageEntity>() {
override fun onClick(view: View, info: ImageEntity) {
finishAfterTransition()
}
})
// 用的 ViewPager2, 所以适配器跟 RecycleView 的一样;
// 用 ViewPager 也无所谓, 只关心 item 中 ImageView 的 transitionName 即可
binding.vpPager.adapter = mAdapter
mAdapter.submitList(imgRes)
binding.vpPager.setCurrentItem(intent.getIntExtra("index", 0), false)
// 当 ViewPager 准备完毕后, 需要再次启动转场动画. 这里监听一下
// 这里博主的 Activity 实现了 ViewTreeObserver.OnGlobalLayoutListener
binding.vpPager.viewTreeObserver.addOnGlobalLayoutListener(this)
}
// OnGlobalLayoutListener 的回调
override fun onGlobalLayout() {
ActivityCompat.startPostponedEnterTransition(this) // 再次开启动画
binding.vpPager.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
布局文件:
<!-- Activity中的 ViewPager2 -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- item_img_pager.xml; !!! 注意这里的 android:transitionName !!! -->
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="item"
type="com.example.kotlinmvpframe.yiyou.entity.MultipleEntity.ImageEntity" />
<variable
name="handler"
type="com.example.kotlinmvpframe.test.testtwo.Handler" />
</data>
<ImageView
android:id="@+id/iv_img_item"
style="@style/img_wrap"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:transitionName="iv_trans_one"
app:imgRes="@{item.res}"
android:onClick="@{v -> handler.onClick(v, item)}"/>
</layout>
3.返回时处理
viewpager 滑动后, position 变了. setResult. 控制前页转场view
// ViewPager 页
override fun finishAfterTransition() {
// 通知前页, 最新的索引位置;
setResult(RESULT_OK, Intent().putExtra("index",binding.vpPager.currentItem))
super.finishAfterTransition()
}
// 前页, GridView 页
override fun onActivityReenter(resultCode: Int, data: Intent?) {
data ?: return
val index = data.getIntExtra("index", 0)
// 通过 Tag 找到 ImagerView
val img = binding.rvRecycle.findViewWithTag<ImageView>(imgRes[index].res)
if (img != null) {
setExitSharedElementCallback(object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String?>,
sharedElements: MutableMap<String?, View?>
) {
names.clear()
sharedElements.clear()
sharedElements["iv_trans_one"] = img
setExitSharedElementCallback(null as SharedElementCallback?)
}
})
}
}
onActivityReenter: 通过过度动画返回的时候会调用.
onActivityResult: 不能使用, 因为它调用时, 后页的退场动画已经执行完了.
总结
没有总结
上一篇: Activity: 一、生命周期,启动模式,FLAG,Context
下一篇: Activity: 三、Android Result API (酝酿中…)