Android 过渡动画实践

Android过渡动画实践

前言

在学习矢量动画的时候,看到篇别人写的《Android过渡动画,发现掘金小秘密》,觉得挺有意思的,算是拿人家内容做了个实践吧,后面又加了点在Transition和Visibility的实践,实践的过程中也踩了挺多坑,下面就写篇文章记录下,可能不会详细说明原理,但是内容都在代码里,

布局变换动画

还是先从简单的来吧,前面Animation和Animator里面讲到过LayoutAnimation和LayoutTransition,这里再来看一下吧,应该也算是中过渡动画。

LayoutAnimation

LayoutAnimation的使用我这就不重新写了,就是View动画,贴一下之前文章该章节的链接,也可以看下效果:

Android View动画实践 - LayoutAnimation

LayoutTransition

LayoutTransition也是一样的,只不过里面实现变成了属性动画:

Android 属性动画实践 - LayoutTransition

Activity过渡动画

上面两个动画在ViewGroup的子view发生变化的时候,会有过渡效果,而我们一般还会在Activity的过渡需要用到动画,下面讲讲。

overridePendingTransition

使用Activity的过渡动画,最简单的就是使用overridePendingTransition了,不过overridePendingTransition在Android14被废弃,推荐使用overrideActivityTransition,需要改compileSdkVersion,这里就不提了。

overridePendingTransition在前面Animation的文章中也有写到,这里贴一下:

Android View动画实践 - overridePendingTransition

ActivityOptions过渡动画

overridePendingTransition也是在Android5以前使用的方法了,在Android5后面,Google提供了新的Activity过渡动画,下面来看下。

因为过渡动画有几种,我这加了个单选框来选择过渡动画类型,先看下布局文件:

xml 复制代码
<TextView
    android:text="使用SceneTransitionAnimation (Android5+)"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    tools:ignore="HardcodedText"
    />

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center_vertical"
    >

    <Button
        android:id="@+id/sceneTransition"
        android:text="点击跳转"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        tools:ignore="HardcodedText"
        />

    <RadioGroup
        android:id="@+id/sceneType"
        android:orientation="horizontal"
        android:gravity="center_vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <RadioButton
            android:id="@+id/explode"
            android:text="Explode"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:ignore="HardcodedText"
            />

        <RadioButton
            android:id="@+id/slide"
            android:checked="true"
            android:text="Slide"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:ignore="HardcodedText"
            />

        <RadioButton
            android:id="@+id/fade"
            android:text="Fade"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:ignore="HardcodedText"
            />

    </RadioGroup>

</LinearLayout>

布局里面就是一个点击跳转的按钮,和三个选择过渡动画类型的单选框,代码也比较简单:

java 复制代码
// Activity过渡动画(Android5之后使用)
binding.sceneTransition.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        val intent = Intent(requireContext(), SceneTransitionActivity::class.java)

        // 传入过渡动画类型
        intent.putExtra("type", when(binding.sceneType.checkedRadioButtonId) {
            R.id.explode -> "explode"
            R.id.slide -> "slide"
            R.id.fade -> "fade"
            else -> "slide"
        })

        // Create an ActivityOptions to transition between Activities using cross-Activity
        // scene animations.
        val optionsCompat =
            ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity())

        startActivity(intent, optionsCompat.toBundle())
    }else {
        showToast("当前系统版本不支持: ${Build.VERSION.SDK_INT}")
    }
}

要实现Activity的过渡效果,需要使用makeSceneTransitionAnimation生成一个optionsCompat,并转成bundle给另一个activity传过去。这里除了makeSceneTransitionAnimation,其他和我们没什么关系。

所以更重要的应该是在另一个activity里面,下面看一下SceneTransitionActivity的写法:

java 复制代码
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // AppCompatActivity中window会被style影响,需要在style中设置下面两个属性
        // 启用窗口过渡属性;
        // window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
    
        // 设置是否需动画覆盖,转场动画中动画之间会有覆盖的情况
        // 可以设置false来让动画有序的进入退出
        // window.allowEnterTransitionOverlap = false
    
        // 根据传入信息配置过渡动画
        when(intent.getStringExtra("type")) {
            "explode" -> Explode()
            "slide" -> Slide()
            "fade" -> Fade()
            else -> CustomVisibility()
        }.apply {
        duration = 500
    
        // 排除状态栏和导航栏(这两个还真有用!!!navigationBarBackground效果不明显)
        // 关于排除的id,可以自定义一个Visibility,在onAppear方法中用Log查看
        excludeTarget(android.R.id.statusBarBackground, true)
        excludeTarget(android.R.id.navigationBarBackground, true)
        // 排查标题栏
        excludeTarget(R.id.action_bar_container, true)
        }.also { transition ->
            // 退出当前界面的过渡动画
            window.exitTransition = transition
            // 进入当前界面的过渡动画
            window.enterTransition = transition
            // 重新进入界面的过渡动画
            window.reenterTransition = transition
        }
    }
    //设置活动布局
    setContentView(mContextView)
}

这里就踩坑了,网上的文章基本都是说window的requestFeature要在setContentView前调用,我在这前面调用了还是会报错,因为AppCompatActivity会设置style,设置style的时候会调用setContentView,这样window的属性就不让修改了,正确的操作是继承原有的主题,再修改window的参数:

manifest里面

xml 复制代码
<activity android:name=".tech.scene.SceneTransitionActivity" android:theme="@style/CustomActivityTheme"/>

res/values/themes/themes.xml

xml 复制代码
<style name="CustomActivityTheme" parent="Theme.Fundark">
    <!-- 启用 FEATURE_CONTENT_TRANSITIONS 特性 -->
    <item name="android:windowContentTransitions">true</item>
    <item name="android:windowAllowEnterTransitionOverlap">true</item>
</style>

也就是把代码中动态设置的属性,放到style文件去。另外在activity返回的时候也要注意下,带动画返回:

java 复制代码
binding.button.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // 带动画返回
        finishAfterTransition()
    }else {
        finish()
    }
}

说了这么多,还是看下效果吧:

共享过渡动画

在看了上面三种自带的过渡效果,还有一种让我觉得很炫酷的过渡效果,这里必须得提一下,那就是共享过渡动画。

只不过不知道怎么说,它到底是Activity过渡动画,还是布局过渡动画,感觉像一个View从起始位置移动到目标位置,问GPT却说是TransitionManager控制的在两个页面变换的同一个View,原理有时间再深究,这里看下使用吧:

我这就用了一个TextView来验证,设置了TextView的background:

xml 复制代码
<TextView
    android:id="@+id/sceneTransitionAnimation"
    android:text="click"
    android:background="@drawable/ic_launcher"
    android:layout_width="wrap_content"
    android:layout_height="80dp"
    android:layout_margin="5dp"
    android:clickable="true"
    android:focusable="true"
    tools:ignore="HardcodedText"
    />

在启动activity的时候要给makeSceneTransitionAnimation传入要过渡的元素,以及另一个界面接收的标识符:

java 复制代码
// 共享元素过渡动画
binding.sceneTransitionAnimation.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        val intent = Intent(requireContext(), AnimatorActivity::class.java)

        // iv是当前点击的图片  share字符串是第二个activity布局中设置的**transitionName**属性
        val optionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(
            requireActivity(),
            binding.sceneTransitionAnimation,
            "sceneTransition")

        val bundle = optionsCompat.toBundle()
        // 多个过渡元素情况,记得import androidx.core.util.Pair
//                val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
//                    requireActivity(),
//                    Pair(binding.sceneTransitionAnimation, "sceneTransition")
//                ).toBundle()

        startActivity(intent, bundle)
    }else {
        showToast("当前系统版本不支持: ${Build.VERSION.SDK_INT}")
    }
}

