Material Components—预备役选手Transition

Transition是Android Framework在4.4引入的一个全新的动画框架,可以说是非常古老了,那为什么我现在还要讲Transition呢,其实是想通过Transition来引入Material Design Motion。Transition实际上是MD Motion的基础,同时,也是现代化Android开发动画的基础。

国际惯例,官网镇楼。

https://developer.android.google.cn/training/transitions

基础概念

其实从当时的设计来看,Google在提出Transition框架的时候,就已经准备通过申明式UI的方式来创建动画了,Transition框架的一个核心概念就是Scene,它描述的是一个场景,一个动画的状态值,通常情况下,是动画的起始状态值,有了这样一个起始态,再加上动画的具体类型,就可以完整的描述一个动画的执行过程,所以,在申明式的UI编程中,一切都是以Scene作为基础来进行的。

Transition的本质,实际上就是根据状态差异来生成属性动画,它实际上是对属性动画的抽象和封装。

下面通过一个简单的例子,来演示下如何使用Scene。

创建Scene Layout

首先,创建两个Scene Layout,用于描述动画的两个状态,这里简单的创建两个布局,一个布局在左上角和右下角展示一个ImageView,另一个布局在左下角和右上角展示一个ImageView,代码如下所示。

<?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:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@mipmap/ic_launcher" />

    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@android:mipmap/sym_def_app_icon" />

</androidx.constraintlayout.widget.ConstraintLayout>

另一个布局,只有Item的位置发生的改变,id不变,这里就不贴重复代码了,要记住的是,对于一个元素的动画来说,在不同的Scene中,只要id不变,元素就不变,元素位置、属性的改变,这就是动画效果。

创建Scene Container

一般来说,在一个静态布局下,创建具有多个Scene的布局,会将动静部分分离,将要展示动画的部分,放置在一个Container中,便于管理,在前面创建好Scene Layout后,下面在主界面的xml中,创建它们的Container,代码如下所示。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rootContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <include layout="@layout/base_scene1" />

</FrameLayout>

通过TransitionManager驱动动画

在代码中,通过Scene.getSceneForLayout来创建Scene对象,再通过TransitionManager.go来加载指定的场景,代码如下所示。

class MainActivity : AppCompatActivity() {
    var flag = true

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val scene1 = Scene.getSceneForLayout(rootContainer, R.layout.base_scene1, this)
        val scene2 = Scene.getSceneForLayout(rootContainer, R.layout.base_scene2, this)
        rootContainer.setOnClickListener {
            if (flag) {
                TransitionManager.go(scene2)
            } else {
                TransitionManager.go(scene1)
            }
            flag = !flag
        }
    }
}

当Scene发生改变时,TransitionManager会自动为其生成相应的动画效果。默认情况下,TransitionManager使用AutoTransition,即渐隐渐显合并位移动画,源码如下所示。

image-20201208195756453

所以这里还可以指定动画效果,例如我们只指定位置改变的动画,代码如下所示。

TransitionManager.go(scene2, ChangeBounds())

SDK内置了很多种类的动画效果,如图所示。

截屏2020-12-5.34.07

其中几种比较常用的解析如下。

  • ChangeBounds:检测view的位置边界,创建移动和缩放动画

  • ChangeTransform:检测view的scale和rotation,创建缩放和旋转动画

  • ChangeClipBounds:检测view的剪切区域的位置边界,和ChangeBounds类似,ChangeBounds指定的是剪切区域setClipBound中的rect

  • ChangeImageTransform:检测ImageView的大小、位置以及ScaleType,并创建相应动画

  • ChangeScroll:检测ViewGroup的Scroll,创建Scroll动画

  • Fade、Slide、Explode:检测View的Visibility,创建渐入、滑动、爆炸动画

创建Transition动画的几种方式

不论是transition的哪种使用方式,transition动画都有以下几种创建方式。

通过xml创建Transition动画

在res/transition下创建一个transitionSet的描述文件,代码如下所示。

<?xml version="1.0" encoding="utf-8"?>
<transitionSet>

    <explode />
    <fade />
    
</transitionSet>

在代码中,就可以通过类似LayoutInflater的方式来创建Transition,代码如下所示。

TransitionManager.go(
    scene2,
    TransitionInflater.from(this).inflateTransition(R.transition.transition_from_xml)
)

除了创建transitionSet的复合动画效果,创建单个的transition动画也是一样的,例如下面的代码。

<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:interpolator/bounce"
    android:duration="200"
    android:transitionOrdering="sequential" />

