Android 共享元素转场效果

概述

Android 共享元素转场(Shared Element Transition)是一种在 Activity 或 Fragment 之间进行平滑过渡的动画效果。它允许指定 UI 元素在两个界面间"共享",从而创建连续的视觉体验,让用户感觉元素是从一个界面"移动"到了另一个界面。

Activity之间的共享元素转场

简单使用

源Activity:

  • 在布局文件中,为共享元素添加 android:transitionName 属性
  • ActivityOptionsCompat.makeSceneTransitionAnimation():配置共享元素
class OneActivity : AppCompatActivity() { 
    private val image = R.drawable.cherry

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_one)
        imageView.setImageResource(drawableRes)

        imageView.setOnClickListener {
            val intent = Intent(context, TwoActivity::class.java).apply {
                putExtra("image", image)
            }
            // 共享元素配置
            val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                this@OneActivity,
                imageView,
                "image_transition"
            )
            startActivity(intent, options.toBundle())
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center"
              android:orientation="vertical"
              tools:context=".OneActivity">

    <ImageView
               android:id="@+id/imageView"
               android:layout_width="match_parent"
               android:layout_height="100dp"
               android:transitionName="image_transition" />

</LinearLayout>

目标Activity:

  • 在布局文件中,为对应元素添加 android:transitionName 属性
  • postponeEnterTransition():暂停转场,等待资源加载完成
  • startPostponedEnterTransition():开始转场
class TwoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_two)
        val image = intent.getIntExtra("image", -1)
        imageView.setImageResource(image)
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center"
              android:orientation="vertical"
              tools:context=".TwoActivity">

    <ImageView
               android:id="@+id/imageView"
               android:layout_width="match_parent"
               android:layout_height="300dp"
               android:scaleType="centerCrop"
               android:transitionName="image_transition" />
</LinearLayout>

多个共享元素处理

源Activity:

class SrcActivity : AppCompatActivity() {
    private lateinit var context: Context
    private lateinit var imageView: ImageView
    private lateinit var textView: TextView

    private val image = R.drawable.cherry
    private val text = "hello world"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_src)
        context = this
        imageView = findViewById(R.id.imageView)
        textView = findViewById(R.id.textView)

        imageView.setImageResource(image)
        textView.text = text

        imageView.setOnClickListener {
            val intent = Intent(context, DestActivity::class.java).apply {
                putExtra("image", image)
                putExtra("text", text)
            }
            val options = ActivityOptions.makeSceneTransitionAnimation(
                this@SrcActivity,
                android.util.Pair.create(imageView, "image_transition"),
                android.util.Pair.create(textView, "text_transition"),
            )
            startActivity(intent, options.toBundle())
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/main"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center"
              android:orientation="vertical"
              tools:context=".SrcActivity">

    <ImageView
               android:id="@+id/imageView"
               android:layout_width="match_parent"
               android:layout_height="100dp"
               android:transitionName="image_transition" />

    <TextView
              android:id="@+id/textView"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:transitionName="text_transition" />
</LinearLayout>

目标Activity:

class DestActivity : AppCompatActivity() {
    private lateinit var imageView: ImageView
    private lateinit var textView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_dest)
        imageView = findViewById(R.id.imageView)
        textView = findViewById(R.id.textView)

        val image = intent.getIntExtra("image", -1)
        val text = intent.getStringExtra("text") ?: ""
        imageView.setImageResource(image)
        textView.text = text
    }
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:orientation="vertical"
             tools:context=".DestActivity">

    <ImageView
               android:id="@+id/imageView"
               android:layout_width="match_parent"
               android:layout_height="300dp"
               android:scaleType="centerCrop"
               android:transitionName="image_transition" />

    <TextView
              android:id="@+id/textView"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_gravity="bottom|center_horizontal"
              android:scaleType="centerCrop"
              android:textSize="24sp"
              android:transitionName="text_transition" />
</FrameLayout>

Fragment之间的共享元素转场

源Fragment:

