You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Activity间SharedElementTransition及横竖屏切换时的RecyclerView动画?

嘿,这两个问题都是RecyclerView结合共享元素动画的典型场景,我来一步步给你拆解解决思路和具体实现方式~

问题1:Activity间SharedElementTransition为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)
    }
    
问题2:横向→纵向RecyclerView切换+横竖屏切换动画

这个场景更复杂,因为两个RecyclerView的布局方向不同,条目位置、尺寸都有变化,需要自定义动画逻辑,同时处理横竖屏切换的状态保存。

一、Activity间横向→纵向RecyclerView切换动画

核心是自定义共享元素的变换动画,让系统能处理布局方向变化带来的视图形变,同时确保对应条目有相同的transitionName:

  1. 给两个RecyclerView的对应条目设置相同的唯一transitionName(和问题1的方式一致)。

  2. 自定义共享元素变换动画:
    默认的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
        }
    }
    
  3. 启动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())
    }
    
  4. 目标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的效果,需要处理配置变化的状态保存,同时利用系统的共享元素动画机制:

  1. 允许系统重新创建Activity(不推荐用configChanges跳过重建,因为会丢失系统的转场支持)。

  2. 保存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())
    }
    
  3. 恢复状态并配置动画:
    在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()
    }
    
  4. 触发横竖屏切换的动画:
    当用户切换横竖屏时,系统会重建Activity,此时之前设置的transitionName会让系统自动匹配对应的条目View,执行自定义的变换动画。如果需要更精准的控制,可以在onStart中再次确认View的状态,确保动画正常触发。

小技巧补充

  • 如果条目包含图片,建议配合图片加载库的过渡动画(比如Glide的DrawableTransitionOptions.withCrossFade()),让图片的显示和共享元素动画更协调。
  • 若遇到动画不触发的情况,检查transitionName是否完全一致,以及View是否已经完成测量布局(可以用View.post()延迟操作)。

内容的提问来源于stack exchange,提问作者Islam Assem

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:03:15