同样可以通过TransitionInflater进行创建。

通过代码创建

对于单个的transition动画,可以通过下面的方式进行创建。

Slide().apply {
    duration = 200
    slideEdge = Gravity.BOTTOM
}

对于复合的transition动画,可以通过下面的方式进行创建。

TransitionSet().apply {
    addTransition(Fade())
    addTransition(Slide())
}

前面提到到AutoTransition,也是继承的TransitionSet实现的复合动画。

不论是怎么使用transition动画,这些创建transition的方式都是可以混用的。

beginDelayedTransition

在前面的讲解中,TransitionManager.go是基于场景Scene切换而产生的动画效果。而Transition框架还提供一种类似自动检测的动画机制,这就是通过beginDelayedTransition来实现的。

下面通过代码来演示下。

rootContainer.setOnClickListener {
    val size = imageView1.width
    TransitionManager.beginDelayedTransition(
        rootContainer, 
        TransitionInflater.from(this).inflateTransition(R.transition.transition_from_xml))
    val layoutParams = imageView1.layoutParams
    if (flag) {
        layoutParams.width = (size / 1.2).toInt()
        layoutParams.height = (size / 1.2).toInt()
        imageView1.layoutParams = layoutParams
        imageView2.visibility = View.VISIBLE
        imageView3.visibility = View.VISIBLE
        imageView4.visibility = View.VISIBLE
    } else {
        layoutParams.width = (size * 1.2).toInt()
        layoutParams.height = (size * 1.2).toInt()
        imageView1.layoutParams = layoutParams
        imageView2.visibility = View.INVISIBLE
        imageView3.visibility = View.INVISIBLE
        imageView4.visibility = View.INVISIBLE
    }
    flag = !flag
}

当我们调用TransitionManager.beginDelayedTransition后,相当于在当前状态下打了个tag,将当前状态下的View属性,创建为初始Scene,在此之后View发生的属性改变,都将被生成新的Scene,从而产生动画效果,这也就是beginDelayedTransition这个API命名的原因。

在上面的代码中,在初始场景下,调用了beginDelayedTransition,创建的动画是changeBounds和explode,在这之后,修改了4个ImageView的属性——尺寸和visibility,并被作用了changeBounds和explode的动画效果,最后效果如下所示。

demo1

类似的,你还可以设置Slide这样的visibility动画效果,实现滑动的切换效果。

动画效果进阶

Slide

和Fade效果类似,它们都是继承自Visibility,它比Fade多了一些属性,除了可以设置属性动画的一些常见属性外,还可以设置Slide方向等属性。

Explode

Explode与Slide十分相似,但是元素将根据Transition Epicenter,辐射状移动,这个Epicenter可以通过setEpicenterCallback来设置。

在Explode中,动画通过TransitionPropagation计算每个动画的开始延迟,例如,默认情况下Explode使用CircularPropagation,动画的延迟取决于元素和Epicenter之间的距离,在代码中,可以通过setPropagation来设置自定义的TransitionPropagation,示例代码如下所示。

// 确定Explode中心点坐标
val viewRect = Rect()
clickedView.getGlobalVisibleRect(viewRect)
// 设置Explode Epicenter
val explode: Transition = Explode().apply {
    epicenterCallback = object : Transition.EpicenterCallback() {
        override fun onGetEpicenter(transition: Transition?): Rect {
            return viewRect
        }
    }
}
explode.duration = 1000
ChangeImageTransform

ChangeImageTransform会对图片进行Matrix变换,主要作用的是ImageView的ScalaType属性,通常情况下,ChangeImageTransform会和ChangeBounds配合使用,示例代码如下所示。

TransitionSet().apply {
    addTransition(ChangeBounds())
    addTransition(ChangeImageTransform())
}
ChangeBounds

ChangeBounds用于改变元素的尺寸和坐标位置,默认情况下,是直线运动的,通过配置Path,可以设置ChangeBounds的曲线运动路径,示例代码如下所示。

TransitionManager.beginDelayedTransition(transitionsContainer,
        ChangeBounds().apply {
            pathMotion = ArcMotion().also { duration = 300 }
        })
val params = button.getLayoutParams() as FrameLayout.LayoutParams
params.gravity = if (isReturnAnimation) Gravity.LEFT or Gravity.TOP else Gravity.BOTTOM or Gravity.RIGHT
button.setLayoutParams(params)

ArcMotion可以设置minimumHorizontalAngle、minimumVerticalAngle、maximumAngle这样的属性来设置路径的具体形态。