下面是另一个界面的布局,注意里面的transitionName(这里要求v21):

xml 复制代码
<TextView
    android:id="@+id/sceneTransitionAnimation"
    android:transitionName="sceneTransition"
    android:text="SceneTransitionAnimation(click)"
    android:background="@drawable/ic_launcher"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    android:layout_margin="5dp"
    android:clickable="true"
    android:focusable="true"
    tools:ignore="HardcodedText"
    />

共享过渡也支持多个元素的过渡,可以看下上面代码注释的地方,AnimatorActivity返回的时候和上面类似,要注意使用finishAfterTransition返回,下面看下效果:

Transition过渡动画

实际上一节后面两个Activity过渡动画,都是通过TransitionManager实现的,前后两个activity是一个Scene,TransitionManager通过Transition将两者之间的过渡整合起来。

下面我们先从简单的出发,慢慢去体验下这个Transition过渡动画。

Layout变化动画

上面两个不同的activity是一种场景,两个不同的布局(include引入)也能是不同的Scene,听起来很懵,下面看代码:

xml 复制代码
<FrameLayout
    android:id="@+id/contentPanel"
    android:layout_weight="1"
    android:layout_width="wrap_content"
    android:layout_height="0dp">

    <include
        android:id="@+id/includeLayout"
        layout="@layout/layout_scene_first"/>

</FrameLayout>

上面是layout里面的代码,还有两个不同的布局,用于include:

res/layout/layout_scene_first.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/gray">

    <TextView
        android:id="@+id/sceneFirst"
        android:text="布局变化前"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_margin="5dp"
        android:gravity="center_vertical"
        tools:ignore="HardcodedText"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

res/layout/layout_scene_second.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:background="@color/gray"
    >

    <TextView
        android:id="@+id/sceneSecond"
        android:text="布局变化后"
        android:background="@drawable/ic_launcher"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="5dp"
        tools:ignore="HardcodedText"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

两个布局区别不大,就文字和宽度不一样,不过还是很容易肉眼区分的,切换两个layout的代码和触发过渡动画的代码也比较简单:

java 复制代码
// 布局变化动画
val firstScene = Scene.getSceneForLayout(binding.contentPanel,
    R.layout.layout_scene_first, requireContext())

val secondScene = Scene.getSceneForLayout(binding.contentPanel,
    R.layout.layout_scene_second, requireContext())

// ViewBinding无法拿到第二个layout中的id
// binding.includeLayout.sceneFirst.setOnClickListener {
var isFirst = true
binding.contentPanel.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        TransitionManager.go(if(isFirst) secondScene else firstScene, Slide(Gravity.START))
        isFirst = !isFirst
    }
}

定义两个Scene,然后通过TransitionManager的go方法执行就行了,比较坑的就是在viewBindding里面拿不到布局-_-||。

我这加了个Slide的过渡效果,这个上面有用到过,实际它就是一个Transition,这里先看下效果,Transition后面我们会自定义它,那时候就能理解了:

Layout变化动画(子view属性)

上面我们是通过两个include的布局创建两个Scene来实现过渡效果的,实际上我们更多需要的是view属性变换后的过渡动画,听起来不就是属性动画么,可是还是有区别的,属性动画是我们自己去操作view,而这个过渡动画是我们修改view属性后,两个状态之间的过渡。

简单理解就是,系统会创建两个Scene,分别对应属性变换前和属性变换后,中间根据Transition的设定进行变化,产生过渡效果,说这么多,还是实操好理解:

xml 复制代码
<LinearLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/sceneTransitionSystem"
        android:text="click"
        android:background="@drawable/ic_launcher"
        android:layout_width="wrap_content"
        android:layout_height="80dp"
        android:layout_margin="5dp"
        android:clickable="true"
        android:focusable="true"
        tools:ignore="HardcodedText"
    />
    
</LinearLayout>