class SrcFragment : Fragment() {
    private lateinit var imageView: ImageView
    private lateinit var textView: TextView

    private val image = R.drawable.cherry
    private val text = "hello world"

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_src, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        imageView = view.findViewById(R.id.imageView)
        textView = view.findViewById(R.id.textView)

        imageView.setImageResource(image)
        textView.text = text

        imageView.setOnClickListener {
            parentFragmentManager.beginTransaction()
                .setReorderingAllowed(true) // 启用转场重排序
                .addSharedElement(imageView, "image_transition")
                .addSharedElement(textView, "text_transition")
                .replace(R.id.fragment_container, DestFragment.newInstance(image, text))
                .addToBackStack(null)
                .commit()
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<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:gravity="center"
    android:orientation="vertical"
    tools:context=".SrcFragment">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:transitionName="image_transition" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:transitionName="text_transition" />
</LinearLayout>

目标Fragment:

class DestFragment : Fragment() {
    private lateinit var imageView: ImageView
    private lateinit var textView: TextView

    private var image = -1
    private var text = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            image = it.getInt("image", -1)
            text = it.getString("text", "")
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_dest, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        imageView = view.findViewById(R.id.imageView)
        textView = view.findViewById(R.id.textView)

        // 配置共享元素进入转场
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
            .inflateTransition(android.R.transition.move)

        // 配置共享元素返回转场
        sharedElementReturnTransition = TransitionInflater.from(requireContext())
            .inflateTransition(android.R.transition.move)

        // 配置普通元素的进入和退出转场
        enterTransition = Fade()
        exitTransition = Fade()

        imageView.setImageResource(image)
        textView.text = text
    }

    companion object {
        fun newInstance(image: Int, text: String) = DestFragment().apply {
            arguments = Bundle().apply {
                putInt("image", image)
                putString("text", text)
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    tools:context=".DestFragment">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:scaleType="centerCrop"
        android:transitionName="image_transition" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center_horizontal"
        android:scaleType="centerCrop"
        android:textSize="24sp"
        android:transitionName="text_transition" />
</FrameLayout>

与RecyclerView配合使用

数据模型:

class Item(val image: Int, val text: String)

Adapter:

class ListAdapter(
    private val list: List<Item>,
    private val onItemClick: (Item, View, View) -> Unit
) : RecyclerView.Adapter<ListAdapter.ViewHolder>() {
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false))
    }

    override fun onBindViewHolder(
        holder: ViewHolder,
        position: Int
    ) {
        holder.imageView.transitionName = "image_transition"
        holder.textView.transitionName = "text_transition"
        list[position].let { item ->
            holder.imageView.setImageResource(item.image)
            holder.textView.text = item.text
            holder.itemView.setOnClickListener {
                onItemClick(item, holder.imageView, holder.textView)
            }
        }
    }

    override fun getItemCount() = list.size

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView = itemView.findViewById<ImageView>(R.id.imageView)
        val textView = itemView.findViewById<TextView>(R.id.textView)
    }
}

使用:

class ListActivity : AppCompatActivity() {
    private lateinit var rv: RecyclerView

    private val list = mutableListOf<Item>().apply {
        for (i in 1..10) {
            add(Item(R.drawable.apple, "苹果"))
            add(Item(R.drawable.cherry, "樱桃"))
            add(Item(R.drawable.pear, "梨子"))
            add(Item(R.drawable.mango, "芒果"))
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_list)

        rv = findViewById(R.id.rv)
        val adapter = ListAdapter(list) { item, imageView, textView ->
            if (imageView.isAttachedToWindow && textView.isAttachedToWindow) {
                val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                    this,
                    Pair.create(imageView, "image_transition"),
                    Pair.create(textView, "text_transition")
                )
                val intent = Intent(this, DestActivity::class.java).apply {
                    putExtra("image", item.image)
                    putExtra("text", item.text)
                }
                startActivity(intent, options.toBundle())
            }
        }
        rv.adapter = adapter
    }
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