当然,你也可以通过patternPathMotion来设置类似SVG的自定义路径。

setTransitionName

在使用beginDelayedTransition执行Transition动画时,可以通过设置transitionName来指定动画场景起始的相同元素,并让这些元素执行transition动画,例如为当前界面中的N个元素setTransitionName,当移除界面上全部元素后,只要setTransitionName的值相同,这些元素依然可以执行动画效果。

Transition界面切换

同样的,官网镇楼。

https://developer.android.google.cn/training/transitions/start-activity

Transition框架的一个重要使用场景,就是Activity和Fragment的切换动画。通常情况下,界面的切换动画分为两种类型——Content Transition和Shared Element Transition。

Content Transition

对于一次切换来说,A -> B,使用Transition的流程如下所示。

  • A.exitTransition Transition框架会先遍历A界面确定要执行动画的view(非共享元素view),执行A.exitTransition()前A界面会获取界面的start scene(view 处于VISIBLE状态),然后将所有的要执行动画的view设置为INVISIBLE,并获取此时的end scene(view 处于INVISIBLE状态).根据transition分析差异的不同创建执行动画。

  • B.enterTransition Transition框架会先遍历B界面,确定要执行动画的view,设置为INVISIBLE。执行B.enterTransition()前获取此时的start scene(view 处于INVISIBLE状态),然后将所有的要执行动画的view设置为VISIBLE,并获取此时的end scene(view 处于VISIBLE状态).根据transition分析差异的不同创建执行动画。

同理,在从B -> A,返回到A时,流程类似,只不过调用的方法不同。

  • A.reenterTransition

  • B.returnTransition

