如何实现Activity间SharedElementTransition及横竖屏切换时的RecyclerView动画?
嘿,这两个问题都是RecyclerView结合共享元素动画的典型场景,我来一步步给你拆解解决思路和具体实现方式~
RecyclerView的条目是动态生成的,默认的共享元素动画没法直接识别这些动态View,所以核心是提前给要共享的View设置唯一标识,让系统能在两个Activity间精准匹配。具体步骤如下:
给RecyclerView条目中的共享View设置唯一
transitionName:
在Adapter的onBindViewHolder方法里,给每个条目中要共享的元素(比如图片、标题)设置独一无二的transitionName,比如用条目的ID做后缀,避免冲突:override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { val item = itemList[position] // 给共享View设置唯一标识 holder.itemView.iv_thumbnail.transitionName = "item_thumbnail_${item.id}" // 其他绑定逻辑... }启动第二个Activity时传入共享元素:
当用户点击条目时,获取被点击的共享View,用ActivityOptionsCompat打包传入:holder.itemView.setOnClickListener { val clickedThumbnail = holder.itemView.iv_thumbnail val intent = Intent(this, DetailActivity::class.java).apply { putExtra("ITEM_ID", item.id) } val options = ActivityOptionsCompat.makeSceneTransitionAnimation( this, clickedThumbnail, clickedThumbnail.transitionName ) startActivity(intent, options.toBundle()) }在目标Activity中匹配共享View:
打开DetailActivity后,找到对应的共享View,设置和源Activity完全相同的transitionName。如果目标View是异步加载的(比如图片从网络获取),要先延迟动画,等View准备就绪再启动:override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_detail) // 先延迟进入动画,等View加载完成 supportPostponeEnterTransition() val itemId = intent.getIntExtra("ITEM_ID", 0) val detailThumbnail = findViewById<ImageView>(R.id.iv_detail_thumbnail) detailThumbnail.transitionName = "item_thumbnail_$itemId" // 比如用Glide加载图片,加载完成后启动动画 Glide.with(this) .load(getItemImageUrl(itemId)) .listener(object : RequestListener<Drawable> { override fun onResourceReady( resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean ): Boolean { // View准备好,启动延迟的动画 supportStartPostponedEnterTransition() return false } // 其他回调... }) .into(detailThumbnail) }
这个场景更复杂,因为两个RecyclerView的布局方向不同,条目位置、尺寸都有变化,需要自定义动画逻辑,同时处理横竖屏切换的状态保存。
一、Activity间横向→纵向RecyclerView切换动画
核心是自定义共享元素的变换动画,让系统能处理布局方向变化带来的视图形变,同时确保对应条目有相同的transitionName:
给两个RecyclerView的对应条目设置相同的唯一transitionName(和问题1的方式一致)。
自定义共享元素变换动画:
默认的ChangeBounds动画只能处理简单的位置缩放,我们可以继承它来定制动画的插值器、时长,甚至添加额外的效果:class CrossOrientationItemTransition : ChangeBounds() { override fun createAnimator( sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues? ): Animator? { val baseAnimator = super.createAnimator(sceneRoot, startValues, endValues) // 自定义动画参数,比如用减速插值器让动画更自然 baseAnimator?.apply { interpolator = DecelerateInterpolator() duration = 600 } return baseAnimator } }启动Activity时配置自定义动画:
在源Activity中,设置进入和返回的共享元素动画,再启动目标Activity:holder.itemView.setOnClickListener { val clickedView = holder.itemView.iv_thumbnail val intent = Intent(this, VerticalListActivity::class.java).apply { putExtra("SELECTED_ITEM_ID", item.id) } // 设置自定义的共享元素动画 window.sharedElementEnterTransition = CrossOrientationItemTransition() window.sharedElementReturnTransition = CrossOrientationItemTransition() val options = ActivityOptionsCompat.makeSceneTransitionAnimation( this, clickedView, clickedView.transitionName ) startActivity(intent, options.toBundle()) }目标Activity等待RecyclerView加载完成:
纵向RecyclerView的条目位置是动态计算的,所以同样要延迟动画,等RecyclerView布局完成后再启动:override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_vertical_list) supportPostponeEnterTransition() val rvVertical = findViewById<RecyclerView>(R.id.rv_vertical) rvVertical.layoutManager = LinearLayoutManager(this) val adapter = VerticalItemAdapter(itemList) rvVertical.adapter = adapter // 监听RecyclerView布局完成,启动动画 rvVertical.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { rvVertical.viewTreeObserver.removeOnGlobalLayoutListener(this) supportStartPostponedEnterTransition() } }) }
二、同一Activity横竖屏切换的条目动画效果
要实现类似GIF的效果,需要处理配置变化的状态保存,同时利用系统的共享元素动画机制:
允许系统重新创建Activity(不推荐用
configChanges跳过重建,因为会丢失系统的转场支持)。保存RecyclerView的状态和选中条目:
在onSaveInstanceState中记录当前选中的条目ID和RecyclerView的滚动状态:private var selectedItemId = -1 override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putInt("SELECTED_ITEM_ID", selectedItemId) outState.putParcelable("RV_LAYOUT_STATE", rvList.layoutManager?.onSaveInstanceState()) }恢复状态并配置动画:
在onCreate中恢复之前的状态,同时设置横竖屏切换的共享元素动画:override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val rvList = findViewById<RecyclerView>(R.id.rv_list) // 根据当前屏幕方向设置布局管理器 val layoutManager = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) } else { LinearLayoutManager(this) } rvList.layoutManager = layoutManager val adapter = ItemAdapter(itemList) rvList.adapter = adapter // 恢复状态 savedInstanceState?.let { selectedItemId = it.getInt("SELECTED_ITEM_ID") rvList.layoutManager?.onRestoreInstanceState(it.getParcelable("RV_LAYOUT_STATE")) // 找到选中的条目View,设置transitionName val selectedPosition = itemList.indexOfFirst { it.id == selectedItemId } rvList.post { val selectedView = rvList.findViewHolderForAdapterPosition(selectedPosition)?.itemView?.iv_thumbnail selectedView?.transitionName = "item_thumbnail_$selectedItemId" } } // 设置横竖屏切换的共享元素动画 window.sharedElementEnterTransition = CrossOrientationItemTransition() window.sharedElementReturnTransition = CrossOrientationItemTransition() }触发横竖屏切换的动画:
当用户切换横竖屏时,系统会重建Activity,此时之前设置的transitionName会让系统自动匹配对应的条目View,执行自定义的变换动画。如果需要更精准的控制,可以在onStart中再次确认View的状态,确保动画正常触发。
小技巧补充
- 如果条目包含图片,建议配合图片加载库的过渡动画(比如Glide的
DrawableTransitionOptions.withCrossFade()),让图片的显示和共享元素动画更协调。 - 若遇到动画不触发的情况,检查transitionName是否完全一致,以及View是否已经完成测量布局(可以用
View.post()延迟操作)。
内容的提问来源于stack exchange,提问作者Islam Assem