上面就是一个TextView,放在一个id名为container的LinearLayout里面,下面是操作它变化的代码:

java 复制代码
// 布局变化过渡动画(子view属性)
var isFirst2 = true
binding.sceneTransitionSystem.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // 延迟启动root的变化
        TransitionManager.beginDelayedTransition(binding.root, AutoTransition())
        // 修改root内子View,设置变化
        binding.sceneTransitionSystem.layoutParams.apply {
            width = if (isFirst2) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
        }.also {
            binding.sceneTransitionSystem.layoutParams = it
        }
        isFirst2 = !isFirst2
    }
}

代码比较简单,通过TransitionManager的beginDelayedTransition方法,会对要产生变化的View进行监听,随后我们改变它的子view属性,就能触发Transition了。

这里的beginDelayedTransition方法可以不传第二个Transition参数,默认是AutoTransition,还是挺炫酷的:

过渡动画: TransitionSet

如果不看Transition相关的内容,我还不知道Android的res文件夹还能自动生成一个transition目录:

而且里面还支持这么多的标签:

不过这些都是Android已经定义好的transition,按需求使用就可以了,下面用前面的Layout变化动画来实践下:

java 复制代码
// 过渡动画: TransitionSet
// 继续用上面的布局变化动画,从XML中读取transition
val firstScene2 = Scene.getSceneForLayout(binding.contentPanelSet,
    R.layout.layout_scene_first, requireContext())
val secondScene2 = Scene.getSceneForLayout(binding.contentPanelSet,
    R.layout.layout_scene_second, requireContext())

val transitionSet =
    TransitionInflater.from(requireContext()).inflateTransition(R.transition.transition_set)
// 使用代码创建
//        TransitionSet().apply {
//            // 为目标视图滑动添加动画效果
//            addTransition(changeScroll())
//            // 为目标视图布局边界的变化添加动画效果
//            addTransition(ChangeBounds())
//            // 为目标视图裁剪边界的变化添加动画效果
//            addTransition(changeClipBounds())
//            // 为目标视图缩放和旋转方面的变化添加动画效果
//            addTransition(changeTransform())
//            // 为目标图片尺寸和缩放方面的变化添加动画效果
//            addTransition(ChangeImageTransform())
//
//            // 继承Visibility类,
//            addTransition(Slide())
//            addTransition(Explode())
//            addTransition(Fade(Fade.MODE_IN))
//        }

var isFirst3 = true
binding.contentPanelSet.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        TransitionManager.go(if(isFirst3) secondScene2 else firstScene2, transitionSet)
        isFirst3 = !isFirst3
    }
}

代码和布局都没什么好解释的了,和上面一样,重点就是通过TransitionInflater创建了一个transitionSet:

res/transition/transition_set.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:transitionOrdering="together">

    <changeScroll />

    <changeImageTransform />

    <changeBounds />

    <fade />

</transitionSet>

我们加了几种效果,一些常用的系统transition解释,可以看下我在用代码创建TransitionSet的注释:

java 复制代码
// 使用代码创建
TransitionSet().apply {
    // 为目标视图滑动添加动画效果
    addTransition(changeScroll())
    // 为目标视图布局边界的变化添加动画效果
    addTransition(ChangeBounds())
    // 为目标视图裁剪边界的变化添加动画效果
    addTransition(changeClipBounds())
    // 为目标视图缩放和旋转方面的变化添加动画效果
    addTransition(changeTransform())
    // 为目标图片尺寸和缩放方面的变化添加动画效果
    addTransition(ChangeImageTransform())

    // 继承Visibility类,
    addTransition(Slide())
    addTransition(Explode())
    addTransition(Fade(Fade.MODE_IN))
}

实际都是transition,注重它的效果就行吧,深入源码我觉得看有没有多余时间吧,下面看下效果:

自定义Transition

看了上面那么多Transition,不动手自己练几个,都不好意思说我这篇文章是实践的,下面就是手撕环节了。

