Cursor blinking

Android Transition 动画

Android 基础|Android动画|字数 1,683|阅读时长≈ 5 分钟

简介

在 Android 中,Transition 是用于管理场景切换过渡动画的类。它可以帮助您在界面元素发生改变时,实现平滑的动画效果,包括视图的添加、移除、显示、隐藏等操作。Transition 框架提供了丰富的动画效果和配置选项,使得开发者可以轻松地创建各种各样的过渡动画效果。

以下是一些常用的 Transition 动画类和概念:

  • Transition:Transition 类是所有过渡动画效果的基类,它有许多子类用于实现不同的过渡效果。常见的子类包括 Fade(淡入淡出效果)、Slide(滑动效果)、Explode(爆炸效果)等。
  • TransitionManager:TransitionManager 是一个用于管理场景切换时应用那些过度动画的类。
  • Scene:场景是一组视图的集合,代表了一个特定的界面状态(View 层次结构中各个 view 属性值的集合)。在切换场景时,可以定义不同的场景,并通过 TransitionManager 在不同场景之间进行过渡动画。
  • Shared Element Transition:共享元素过渡是一种特殊的过渡动画,用于在两个界面之间共享视图元素的过渡效果。通常与 TransitionManager 结合使用。

使用 Transition 动画的一般步骤如下:

  1. 创建开始和结束场景,定义视图元素的状态。
  1. 创建相应的 Transition 对象,设置过渡动画效果。
  1. 使用 TransitionManager.go(Scene scene, Transition transition) 类指定下个场景和过度动画。
  1. 在场景切换时,更新视图元素的状态,然后将过渡动画应用到视图上。

通过使用 Transition 动画,您可以为应用程序的界面切换添加更加生动和流畅的动画效果,从而提升用户体验。

过渡动画示例.gif
过渡动画示例.gif

Transition

定义切换的起始场景和结束场景后,要创建一个定义动画的 Transition 对象。借助该框架,您可以在资源文件中指定内置过渡并在代码中对其进行膨胀,也可以直接在代码中创建内置过渡的实例。

标记影响
AutoTransition默认过渡。按照该顺序淡出、移动和调整大小,以及淡入视图。
ChangeBounds移动视图并调整其大小。
ChangeClipBounds捕获场景变化前后的 View.getClipBounds(),并在过渡期间为这些变化添加动画效果。
ChangeImageTransform捕获场景变化前后的 ImageView 矩阵,并在过渡期间为其添加动画效果。
ChangeScroll捕获场景变化前后的目标的滚动属性,并以动画方式呈现所有变化。
ChangeTransform捕获场景切换前后的视图缩放和旋转,并在过渡期间为这些更改添加动画效果。
Explode跟踪起始场景和结束场景中目标视图可见性的变化,并将视图移入或移出场景边缘。
Fadefade_in 淡入视图。fade_out 淡出视图。fade_in_out(默认)执行 fade_out,后跟 fade_in。
Slide跟踪起始场景和结束场景中目标视图可见性的变化,并将视图从场景的其中一个边缘移入或移出。

ChangeBoundsChangeClipBoundsChangeImageTransformChangeScrollChangeTransform 是Transition 的直接子类,FadeExplodeSlide 是 Transition 的间接子类,除此之外,PathMotion、PatternPathMotion、ArcMotion 为 Transition 提供路径支持。

Shared Element Transition

共享元素过渡(Shared Element Transition)是一种特殊的过渡动画,用于在两个 Activity 或 Fragment 之间共享视图元素的平滑过渡效果。这种动画效果可以使用户在界面切换时,看到共享的视图元素从一个界面过渡到另一个界面,增强用户体验并提升界面之间的连贯性。