界面切换动画是建立在visibility的改变的基础上的,所以getWindow().setEnterTransition(transition);中的参数一般传的是Fade,Slide,Explode类的实例(因为这三个类是通过分析visibility不同创

简而言之。

A.exitTransition(): 从A->B时,A的退出动画

B.enterTransition(): 从A->B时,B的进场动画

B.returnTransition(): 从B->A时,B的退出动画

A.reenterTransition(): 从B->A时,A的进场动画

一般来说,如果不设置returnTransition和reenterTransition,那么这两个场景的动画,会使用exitTransition和enterTransition的反转动画。

下面就通过一个例子来演示下如何设置界面切换的动画效果。

要注意的是,Transition的实现有两个版本,platform版和AndroidX版,他们的差异在于,AndroidX版的Transition是后续会持续迭代的版本,但是不支持Activity和Window间的动画(至于为什么要这样设计,我在之前的文章中已经解释过了),platform版支持,但是后续不再维护。

首先,在Theme中设置Transition开关,如下所示。

image-20201209200311726

如果你使用的是Material Design Theme,那么这个值默认为true。

在代码中,可以设置如下所示。

window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
设置Transition切换的两种方式

Transition的切换设定,可以在代码或者Theme中进行设置。

在Theme中,可以设置如下。

image-20201210175859139

在代码中,可以如下所示。

window.exitTransition = Explode()
window.reenterTransition = Slide()

一般来说,如果是针对全局的设置,可以放在Theme中,但是在代码中设置,会更加灵活。

动画默认的持续时间,也是可以设置的,代码如下所示。

window.transitionBackgroundFadeDuration = 3000
Transition View & Transition Group

前面在讲解Content Transition的执行过程的时候,提到了在动画开始前,系统会调用ViewGroup.captureTransitioningViews函数,来获取需要进行Transition处理的View,如图所示。

image-20201210191105455

在默认情况下,Transition Group的判断如下所示。

image-20201210191206722

另外,在代码中,还可以通过View.setTransitionGroup(boolean)来主动将一部分View设置为Transition Group,从而在整体上执行动画。

为什么会有这样一个需求呢?其实很明显,Transition会遍历页面中的所有View,包括Toolbar、StatusBar这类的可能通用的组件,那么这个时候,在生成Transition切换动画的时候,就会产生一些不和谐的画面,比如这些通用组件的错位,所以,Transition框架提供了addTarget和excludeTarget方法来指定需要执行Transition切换动画的元素。

在代码中,设置如下。

window.returnTransition = Slide().apply {
    slideEdge = Gravity.BOTTOM
    excludeTarget(android.R.id.statusBarBackground, true)
    excludeTarget(androidx.appcompat.R.id.action_bar_container, true)
}

这样就可以在执行Transition动画的时候,排除StatusBar和默认的ToolBar的动画效果,在xml中,可以在具体的Transition动画标签中设置,如下所示。

<changeBounds>
    <targets android:excludeId="@android:id/statusBarBackground" />
</changeBounds>
Transition Overlap

默认情况下,Transition的动画执行不是线性的,即并非A界面的退出动画执行完毕后才会执行B界面的进入动画,它们的执行是有一定的并行时间的(即默认为true),称之为Overlap,在代码中可以对这个行为进行控制,如下所示。

<item name="android:windowAllowEnterTransitionOverlap">false</item>
<item name="android:windowAllowReturnTransitionOverlap">false</item>

在代码中,可以设置如下。

window.allowEnterTransitionOverlap = true
window.allowReturnTransitionOverlap = true
启动Transition

在启动新的Activity时,需要传入一个特殊的Bundle对象,代码如下所示。

startActivity(
    Intent(this, AnotherActivity::class.java),
    ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
)

用这个方法替换传统的startActivity方法,就可以启动Transition切换动画了。

Shared Element Transition

对于Transition来说,Content Transition单纯的是两个页面间的切换动画,每个页面间都是单独的执行动画过程,而Shared Element Transition则不同,它标记了两个界面切换时需要共享动画效果的元素,让某些指定的元素,动画效果更佳丰富。

而对于执行过程中,Content Transition和Shared Element Transition的流程是一致的,只不过为了区分这两种不同的Transition类型,在原有命名的基础上,增加了sharedElement前缀,如下所示。

window.sharedElementExitTransition

不过一般情况下,sharedElementXXXXXTransition不用设置,因为默认是创建类似ChangeBounds的位移和尺寸改变动画。对于Content Transition来说,通常会使用Fade、Slide、Explode这类继承Visibility的Transition动画,而对于Shared Element Transition来说,动画执行前,需要指定要共享的元素的ID,并分析AB界面中,指定ID的元素的属性变化,从而生成属性动画,所以说,即使是Shared Element Transition,所有的动画效果实际上都是发生在B界面中的,共享的元素并没有在两个界面中传递。

共享元素这个属性的指定,就需要使用android:transitionName来进行指定。

启动Shared Element Transition与Content Transition类似,只是需要指定下共享元素的transitionName,代码如下所示。

val intent = Intent(this, SecondActivity::class.java)
val activityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(this,
        Pair(imageView, "share_image"), Pair(textview, "share_text"))
startActivity(intent, activityOptionsCompat.toBundle())
延迟共享元素动画

在某些情况下,共享元素动画需要延迟一部分时间再执行,例如需要等布局渲染完毕,或者网络图片加载完成后再执行动画。这种场景下,就需要使用延迟加载的方式了,主要涉及的API有两个,即postponeEnterTransition()和startPostponedEnterTransition(),在需要延迟的场景下,先使用postponeEnterTransition暂停动画的执行过程,再在合适的场景下(例如在ViewTree渲染完成或者图片加载完成后),使用startPostponedEnterTransition恢复动画的执行。

这个API也经常用来解决Transition动画切换过程中闪烁的一些问题,例如在进入B界面的时候先暂停动画,在ViewTreeObserver中渲染完毕后再开启Transition动画执行。

SharedElementCallback

考虑这样一个场景,A界面通过RecyclerView展示数据列表,点击Item后跳转B界面,B界面通过ViewPager展示详细数据,当在B界面滑动数据后,回到A界面,A界面应该刷新数据到B界面访问到的数据,这里就需要用到Shared Element Transition提供的SharedElementCallback了。

在上面的场景下,给A界面设置setExitSharedElementCallback(SharedElementCallback),给B界面设置setEnterSharedElementCallback(SharedElementCallback),这样就可以实现更新的回调。

setExitSharedElementCallback(SharedElementCallback):在Activity exit和reenter时都会触发 setEnterSharedElementCallback(SharedElementCallback):在Activity enter和return时都会触发。

使用Transition动画的一般方式

先来看下这样一个效果,如图所示。

transition

结合这样一个例子,我们来看下一般如何处理transition动画,首先,要对动画过程进行拆解,无论做什么动画,这都是第一步。

在使用Transition动画时,大部分的场景都是Content Transition和Shared Element Transition同时使用的,这个例子也是这样,我们可以发现,Image和Text,使用的是Shared Element Transition,而界面B的其它部分,使用的是Content Transition,而界面A,通常不用设置Transition。

Shared Element Transition部分

sharedElementEnterTransition通常也不用设置,默认会使用ChangeBounds,当然,你也可以修改ChangeBounds的默认行为,例如interpolator,arcMotion等。这里需要执行共享元素的Item,就是Image和Text,所以在B界面的XML中,需要指定对应的transitionName即可。界面B的布局代码如下所示。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/top"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <ImageView
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:layout_gravity="center"
            android:src="@mipmap/ic_launcher"
            android:transitionName="share_image" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="40dp"
            android:text="xuyisheng"
            android:textSize="30sp"
            android:transitionName="share_text" />

        <TextView
            android:id="@+id/anotherText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal|bottom"
            android:text="Transition" />

    </FrameLayout>

    <LinearLayout
        android:id="@+id/bottom"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:orientation="vertical">

        <FrameLayout
            android:id="@+id/item1"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_margin="8dp"
            android:background="#bebebe" />

        <FrameLayout
            android:id="@+id/item2"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_margin="8dp"
            android:background="#bebebe" />

        <FrameLayout
            android:id="@+id/item3"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_margin="8dp"
            android:background="#bebebe" />

    </LinearLayout>

</LinearLayout>

界面A的代码比较简单,如下所示。

<?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:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginLeft="32dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.3" />

    <TextView
        android:id="@+id/textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="32dp"
        android:textSize="30sp"
        android:text="xuyisheng"
        app:layout_constraintBottom_toBottomOf="@+id/imageView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/imageView" />

</androidx.constraintlayout.widget.ConstraintLayout>

Content Transition部分

下面的内容和中间的文本,使用的是Content Transition,只需要针对这些元素,做相应的enterTransition即可,代码如下所示enter_anim.xml。

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">

    <fade android:duration="500">
        <targets>
            <target android:targetId="@android:id/statusBarBackground" />
        </targets>
    </fade>

    <slide android:startDelay="600">
        <targets>
            <target android:targetId="@id/item1" />
        </targets>
    </slide>

    <slide android:startDelay="700">
        <targets>
            <target android:targetId="@id/item2" />
        </targets>
    </slide>

    <slide android:startDelay="800">
        <targets>
            <target android:targetId="@id/item3" />
        </targets>
    </slide>

    <slide
        android:slideEdge="left"
        android:startDelay="500">
        <targets>
            <target android:targetId="@id/anotherText" />
        </targets>
    </slide>

</transitionSet>

TransitionListener

所有的Transition,都可以设置TransitionListener来监听其执行过程,代码如下所示。

window.enterTransition =
    TransitionInflater.from(this).inflateTransition(R.transition.enter_anim).apply {
        addListener(object : Transition.TransitionListener {
            override fun onTransitionStart(transition: Transition?) {
            }

            override fun onTransitionEnd(transition: Transition?) {
            }

            override fun onTransitionCancel(transition: Transition?) {
            }

            override fun onTransitionPause(transition: Transition?) {
            }

            override fun onTransitionResume(transition: Transition?) {
            }
        })
    }

例如可以在Transition结束后,执行其他的属性动画等等。

退出动画

在B界面退出的时候,我这里使用了新的动画效果,即设置了returnTransition,并非默认效果,而且这里有一点需要注意,那就是enterTransition时,是针对单独的元素设置的,而returnTransition,则是分成了上下两个部分进行动画(主要是下部分),所以这里需要使用到前面提到的TransitionGroup的概念。在enterTransition的时候,TransitionGroup要设置为false,在returnTransition的时候,TransitionGroup要设置为true(因为ViewGroup只要设置了background或者TransitionName,就会被判断为TransitionGroup为true)。代码如下所示return_anim.xml。

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="800">

    <slide android:slideEdge="top">
        <targets>
            <target android:targetId="@id/top" />
        </targets>
    </slide>

    <slide android:slideEdge="left">
        <targets>
            <target android:targetId="@id/bottom" />
        </targets>
    </slide>

    <fade>
        <targets>
            <target android:targetId="@android:id/statusBarBackground" />
        </targets>
    </fade>

</transitionSet>

组装动画

在分解完这些动画后,就可以将整个过程串联起来了,界面A代码如下所示。

package com.example.myapplication

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        root.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            val activityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(this,
                    Pair(imageView, "share_image"), Pair(textview, "share_text"))
            startActivity(intent, activityOptionsCompat.toBundle())
        }
    }
}