java 复制代码
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.transition.Transition
import android.transition.TransitionValues
import android.view.ViewGroup

class CustomTransition : Transition() {
    private val rightValue = "rightValue"

    override fun captureStartValues(transitionValues: TransitionValues) {
        // 获得view,可以拿到起始属性值,保存到transitionValues.values去
        val view = transitionValues.view
        transitionValues.values[rightValue] = view.right
    }

    override fun captureEndValues(transitionValues: TransitionValues) {
        // 和上面一样,保持结束值
        val view = transitionValues.view
        transitionValues.values[rightValue] = view.right
    }

    override fun createAnimator(
        sceneRoot: ViewGroup,
        startValues: TransitionValues,
        endValues: TransitionValues
    ): Animator {

        val view = endValues.view
        val start = startValues.values[rightValue] as Int
        val end = endValues.values[rightValue] as Int

        // 将自定义动画应用于目标视图
        val animatorSet = AnimatorSet()
        val animatorSetOther = AnimatorSet()
        animatorSetOther.playSequentially(
            ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.5f),
            ObjectAnimator.ofFloat(view, "scaleY", 1.5f, 1f)
        )
        animatorSet.playTogether(
            ObjectAnimator.ofInt(view, "right", start, end),
            animatorSetOther
        )
        return animatorSet
    }
}

自定义一个Transition需要实现三个方法,分别是captureStartValues和captureEndValues方法用于捕获初始及结束的变化值,在createAnimator方法里面实现变化效果。

看到createAnimator方法,以及它要求的返回值Animator,它的神秘面纱就被扯下来了,就是这么简单。。。

我们自定义的这个CustomTransition效果解释下,就是在view的right属性变化后,给它平滑的扩张过去,并且加了个Y轴上的先放大后缩小效果,我觉得还挺nice的,下面看下效果图:

自定义Visibility

虽然我们自定义了Transition,但是我们在Activity过渡中用到的Slide、Explode、Fade却是继承的Visibility,Visibility继承了Transition,会在View修改Visibility的时候触发。

自定义Visibility比自定义Transition复杂一些,下面看下代码:

java 复制代码
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.transition.TransitionValues
import android.transition.Visibility
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.core.animation.addListener

class CustomVisibility: Visibility() {

    // 用于控制view出现时的外观变化,类似transition功能
    override fun onAppear(
        sceneRoot: ViewGroup,
        view: View,
        startValues: TransitionValues,
        endValues: TransitionValues
    ): Animator {
        return super.onAppear(sceneRoot, view, startValues, endValues)
    }