实现共享元素过渡的一般步骤如下:

  1. 标识共享元素:在要共享的视图元素中设置一个独一无二的标识,通常通过在 XML 布局文件中使用 android:transitionName 属性来实现。这个标识在两个界面中应该是相同的。
  1. 启动共享元素过渡:在启动新的 Activity 或 Fragment 时,通过设置共享元素过渡的动画效果,告诉系统哪些视图元素应该进行共享元素过渡。这通常在 ActivityOptionsCompat 或 FragmentTransaction 中进行设置。
  1. 建立过渡动画:在 onCreate() 方法中调用 postponeEnterTransition() 方法来延迟共享元素过渡的执行。然后在视图准备好后调用 startPostponedEnterTransition() 来开始过渡动画。
  1. 设置共享元素回调:在新的 Activity 或 Fragment 中,通过设置共享元素回调 setEnterSharedElementCallback() 来处理共享元素的过渡。
  1. 结束共享元素过渡:在新的 Activity 或 Fragment 中,在共享元素过渡完成后,通过调用 setSharedElementReturnTransition() 和 setSharedElementEnterTransition() 来设置共享元素的返回和进入动画。

共享元素过渡提供了一种优雅的方式来实现界面元素之间的过渡效果,特别适用于展示列表中的详细信息或者图片等情景。通过使用共享元素过渡,可以让用户感受到界面间的自然连接,提升应用的用户体验。

下面示例演示了使用共享元素过渡:

screen-20240327-092354.gif
screen-20240327-092354.gif
Code
// 在 Activity A 中设置要共享的视图元素private fun toActivityB() {        val intent = Intent(this@ShareElement0AActivity, ShareElement0BActivity::class.java)        var pair0 = Pair<View, String>(iv_image, "iv_image")        var pair1 = Pair<View, String>(tv_text, "tv_text")        val activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(this, pair0, pair1)        startActivity(intent, activityOptions.toBundle())    }    // Activity B    class ShareElement0BActivity : BaseActivity() {       override fun onCreate() {        setUpTransition()    }        private fun setUpTransition() {		    // 设置那些视图元素应该进行共享元素过渡        ViewCompat.setTransitionName(iv_image, "iv_image")        ViewCompat.setTransitionName(tv_text, "tv_text")        // 设置过渡动画        val changeBounds = ChangeBounds()        val changeImageTransform = ChangeImageTransform()        val textResize = TextResize()        textResize.addTarget(tv_text)        val transitionSet = TransitionSet()        transitionSet.addTransition(changeBounds)        transitionSet.addTransition(changeImageTransform)        transitionSet.addTransition(textResize)        transitionSet.duration = 1000        window.sharedElementEnterTransition = transitionSet				 // 设置共享元素回调        this.setEnterSharedElementCallback(object : SharedElementCallback() {             /**             * 最先调用,用于动画开始前替换 ShareElements ,比如在Activity B 翻过若干页大图之后,返回 Activity A             * 的时候需要缩小回到对应的位置图片,就需要在这里进行替换             */            override fun onMapSharedElements(                names: List<String>,                sharedElements: Map<String, View>            ) {                for (name in names) {                    LogUtils.w("------B onMapSharedElements name=$name")                }            }             /**             * 表示ShareElement已经全部就位,可以开始动画了             */            override fun onSharedElementsArrived(                sharedElementNames: MutableList<String>?,                sharedElements: MutableList<View>?,                listener: OnSharedElementsReadyListener?            ) {                super.onSharedElementsArrived(sharedElementNames, sharedElements, listener)                LogUtils.d("------B onSharedElementsArrived")            }             /**             * 在之前的步骤里 (onMapSharedElements) 被从 ShareElements 列表里除掉的 View 会在此回调,             * 不处理的话默认进行 alpha 动画消失             */            override fun onRejectSharedElements(rejectedSharedElements: MutableList<View>?) {                super.onRejectSharedElements(rejectedSharedElements)                LogUtils.d("------B onRejectSharedElements")            }             /**             *在这里会把 ShareElement 里值得记录的信息存到为 Parcelable 格式,以发送到Activity B             *默认处理规则是 ImageView 会特殊记录 Bitmap、ScaleType、Matrix,其它View只记录大小和位置             */            override fun onCaptureSharedElementSnapshot(                sharedElement: View?,                viewToGlobalMatrix: Matrix?,                screenBounds: RectF?            ): Parcelable {                return super.onCaptureSharedElementSnapshot(                    sharedElement,                    viewToGlobalMatrix,                    screenBounds                )            }             /**             *在这里会把 Activity A 传过来的 Parcelable 数据,重新生成一个View,这个 View 的大小和位置会与 Activity A 里的             *ShareElement 一致,             */            override fun onCreateSnapshotView(context: Context?, snapshot: Parcelable?): View {                LogUtils.d("------B onCreateSnapshotView")                return super.onCreateSnapshotView(context, snapshot)            }             /**             * 共享元素过渡开始时被调用。您可以在这个方法中执行一些准备工作,比如隐藏或调整视图元素的属性,以便在过渡期间产生更好的动画效果。             * 这个方法通常用于在过渡开始时执行一些动画或视图操作。             * @param sharedElementNames             * @param sharedElements             * @param sharedElementSnapshots             */            override fun onSharedElementStart(                sharedElementNames: MutableList<String>?,                sharedElements: MutableList<View>?,                sharedElementSnapshots: MutableList<View>?            ) {                LogUtils.d("------B onSharedElementStart")                 targetTextSize = tv_text.textSize                targetTextColors = tv_text.textColors                targetTextPadding = Rect(                    tv_text.getPaddingLeft(),                    tv_text.getPaddingTop(),                    tv_text.getPaddingRight(),                    tv_text.getPaddingBottom()                )                tv_text.setTextColor(intent.getIntExtra(EXTRA_TEXTCOLOR, Color.BLACK))                var textSize = intent.getFloatExtra(EXTRA_TEXTSIZE, targetTextSize)                tv_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)                val padding = intent.getParcelableExtra<Rect>(EXTRA_TEXT_PADDING)                tv_text.setPadding(padding.left, padding.top, padding.right, padding.bottom)            }            /**             * 方法在共享元素过渡结束时被调用。您可以在这个方法中执行一些清理工作,比如恢复视图元素的原始属性或状态。             * 这个方法通常用于在过渡结束时执行一些清理操作。             * @param sharedElementNames             * @param sharedElements             * @param sharedElementSnapshots             */            override fun onSharedElementEnd(                sharedElementNames: MutableList<String>?,                sharedElements: MutableList<View>?,                sharedElementSnapshots: MutableList<View>?            ) {                LogUtils.d("------B onSharedElementEnd")                tv_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize)                tv_text.setTextColor(targetTextColors)                tv_text.setPadding(                    targetTextPadding?.left!!, targetTextPadding?.top!!,                    targetTextPadding?.right!!, targetTextPadding?.bottom!!                )                tv_text.requestLayout()                forceSharedElementLayout(rl_container)            }        })    }}    

具体示例代码请查看示例代码,示例代码包含共享动画怎么处理状态栏、RecyclerView 到 Viewpager 的过渡等

!icon 打开一个新的 Activity, 可分别为 window 入场(Activity 切换动画)、View 入场设置动画(Activity::onResume 之后基于布局执行的动画),典型的如底部弹出的页面(半透明背景、视图从底部弹出)

当使用 ActivityOptionsCompat.makeSceneTransitionAnimation() 启动 一个 Activity 时(window 背景被设置为透明,并取消入场动画)直到新页面数据准备好(Activity::onResume 之后能获取到布局信息,可参考 View 绘制流程)才开始过渡动画,这其实是一个拿时间换空间的操作。特别是在大型项目中过渡动画要谨慎使用。

扩展

我在一个闹钟 App 中过渡动画的应用效果

共享动画示例.gif
共享动画示例.gif

参考文档&资源