界面B,代码如下所示。

package com.example.myapplication

import android.os.Bundle
import android.transition.Transition
import android.transition.TransitionInflater
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.second.*

class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.second)
        window.enterTransition =
            TransitionInflater.from(this).inflateTransition(R.transition.enter_anim).apply {
                addListener(object : Transition.TransitionListener {
                    override fun onTransitionStart(transition: Transition?) {
                    }

                    override fun onTransitionEnd(transition: Transition?) {
                    }

                    override fun onTransitionCancel(transition: Transition?) {
                    }

                    override fun onTransitionPause(transition: Transition?) {
                    }

                    override fun onTransitionResume(transition: Transition?) {
                    }
                })
            }
        bottom.isTransitionGroup = false
        window.returnTransition =
            TransitionInflater.from(this).inflateTransition(R.transition.return_anim)
    }

    override fun onBackPressed() {
        bottom.isTransitionGroup = true
        super.onBackPressed()
    }
}

通过这种方式,就完成了Transition动画的一般开发过程,总结一下,主要就是下面几个步骤。

  • 拆解动画:将过渡动画拆分成Content Transition和Shared Element Transition

  • 针对Content Transition,对每个元素编写相应的动画

  • 针对Shared Element Transition,确定好TransitionGroup后,指定两个页面之间的transitionName

  • 组装动画:借助生命周期回调等状态,将动画串联起来

