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. 提供了两个完整的代码示例演示了共享元素转场动画效果,读者可以非常方便的复制代码来实现文章中的效果,不会有那种虎头蛇尾,缺少各种上下文代码的烂尾教程所带来的困扰🤣。

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

### 回答1: CentOS 7上启动httpd服务失败可能有多种原因,以下是一些常见的解决方法: 1. 检查httpd配置文件是否正确:可以使用命令`httpd -t`检查httpd配置文件是否正确,如果有错误,需要修改配置文件。 2. 检查端口是否被占用:可以使用命令`netstat -tlnp`查看端口是否被占用,如果被占用需要释放端口或修改httpd配置文件中的端口号。 3. 检查httpd服务是否安装:可以使用命令`rpm -qa | grep httpd`查看httpd服务是否安装,如果没有安装需要先安装httpd服务。 4. 检查httpd服务是否启动:可以使用命令`systemctl status httpd`查看httpd服务是否启动,如果没有启动需要使用命令`systemctl start httpd`启动httpd服务。 5. 检查SELinux是否开启:如果SELinux开启,可能会导致httpd服务启动失败,需要使用命令`setenforce 0`关闭SELinux,或者修改SELinux策略。 以上是一些常见的解决方法,如果以上方法都无法解决问题,可以查看httpd服务日志文件,找到具体的错误信息,然后根据错误信息进行解决。 ### 回答2: CentOS 7上的httpd服务启动失败可能有多种原因。以下列出了一些常见问题和解决方法: 1. 端口被占用 当httpd试图占用已被其他程序占用的端口时会启动失败。此时可以通过使用`netstat -tunlp`命令检查端口占用情况,然后杀死占用该端口的进程及时释放端口。或者修改httpd的配置文件,将端口修改为未被占用的端口。 2. 配置文件错误 有时httpd服务的配置文件中可能出现错误,例如语法错误或路径错误等等。在启动httpd服务之前,可以使用`apachectl configtest`命令进行检查,如果输出“Syntax OK”,则表示配置文件没有错误。如果出现错误,则需要根据错误提示进行相应修改。 3. 依赖关系问题 如果httpd依赖的其他程序或库缺失,也会导致启动失败。可以通过使用`systemctl status httpd.service`命令来查看httpd服务状态,如果输出“Failed to start”或“Loaded: failed”,则需要检查依赖关系是否完整。 4. SELinux问题 当SELinux启用时,有时会导致httpd服务启动失败。在这种情况下,可以在SELinux上禁用httpd服务,或者修改httpd配置文件解决SELinux相关的问题。 5. 用户权限问题 httpd服务的启动可能需要特定的用户权限。如果使用的用户权限不够,则无法启动。可以尝试使用root用户启动httpd服务,或者根据需要修改相应的用户权限。 ### 回答3: CentOS 7中的Apache HTTP服务器(httpd)是一个常见的Web服务器,如果遇到httpd服务启动失败的情况,可能会影响服务器正常的工作和对外服务的稳定性。本文将提供一些可能会导致httpd服务启动失败的原因,并给出相应的解决方法。 1. 端口被占用 如果端口被其他进程占用,httpd服务就无法启动。可以通过 netstat -tulpn 命令查看端口占用情况,并杀死占用该端口的进程。如果端口被 httpd 服务自身占用,可以通过 systemctl restart httpd 命令重启 httpd 服务;如果是其他进程占用了端口,可以通过 kill 命令杀死该进程或更改 httpd.conf 文件配置,将 httpd 服务的端口改为其他空闲端口,重新启动。 2. 配置文件错误 httpd 服务的配置文件通常是 /etc/httpd/conf/httpd.conf,如果其中存在语法错误、权限问题或者其它配置错误,可能会导致 httpd 服务启动出错。可以通过将 httpd.conf 文件备份后删掉,重新执行 yum install httpd 命令安装 httpd 服务,然后手动修改 httpd.conf 文件,逐个检查每个配置项是否正确,确认无误后重启 httpd 服务。 3. SELinux 问题 SELinux 是 CentOS 7中提供的一种安全模块,它可以对系统文件和应用程序进行安全管控。如果 SELinux 配置不正确,可能会阻止 httpd 服务正常启动。可以通过修改 /etc/selinux/config 文件中 SELINUX=disabled 来暂时关闭 SELinux,然后重新启动 httpd 服务;或者一个更优的方式是,根据日志确定问题原因,使用命令 semanage 或者 setsebool 等工具将相关目录或者配置加入到 SELinux 许可列表中,重新启动 httpd 服务,以恢复服务正常工作。 4. 防火墙问题 如果你的 CentOs 7 服务器启用了防火墙,有可能会导致 httpd 服务启动失败。可以通过检查防火墙相关配置来确定问题原因,解决方案是修改防火墙规则,将端口 80 或者 443 等 httpd 服务需要的端口放行,重新启动 httpd 服务。 总之,当遇到 httpd 服务启动失败时,不要慌张,可以先通过日志或者执行命令查看错误信息,找到错误原因,然后根据错误原因一步一步解决问题。在解决问题过程中注意备份原始配置文件,以免造成不必要的损失。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

子林Android

感谢老板,老板大气!

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

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

打赏作者

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

抵扣说明:

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

余额充值