文章目录
- 1. 介绍
- 2. 实现方法
- 3. 举例演示
- 3.1 举例一:普通页面间共享元素转场动画
- 3.2 举例二:列表页面共享元素转场动画
- 4. 总结
1. 介绍
在Android开发中,经常会有页面转场的动画效果。普通的转场动画不过是左进右出,渐显渐隐,局限于整个页面。而对于页面中的某个元素,尤其是图片,如果能很自然的过渡到下一个页面,那么用户体验会非常丝滑。这就是Android中的共享元素动画
来看下效果吧:
- 场景一:普通页面间共享元素转场动画
- 场景二:列表页面共享元素转场动画
2. 实现方法
Android中提供了 ActivityOptionsCompat.makeSceneTransitionAnimation
方法来实现场景转场动画,配合xml中的属性android:transitionName
,在Activity跳转时指定共享的元素View,生成bundle对象,然后传入startActivity方法中。系统便会自动为你实现上述效果。
比如页面 A 跳到页面 B,共享页面里的两个 ImageView 做动画,那么在页面A startActivity 时,就需要使用ActivityOptionsCompat.makeSceneTransitionAnimation
生成 Bundle:
public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity,
@NonNull View sharedElement, @NonNull String sharedElementName)
makeSceneTransitionAnimation 方法需要提供三个参数:
- Activity activity :包含共享元素的 activity,也就是当前 activity;
- View sharedElement: 需要有共享动画效果的 View,也就是页面A的 ImageView;
- String sharedElementName: 共享元素的名字,随便起个名字,只要跟目标页面xml里的
android:transitionName
一致即可
该方法返回值是 ActivityOptionsCompat,需要调用 toBundle 方法,将其转换为 Activity 间数据传递用的 Bundle 对象。
接着,调用 startActivity(it, bundle)
方法,将bundle 传入即可。
在页面B中,需要有对应的 ImageView,在它的xml布局里,需要添加属性android:transitionName
,标明它是目标的共享元素。这样页面A中的 ImageView 就能共享到页面B的 ImageView上了。它们之间的大小位置等差异会以动画的形式自然过渡。
核心代码如下:
fun jump(view: View) {
// 生成转场动画的bundle对象
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, imgView!!, "share_image")
.toBundle()
// 如果有参数传递,可以这么添加
bundle?.putString("key1", "value1")
Intent(this, ShareElementActivity2::class.java).let {
it.putExtras(bundle!!)
startActivity(it, bundle!!)
}
}
xml代码
<ImageView
android:id="@+id/imageView"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/test1"
android:transitionName="share_image"
/>
3. 举例演示
接下来,我们用完整的例子来演示共享动画效果。(本案例使用kotlin语言演示,Java也是同理; 本案例中使用到的资源文件test1.jpeg 是一张普通的测速图片,你可以随便找一张图片代替。)
3.1 举例一:普通页面间共享元素转场动画
效果描述:我们将实现两个页面之间的跳转,由 ShareElementActivity1 跳转到 ShareElementActivity2。这两个页面里都有一个 ImageView 展示一张图片,这两张图片的大小位置有差异,前者60x60, 后者200x200。我们要实现的共享动画效果是:页面跳转过程中,这两张图片自然过渡,看起来是前面的小图,自然放大到后者的动画一样,并且整个页面也是自然过渡,没有生硬切换的迹象;页面返回时,大图自然缩小到小图,也是非常自然的动画过渡。具体效果可参考章节1中的效果图。
ShareElementActivity1代码:
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.core.app.ActivityOptionsCompat
import com.example.mytest.R
class ShareElementActivity1 : AppCompatActivity() {
private var imgView: ImageView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_share_element1)
imgView = findViewById(R.id.imageView)
}
fun jump(view: View) {
val bundle =
ActivityOptionsCompat.makeSceneTransitionAnimation(this, imgView!!, "share_image")
.toBundle()
bundle?.putString("key1", "value1")
Intent(this, ShareElementActivity2::class.java).let {
it.putExtras(bundle!!)
startActivity(it, bundle!!)
}
}
}
对应布局文件:activity_share_element1.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".animation.ShareElementActivity1"
tools:ignore="MissingDefaultResource">
<ImageView
android:id="@+id/imageView"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginTop="96dp"
android:scaleType="centerCrop"
android:src="@drawable/test1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="跳转到share2"
android:onClick="jump"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
目标页面:ShareElementActivity2
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import com.example.mytest.R
class ShareElementActivity2 : AppCompatActivity() {
private var imgView: ImageView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_share_element2)
imgView = findViewById(R.id.imageView)
}
fun back(view: View) {
// 调用this.finish()不会有共享元素转场退出效果
// this.finish()
// 模拟返回键,调用系统的back按键,会有共享元素转场退出效果
onBackPressed()
}
}
对应布局文件:activity_share_element2.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".animation.ShareElementActivity2"
tools:ignore="MissingDefaultResource">
<ImageView
android:id="@+id/imageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="96dp"
android:scaleType="centerCrop"
android:src="@drawable/test1"
android:transitionName="share_image"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.459"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="返回"
android:onClick="back"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
注意:
如果要手动从目标页面返回,不能直接调finish方法,那样就不会走系统的共享元素动画了。应该调用系统的back按键onBackPressed
,这样就会有共享元素转场退出效果。
3.2 举例二:列表页面共享元素转场动画
接下来再来举个更实用的例子,从列表上点击进入详情页,应该是非常常见的场景。这里更适合这种共享元素无缝切换的效果,给用户的感觉会非常沉浸式。
列表页ShareElementListActivity,这里我们用到了 RecyclerView+Adapter来实现一个简单的列表
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.core.app.ActivityOptionsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.mytest.R
import com.example.mytest.recyclerviewclick.RcvAdapter
class ShareElementListActivity : AppCompatActivity() {
private var rcv: RecyclerView? = null
private var adapter: RcvAdapter? = null
private var dataList: MutableList<String> = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_share_element_list)
initData()
rcv = findViewById(R.id.rcv)
adapter = RcvAdapter(this, dataList).apply {
onItemClickListener = object: RcvAdapter.OnRcvItemClickListener {
override fun onItemClicked(position: Int, view: View) {
val imageView = view.findViewById<ImageView>(R.id.iv_cover)
val bundle =
ActivityOptionsCompat.makeSceneTransitionAnimation(this@ShareElementListActivity, imageView!!, "share_image")
.toBundle()
Intent(this@ShareElementListActivity, ShareElementActivity2::class.java).let {
this@ShareElementListActivity.startActivity(it, bundle)
}
}
}
}
rcv?.adapter = adapter
rcv?.layoutManager = LinearLayoutManager(this)
}
private fun initData() {
for (i in 0..100) {
dataList.add("条目 $i")
}
}
}
列表数据适配器类:RcvAdapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.example.mytest.R
class RcvAdapter(val context: Context, val dataList: MutableList<String>) :
RecyclerView.Adapter<RcvAdapter.RcvViewHolder>() {
var onItemClickListener: OnRcvItemClickListener? = null
class RcvViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var container: ViewGroup? = null
private var tvContent: TextView? = null
init {
container = itemView.findViewById(R.id.ll_item_container)
tvContent = itemView.findViewById(R.id.tv_content)
}
fun bind(textContent: String, position: Int) {
tvContent?.text = textContent
container?.isSelected = true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RcvViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.list_item_layout, parent, false)
return RcvViewHolder(view)
}
override fun onBindViewHolder(holder: RcvViewHolder, position: Int) {
holder.bind(dataList.get(position), position)
holder.itemView.setOnClickListener {
Toast.makeText(it.context, "点击了$position", Toast.LENGTH_SHORT).show()
onItemClickListener?.onItemClicked(position, it)
}
}
override fun getItemCount(): Int {
return dataList.size
}
interface OnRcvItemClickListener {
fun onItemClicked(position: Int, view: View)
}
}
布局文件:
- ShareElementListActivity页面布局文件:activity_share_element_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".animation.ShareElementListActivity"
tools:ignore="MissingDefaultResource">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rcv"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/list_item_layout"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
- RcvAdapter用到的一个列表Item的布局文件:list_item_layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/ll_item_container"
android:layout_width="match_parent"
android:layout_height="100dp">
<ImageView
android:id="@+id/iv_cover"
android:layout_width="100dp"
android:layout_height="80dp"
android:scaleType="centerCrop"
android:src="@drawable/test1"
android:layout_marginStart="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintStart_toEndOf="@id/iv_cover"
app:layout_constraintTop_toTopOf="@id/iv_cover"
app:layout_constraintBottom_toBottomOf="@id/iv_cover"
>
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="你好啊"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_content_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="你好啊"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
目标页面仍是 ShareElementActivity2,代码在3.1 例子里,不再赘述。
4. 总结
- 本文介绍了Android中共享元素转场动画的效果演示,尤其是比较常见的列表页面共享元素转场动画。
- 介绍了实现方法:通过Android中提供了
ActivityOptionsCompat.makeSceneTransitionAnimation
方法来实现场景转场动画,配合xml中的属性android:transitionName
,在Activity跳转时指定共享的元素View,生成bundle对象,然后传入startActivity方法中,系统便会自动为你实现上述效果。 - 提供了两个完整的代码示例演示了共享元素转场动画效果,读者可以非常方便的复制代码来实现文章中的效果,不会有那种虎头蛇尾,缺少各种上下文代码的烂尾教程所带来的困扰🤣。
如果你对这篇文章有更好的建议,欢迎评论留言交流;
如果这篇文章对你有用,欢迎支持!感谢支持哦😊~