    // 提供了更直接的方式来访问视图的可见性状态,未实现的话,会继续调用上面onAppear方法
    override fun onAppear(
        sceneRoot: ViewGroup,
        startValues: TransitionValues?,
        startVisibility: Int,
        endValues: TransitionValues,
        endVisibility: Int
    ): Animator {
        // 获取要操作的View,注意startValues可能为null
        val view = endValues.view

        // 加日志可以看到受影响的view,可根据ID排除
         Log.d("TAG", "onAppear: view = $view")
        // view = android.view.View{... #1020030 android:id/navigationBarBackground}
        // 比如: excludeTarget(android.R.id.navigationBarBackground, true)

        // 将自定义动画应用于目标视图
        return AnimatorSet().apply {
            duration = 2000
            when(startVisibility) {
                // 从GONE变为显示
                View.GONE -> playTogether(
                    ObjectAnimator.ofFloat(view, "alpha", 0f, 1f),
                    ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f),
                    ObjectAnimator.ofFloat(view, "scaleY", 0f, 1f)
                )
                // 从INVISIBLE变为显示
                View.INVISIBLE -> playTogether(
                    ObjectAnimator.ofFloat(view, "alpha", 0f, 1f)
                )
                // 当view被添时,startVisibility=-1,不加旋转
                else -> {
                    playTogether(
                        ObjectAnimator.ofFloat(view, "alpha", 0f, 1f),
                        ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f),
                        ObjectAnimator.ofFloat(view, "scaleY", 0f, 1f),
                    )
                }
            }
        }
    }

    override fun onDisappear(
        sceneRoot: ViewGroup,
        startValues: TransitionValues,
        startVisibility: Int,
        endValues: TransitionValues?,
        endVisibility: Int
    ): Animator {
        // 获取要操作的View,注意endValues可能为null
        val view = endValues?.view ?: startValues.view

        // 这里要阻止View直接变成endVisibility,在动画结束后再设置
        // 通过日志可以看出这里设置visibility不会再触发onAppear、onDisappear
        Log.d("TAG", "onDisappear: view = $view")
        view.visibility = startVisibility

        return AnimatorSet().apply {
            duration = 2000
            addListener(onEnd = {
                view.visibility = endVisibility
            })
            when(endVisibility) {
                // 最终变为GONE
                View.GONE -> playTogether(
                    ObjectAnimator.ofFloat(view, "alpha", 1f, 0f),
                    ObjectAnimator.ofFloat(view, "scaleX", 1f, 0f),
                    ObjectAnimator.ofFloat(view, "scaleY", 1f, 0f),
                )
                // 最终变为INVISIBLE
                View.INVISIBLE -> playTogether(
                    ObjectAnimator.ofFloat(view, "alpha", 1f, 0f),
                )
                // 当view被remove的时候,endVisibility=-1,不加旋转
                else -> {
                    playTogether(
                        ObjectAnimator.ofFloat(view, "alpha", 1f, 0f),
                        ObjectAnimator.ofFloat(view, "scaleX", 1f, 0f),
                        ObjectAnimator.ofFloat(view, "scaleY", 1f, 0f)
                    )
                }
            }
        }
    }
}

实际我们只要实现其中的onAppear和onDisappear方法即可,参数和Transition类似,只不过多了起始的可见性startVisibility和endVisibility,我们可以根据变化前后的状态做一些操作。

我们在前面的ActivityOptions过渡动画那看下效果,不传值的话用我们自定义的过渡动画:

java 复制代码
// 根据传入信息配置过渡动画
when(intent.getStringExtra("type")) {
    "explode" -> Explode()
    "slide" -> Slide()
    "fade" -> Fade()
    else -> CustomVisibility()
}

在代码里点击过去,没什么区别:

java 复制代码
// 过渡动画Activity: CustomVisibility
binding.customVisibility.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        val intent = Intent(requireContext(), SceneTransitionActivity::class.java)

        // 传入过渡动画类型
        intent.putExtra("type", "CustomVisibility")
        val optionsCompat =
            ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity())

        startActivity(intent, optionsCompat.toBundle())
    }else {
        showToast("当前系统版本不支持: ${Build.VERSION.SDK_INT}")
    }
}

下面是效果图:

有个神奇的地方就是标题栏、状态栏、导航栏、布局内容都会调用onAppear方法,所以这里onAppear会调用多次,可以通过excludeTarget排除。

另外一个有意思的是Visibility的注释里面提到了:

Visibility is determined not just by the View.setVisibility(int) state of views, but also whether views exist in the current view hierarchy.

也就是说一个view被添加到布局和从布局移除的时候也会触发,这里没体现,而且在Activity过渡动画中,Visibility的变化也没体现,我觉得还是得再搞个例子看看。

自定义Visibility(Layout)

前面说到了在Activity的过渡动画中,Visibility的一些性质并没有被体现出来,下面我们就用上面"Layout变化动画(子view属性)"的例子改一改,来体现这个效果。

