Activity: 二、转场动画, RecycleView+ViewPager图片预览


前言

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);

动画执行只需要两步:

  1. 调用 beginDelayedTransition() 指定根布局, 指定动画;
  2. 改变 view 状态;

2.ChangeImageTransform

第二张图的动画效果, 检测 ImageViewScaleType 属性变化, 创建动画;

// 这里用动画集合, 联合 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 图片转场

先上图:


在这里插入图片描述


我们先来分析一下:

  1. 已知, 图片转场时(前后都是ImageView), scaleType会丝滑过度, 像是 ChangeImageTransform 动画;
  2. 使用 ViewPager 需要等它内部 View 准备好
  3. 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 (酝酿中…)

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值