自定义Transition

https://developer.android.google.cn/training/transitions/custom-transitions

官网中其实已经给我们提供了非常详细的说明,同时,参考默认的Slide、Fade这些SDK默认Transition的实现,我们可以很方便的自定义,下面就以一个改变background的Transition为例进行讲解。

首先,需要继承Transition,实现下面三个方法。

  • captureStartValues

  • captureEndValues

  • createAnimator

前面两个方法基本都是设置需要自定义的属性值,重要的是最后一个方法,创建属性动画。

const val CHANGE_COLOR = "xys:change_background_color:color"

class ChangeBackgroundColorTransition : Transition() {

    override fun captureStartValues(transitionValues: TransitionValues?) {
        captureValues(transitionValues)
    }

    override fun captureEndValues(transitionValues: TransitionValues?) {
        captureValues(transitionValues)
    }

    private fun captureValues(transitionValues: TransitionValues?) {
        if (transitionValues != null) {
            transitionValues.values[CHANGE_COLOR] = transitionValues.view.background
        }
    }

    override fun createAnimator(
        sceneRoot: ViewGroup?,
        startValues: TransitionValues?,
        endValues: TransitionValues?
    ): Animator? {
        if (startValues == null || endValues == null) {
            return null
        }
        val endView: View = endValues.view
        val startColorDrawable = startValues.values[CHANGE_COLOR] as ColorDrawable?
        val endColorDrawable = endValues.values[CHANGE_COLOR] as ColorDrawable?
        if (startColorDrawable == null || endColorDrawable == null) {
            return super.createAnimator(sceneRoot, startValues, endValues)
        }
        val startColor = startColorDrawable.color
        val endColor = endColorDrawable.color
        return ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor).apply {
            duration = 3000
            addUpdateListener { animation ->
                val animatedValue = animation.animatedValue as Int
                endView.setBackgroundColor(animatedValue)
            }
        }
    }
}

在前面两个函数中,TransitionValues起到了一个容器的作用,保存了values和views,指定了需要作用的对象和值

使用和系统默认的Transition一样,示例代码如下所示。

TransitionManager.beginDelayedTransition(root, ChangeBackgroundColorTransition())
textview.background = ColorDrawable(Color.parseColor("#bebebe"))

可以发现,实际上Transition就是为不同的属性创建属性动画而已,从自定义Transition就可以看出它的本质。

开源库

最后,推荐几个自定义Transition开源库。

https://github.com/HJ-Money/MTransition

https://github.com/ImmortalZ/TransitionHelper

https://github.com/lgvalle/Material-Animations

https://github.com/andkulikov/Transitions-Everywhere

预备役选手?

好了,终于到最后了,讲了这么多Transition的使用方法,那么为什么我还叫他预备役选手呢?这就是因为,在Material Design Component中,对Motion进行了进一步的封装,即:

  • Container transform

  • Shared axis

  • Fade through

  • Fade

这样四种封装好的Motion,而Transition,则正是它们的基础原理。

所以,Transition现在虽然用的不多,但是掌握了它的原理,才能更好的开启MDC Motion之旅。

最后,介绍下我的网站:https://xuyisheng.top/  点击原文,一键直达

Flutter & Android 关注 《Android群英传》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值