xml 复制代码
<RadioGroup
    android:id="@+id/customVisibilityType"
    android:orientation="horizontal"
    android:gravity="center_vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <RadioButton
        android:id="@+id/visiable"
        android:checked="true"
        android:text="Visiable"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="HardcodedText"
        />

    <RadioButton
        android:id="@+id/invisible"
        android:text="Invisible"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="HardcodedText"
        />

    <RadioButton
        android:id="@+id/gone"
        android:text="Gone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="HardcodedText"
        />

</RadioGroup>

<RadioGroup
    android:id="@+id/customVisibilityAdd"
    android:orientation="horizontal"
    android:gravity="center_vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <RadioButton
        android:id="@+id/add"
        android:text="Add"
        android:checked="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="HardcodedText"
        />

    <RadioButton
        android:id="@+id/remove"
        android:text="Remove"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="HardcodedText"
        />

</RadioGroup>

<LinearLayout
    android:id="@+id/visibilityContainer"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/customVisibilityText"
        android:text="click"
        android:background="@drawable/ic_launcher"
        android:layout_width="wrap_content"
        android:layout_height="80dp"
        android:layout_margin="5dp"
        android:clickable="true"
        android:focusable="true"
        tools:ignore="HardcodedText"
        />

</LinearLayout>

布局里添加了五个单选按钮(换了一行,不然太挤了)来操控下面TextView的状态,因为View.GONE会让TextView消失,页面会变化,这里加了个LinearLayout给它固定下位置。

代码里面根据radioGroup的变化操作textView的属性就行,记得beginDelayedTransition传入自定义的CustomVisibility就ok:

java 复制代码
// 过渡动画Layout: CustomVisibility
val textView = binding.customVisibilityText
binding.customVisibilityType.setOnCheckedChangeListener { _, checkedId ->
    // 设置过渡动画
    TransitionManager.beginDelayedTransition(binding.root, CustomVisibility())
    // 触发
    textView.visibility = when(checkedId) {
        R.id.visiable -> View.VISIBLE
        R.id.invisible -> View.INVISIBLE
        R.id.gone -> View.GONE
        else -> View.VISIBLE
    }
}
binding.customVisibilityAdd.setOnCheckedChangeListener { _, checkedId ->
    // 设置过渡动画
    TransitionManager.beginDelayedTransition(binding.root, CustomVisibility())
    // 触发
    when(checkedId) {
        R.id.add -> {
            if (!binding.visibilityContainer.contains(textView)) {
                // 添加并不会触发Visibility的onAppear
                textView.visibility = View.VISIBLE
                binding.visibilityContainer.addView(textView)
            }
        }
        R.id.remove -> {
            if (binding.visibilityContainer.contains(textView)) {
                // 可以触发onDisappear,但是无法出现过渡效果
                binding.visibilityContainer.removeView(textView)
            }
        }
        else -> {}
    }
}

下面是实际效果:

有点被Visibility的注释唬住了,view的add和remove只有remove会触发onDisappear,而add方法不会触发onAppear,但是这两个方法的visibility会从-1变化,不是visiable、invisible、gone中的任何一种,有点坑。

而且啊,这里动画能太复杂,我一开始加了些旋转动画,结果有一定几率没反应,还有,这里的动画不要点太快,点快了也有一定几率没反应,特别是onAppear方法,好鸡肋啊。。。

Demo

源码在我用来练手的参考里,可以参考下:

SceneTransitionTestDemo.kt

参考文章

Android过渡动画,发现掘金小秘密

Android技术分享| Activity 过渡动画 --- 让切换更加炫酷

android中的Transition动画的使用

小结

这篇文章主要对Android里面过渡动画做了些实践,给出Demo和Gif效果图,包含了Activity过渡效果和布局的过渡效果,还自定义了下Transition和Visibility,又是知识满满啊!

相关推荐
Estar.Lee3 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh3 小时前
uiautomator案例
android
工业甲酰苯胺4 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3434 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee6 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯6 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey8 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!9 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟10 小时前
Android音频采集
android·音视频
小白也想学C11 小时前
Android 功耗分析(底层篇)
android·功耗