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,代码如下所示。
"1.0" encoding="utf-8"?>
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">
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" />
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" />
另一个布局,只有Item的位置发生的改变,id不变,这里就不贴重复代码了,要记住的是,对于一个元素的动画来说,在不同的Scene中,只要id不变,元素就不变,元素位置、属性的改变,这就是动画效果。
创建Scene Container
一般来说,在一个静态布局下,创建具有多个Scene的布局,会将动静部分分离,将要展示动画的部分,放置在一个Container中,便于管理,在前面创建好Scene Layout后,下面在主界面的xml中,创建它们的Container,代码如下所示。
"1.0" encoding="utf-8"?>
"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">
"@layout/base_scene1" />
通过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,即渐隐渐显合并位移动画,源码如下所示。
所以这里还可以指定动画效果,例如我们只指定位置改变的动画,代码如下所示。
TransitionManager.go(scene2, ChangeBounds())
SDK内置了很多种类的动画效果,如图所示。
其中几种比较常用的解析如下。
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的描述文件,代码如下所示。
"1.0" encoding="utf-8"?>
在代码中,就可以通过类似LayoutInflater的方式来创建Transition,代码如下所示。
TransitionManager.go(
scene2,
TransitionInflater.from(this).inflateTransition(R.transition.transition_from_xml)
)
除了创建transitionSet的复合动画效果,创建单个的transition动画也是一样的,例如下面的代码。
"1.0" encoding="utf-8"?>
"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的动画效果,最后效果如下所示。
类似的,你还可以设置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开关,如下所示。
如果你使用的是Material Design Theme,那么这个值默认为true。
在代码中,可以设置如下所示。
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
设置Transition切换的两种方式
Transition的切换设定,可以在代码或者Theme中进行设置。
在Theme中,可以设置如下。
在代码中,可以如下所示。
window.exitTransition = Explode()
window.reenterTransition = Slide()
一般来说,如果是针对全局的设置,可以放在Theme中,但是在代码中设置,会更加灵活。
动画默认的持续时间,也是可以设置的,代码如下所示。
window.transitionBackgroundFadeDuration = 3000
Transition View & Transition Group
前面在讲解Content Transition的执行过程的时候,提到了在动画开始前,系统会调用ViewGroup.captureTransitioningViews函数,来获取需要进行Transition处理的View,如图所示。
在默认情况下,Transition Group的判断如下所示。
另外,在代码中,还可以通过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动画标签中设置,如下所示。
"@android:id/statusBarBackground" />
Transition Overlap
默认情况下,Transition的动画执行不是线性的,即并非A界面的退出动画执行完毕后才会执行B界面的进入动画,它们的执行是有一定的并行时间的(即默认为true),称之为Overlap,在代码中可以对这个行为进行控制,如下所示。
- "android:windowAllowEnterTransitionOverlap">false
- "android:windowAllowReturnTransitionOverlap">false
在代码中,可以设置如下。
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动画时,大部分的场景都是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的布局代码如下所示。
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:id="@+id/top"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center"
android:src="@mipmap/ic_launcher"
android:transitionName="share_image" />
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" />
android:id="@+id/anotherText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:text="Transition" />
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
android:id="@+id/item1"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_margin="8dp"
android:background="#bebebe" />
android:id="@+id/item2"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_margin="8dp"
android:background="#bebebe" />
android:id="@+id/item3"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_margin="8dp"
android:background="#bebebe" />
界面A的代码比较简单,如下所示。
"1.0" encoding="utf-8"?>
"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">
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" />
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" />
Content Transition部分
下面的内容和中间的文本,使用的是Content Transition,只需要针对这些元素,做相应的enterTransition即可,代码如下所示enter_anim.xml。
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android">
"500">
"@android:id/statusBarBackground" />
"600">
"@id/item1" />
"700">
"@id/item2" />
"800">
"@id/item3" />
android:slideEdge="left"
android:startDelay="500">
"@id/anotherText" />
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。
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
android:duration="800">
"top">
"@id/top" />
"left">
"@id/bottom" />
"@android:id/statusBarBackground" />
组装动画
在分解完这些动画后,就可以将整个过程串联起来了,界面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群英传》