Android中的沉浸式丝滑转场之共享元素转场动画

1. 介绍

在Android开发中,经常会有页面转场的动画效果。普通的转场动画不过是左进右出,渐显渐隐,局限于整个页面。而对于页面中的某个元素,尤其是图片,如果能很自然的过渡到下一个页面,那么用户体验会非常丝滑。这就是Android中的共享元素动画

来看下效果吧:

  1. 场景一:普通页面间共享元素转场动画
    共享元素转场动画
  2. 场景二:列表页面共享元素转场动画
    共享列表元素转场动画

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

布局文件:

  1. 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>
  1. 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. 总结

  1. 本文介绍了Android中共享元素转场动画的效果演示,尤其是比较常见的列表页面共享元素转场动画
  2. 介绍了实现方法:通过Android中提供了 ActivityOptionsCompat.makeSceneTransitionAnimation 方法来实现场景转场动画,配合xml中的属性android:transitionName,在Activity跳转时指定共享的元素View,生成bundle对象,然后传入startActivity方法中,系统便会自动为你实现上述效果。
  3. 提供了两个完整的代码示例演示了共享元素转场动画效果,读者可以非常方便的复制代码来实现文章中的效果,不会有那种虎头蛇尾,缺少各种上下文代码的烂尾教程所带来的困扰🤣。

如果你对这篇文章有更好的建议,欢迎评论留言交流;
如果这篇文章对你有用,欢迎支持!感谢支持哦😊~

  • 17
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

子林Android

感谢老板,老板大气!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值