小言_互联网的博客

Material Components—预备役选手Transition

322人阅读  评论(0)

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. <?xml version= "1.0" encoding= "utf-8"?>
  2. <androidx.constraintlayout.widget.ConstraintLayout 
  3.     xmlns:android= "http://schemas.android.com/apk/res/android"
  4.     xmlns:app= "http://schemas.android.com/apk/res-auto"
  5.     android:layout_width= "match_parent"
  6.     android:layout_height= "match_parent">
  7.     <ImageView
  8.         android:id= "@+id/imageView1"
  9.         android:layout_width= "100dp"
  10.         android:layout_height= "100dp"
  11.         app:layout_constraintStart_toStartOf= "parent"
  12.         app:layout_constraintTop_toTopOf= "parent"
  13.         app:srcCompat= "@mipmap/ic_launcher" />
  14.     <ImageView
  15.         android:id= "@+id/imageView2"
  16.         android:layout_width= "100dp"
  17.         android:layout_height= "100dp"
  18.         app:layout_constraintBottom_toBottomOf= "parent"
  19.         app:layout_constraintEnd_toEndOf= "parent"
  20.         app:srcCompat= "@android:mipmap/sym_def_app_icon" />
  21. </androidx.constraintlayout.widget.ConstraintLayout>

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

创建Scene Container

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


   
  1. <?xml version= "1.0" encoding= "utf-8"?>
  2. <FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
  3.     xmlns:tools= "http://schemas.android.com/tools"
  4.     android:id= "@+id/rootContainer"
  5.     android:layout_width= "match_parent"
  6.     android:layout_height= "match_parent"
  7.     tools:context= ".MainActivity">
  8.     <include layout= "@layout/base_scene1" />
  9. </FrameLayout>

通过TransitionManager驱动动画

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


   
  1. class MainActivity : AppCompatActivity() {
  2.      var flag =  true
  3.     override fun onCreate(savedInstanceState: Bundle?) {
  4.         super.onCreate(savedInstanceState)
  5.         setContentView(R.layout.activity_main)
  6.         val scene1 = Scene.getSceneForLayout(rootContainer, R.layout.base_scene1, this)
  7.         val scene2 = Scene.getSceneForLayout(rootContainer, R.layout.base_scene2, this)
  8.         rootContainer.setOnClickListener {
  9.              if (flag) {
  10.                 TransitionManager. go(scene2)
  11.             }  else {
  12.                 TransitionManager. go(scene1)
  13.             }
  14.             flag = !flag
  15.         }
  16.     }
  17. }

当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的描述文件,代码如下所示。


   
  1. <?xml version= "1.0" encoding= "utf-8"?>
  2. <transitionSet>
  3.     <explode />
  4.     <fade />
  5.     
  6. </transitionSet>

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


   
  1. TransitionManager. go(
  2.     scene2,
  3.     TransitionInflater.from(this).inflateTransition(R.transition.transition_from_xml)
  4. )

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


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

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

通过代码创建

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


   
  1. Slide().apply {
  2.     duration =  200
  3.     slideEdge = Gravity.BOTTOM
  4. }

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


   
  1. TransitionSet().apply {
  2.     addTransition(Fade())
  3.     addTransition(Slide())
  4. }

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

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

beginDelayedTransition

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

下面通过代码来演示下。


   
  1. rootContainer.setOnClickListener {
  2.     val size = imageView1.width
  3.     TransitionManager.beginDelayedTransition(
  4.         rootContainer, 
  5.         TransitionInflater.from(this).inflateTransition(R.transition.transition_from_xml))
  6.     val layoutParams = imageView1.layoutParams
  7.      if (flag) {
  8.         layoutParams.width = (size /  1.2).toInt()
  9.         layoutParams.height = (size /  1.2).toInt()
  10.         imageView1.layoutParams = layoutParams
  11.         imageView2.visibility = View.VISIBLE
  12.         imageView3.visibility = View.VISIBLE
  13.         imageView4.visibility = View.VISIBLE
  14.     }  else {
  15.         layoutParams.width = (size *  1.2).toInt()
  16.         layoutParams.height = (size *  1.2).toInt()
  17.         imageView1.layoutParams = layoutParams
  18.         imageView2.visibility = View.INVISIBLE
  19.         imageView3.visibility = View.INVISIBLE
  20.         imageView4.visibility = View.INVISIBLE
  21.     }
  22.     flag = !flag
  23. }

当我们调用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,示例代码如下所示。


   
  1. // 确定Explode中心点坐标
  2. val viewRect = Rect()
  3. clickedView.getGlobalVisibleRect(viewRect)
  4. // 设置Explode Epicenter
  5. val explode: Transition = Explode().apply {
  6.     epicenterCallback = object : Transition.EpicenterCallback() {
  7.         override fun onGetEpicenter(transition: Transition?): Rect {
  8.              return viewRect
  9.         }
  10.     }
  11. }
  12. explode.duration =  1000

ChangeImageTransform

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


   
  1. TransitionSet().apply {
  2.     addTransition(ChangeBounds())
  3.     addTransition(ChangeImageTransform())
  4. }

ChangeBounds

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


   
  1. TransitionManager.beginDelayedTransition(transitionsContainer,
  2.         ChangeBounds().apply {
  3.             pathMotion = ArcMotion().also { duration =  300 }
  4.         })
  5. val params = button.getLayoutParams() as FrameLayout.LayoutParams
  6. params.gravity =  if (isReturnAnimation) Gravity.LEFT or Gravity.TOP  else Gravity.BOTTOM or Gravity.RIGHT
  7. 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

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


   
  1. window.exitTransition = Explode()
  2. 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切换动画的元素。

在代码中,设置如下。


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

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


   
  1. <changeBounds>
  2.     <targets android:excludeId= "@android:id/statusBarBackground" />
  3. </changeBounds>

Transition Overlap

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


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

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


   
  1. window.allowEnterTransitionOverlap =  true
  2. window.allowReturnTransitionOverlap =  true

启动Transition

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


   
  1. startActivity(
  2.     Intent(this, AnotherActivity::class.java),
  3.     ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
  4. )

用这个方法替换传统的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,代码如下所示。


   
  1. val intent = Intent(this, SecondActivity::class.java)
  2. val activityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(this,
  3.         Pair(imageView,  "share_image"), Pair(textview,  "share_text"))
  4. 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的布局代码如下所示。


   
  1. <?xml version= "1.0" encoding= "utf-8"?>
  2. <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android"
  3.     android:layout_width= "match_parent"
  4.     android:layout_height= "match_parent"
  5.     android:orientation= "vertical">
  6.     <FrameLayout
  7.         android:id= "@+id/top"
  8.         android:layout_width= "match_parent"
  9.         android:layout_height= "0dp"
  10.         android:layout_weight= "1">
  11.         <ImageView
  12.             android:layout_width= "150dp"
  13.             android:layout_height= "150dp"
  14.             android:layout_gravity= "center"
  15.             android:src= "@mipmap/ic_launcher"
  16.             android:transitionName= "share_image" />
  17.         <TextView
  18.             android:layout_width= "wrap_content"
  19.             android:layout_height= "wrap_content"
  20.             android:layout_gravity= "center_horizontal"
  21.             android:layout_marginTop= "40dp"
  22.             android:text= "xuyisheng"
  23.             android:textSize= "30sp"
  24.             android:transitionName= "share_text" />
  25.         <TextView
  26.             android:id= "@+id/anotherText"
  27.             android:layout_width= "wrap_content"
  28.             android:layout_height= "wrap_content"
  29.             android:layout_gravity= "center_horizontal|bottom"
  30.             android:text= "Transition" />
  31.     </FrameLayout>
  32.     <LinearLayout
  33.         android:id= "@+id/bottom"
  34.         android:layout_width= "match_parent"
  35.         android:layout_height= "0dp"
  36.         android:layout_weight= "1"
  37.         android:orientation= "vertical">
  38.         <FrameLayout
  39.             android:id= "@+id/item1"
  40.             android:layout_width= "match_parent"
  41.             android:layout_height= "60dp"
  42.             android:layout_margin= "8dp"
  43.             android:background= "#bebebe" />
  44.         <FrameLayout
  45.             android:id= "@+id/item2"
  46.             android:layout_width= "match_parent"
  47.             android:layout_height= "60dp"
  48.             android:layout_margin= "8dp"
  49.             android:background= "#bebebe" />
  50.         <FrameLayout
  51.             android:id= "@+id/item3"
  52.             android:layout_width= "match_parent"
  53.             android:layout_height= "60dp"
  54.             android:layout_margin= "8dp"
  55.             android:background= "#bebebe" />
  56.     </LinearLayout>
  57. </LinearLayout>

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


   
  1. <?xml version= "1.0" encoding= "utf-8"?>
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android= "http://schemas.android.com/apk/res/android"
  3.     xmlns:app= "http://schemas.android.com/apk/res-auto"
  4.     xmlns:tools= "http://schemas.android.com/tools"
  5.     android:id= "@+id/root"
  6.     android:layout_width= "match_parent"
  7.     android:layout_height= "match_parent"
  8.     tools:context= ".MainActivity">
  9.     <ImageView
  10.         android:id= "@+id/imageView"
  11.         android:layout_width= "50dp"
  12.         android:layout_height= "50dp"
  13.         android:layout_marginLeft= "32dp"
  14.         android:src= "@mipmap/ic_launcher"
  15.         app:layout_constraintBottom_toBottomOf= "parent"
  16.         app:layout_constraintLeft_toLeftOf= "parent"
  17.         app:layout_constraintTop_toTopOf= "parent"
  18.         app:layout_constraintVertical_bias= "0.3" />
  19.     <TextView
  20.         android:id= "@+id/textview"
  21.         android:layout_width= "wrap_content"
  22.         android:layout_height= "wrap_content"
  23.         android:layout_marginRight= "32dp"
  24.         android:textSize= "30sp"
  25.         android:text= "xuyisheng"
  26.         app:layout_constraintBottom_toBottomOf= "@+id/imageView"
  27.         app:layout_constraintEnd_toEndOf= "parent"
  28.         app:layout_constraintTop_toTopOf= "@+id/imageView" />
  29. </androidx.constraintlayout.widget.ConstraintLayout>

Content Transition部分

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


   
  1. <?xml version= "1.0" encoding= "utf-8"?>
  2. <transitionSet xmlns:android= "http://schemas.android.com/apk/res/android">
  3.     <fade android:duration= "500">
  4.         <targets>
  5.             <target android:targetId= "@android:id/statusBarBackground" />
  6.         </targets>
  7.     </fade>
  8.     <slide android:startDelay= "600">
  9.         <targets>
  10.             <target android:targetId= "@id/item1" />
  11.         </targets>
  12.     </slide>
  13.     <slide android:startDelay= "700">
  14.         <targets>
  15.             <target android:targetId= "@id/item2" />
  16.         </targets>
  17.     </slide>
  18.     <slide android:startDelay= "800">
  19.         <targets>
  20.             <target android:targetId= "@id/item3" />
  21.         </targets>
  22.     </slide>
  23.     <slide
  24.         android:slideEdge= "left"
  25.         android:startDelay= "500">
  26.         <targets>
  27.             <target android:targetId= "@id/anotherText" />
  28.         </targets>
  29.     </slide>
  30. </transitionSet>

TransitionListener

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


   
  1. window.enterTransition =
  2.     TransitionInflater.from(this).inflateTransition(R.transition.enter_anim).apply {
  3.         addListener(object : Transition.TransitionListener {
  4.             override fun onTransitionStart(transition: Transition?) {
  5.             }
  6.             override fun onTransitionEnd(transition: Transition?) {
  7.             }
  8.             override fun onTransitionCancel(transition: Transition?) {
  9.             }
  10.             override fun onTransitionPause(transition: Transition?) {
  11.             }
  12.             override fun onTransitionResume(transition: Transition?) {
  13.             }
  14.         })
  15.     }

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

退出动画

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


   
  1. <?xml version= "1.0" encoding= "utf-8"?>
  2. <transitionSet xmlns:android= "http://schemas.android.com/apk/res/android"
  3.     android:duration= "800">
  4.     <slide android:slideEdge= "top">
  5.         <targets>
  6.             <target android:targetId= "@id/top" />
  7.         </targets>
  8.     </slide>
  9.     <slide android:slideEdge= "left">
  10.         <targets>
  11.             <target android:targetId= "@id/bottom" />
  12.         </targets>
  13.     </slide>
  14.     <fade>
  15.         <targets>
  16.             <target android:targetId= "@android:id/statusBarBackground" />
  17.         </targets>
  18.     </fade>
  19. </transitionSet>

组装动画

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


   
  1. package com.example.myapplication
  2. import android.content.Intent
  3. import android.os.Bundle
  4. import androidx.appcompat.app.AppCompatActivity
  5. import androidx.core.app.ActivityOptionsCompat
  6. import androidx.core.util.Pair
  7. import kotlinx.android.synthetic.main.activity_main.*
  8. class MainActivity : AppCompatActivity() {
  9.     override fun onCreate(savedInstanceState: Bundle?) {
  10.         super.onCreate(savedInstanceState)
  11.         setContentView(R.layout.activity_main)
  12.         root.setOnClickListener {
  13.             val intent = Intent(this, SecondActivity::class.java)
  14.             val activityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(this,
  15.                     Pair(imageView,  "share_image"), Pair(textview,  "share_text"))
  16.             startActivity(intent, activityOptionsCompat.toBundle())
  17.         }
  18.     }
  19. }

界面B,代码如下所示。


   
  1. package com.example.myapplication
  2. import android.os.Bundle
  3. import android.transition.Transition
  4. import android.transition.TransitionInflater
  5. import androidx.appcompat.app.AppCompatActivity
  6. import kotlinx.android.synthetic.main.second.*
  7. class SecondActivity : AppCompatActivity() {
  8.     override fun onCreate(savedInstanceState: Bundle?) {
  9.         super.onCreate(savedInstanceState)
  10.         setContentView(R.layout.second)
  11.         window.enterTransition =
  12.             TransitionInflater.from(this).inflateTransition(R.transition.enter_anim).apply {
  13.                 addListener(object : Transition.TransitionListener {
  14.                     override fun onTransitionStart(transition: Transition?) {
  15.                     }
  16.                     override fun onTransitionEnd(transition: Transition?) {
  17.                     }
  18.                     override fun onTransitionCancel(transition: Transition?) {
  19.                     }
  20.                     override fun onTransitionPause(transition: Transition?) {
  21.                     }
  22.                     override fun onTransitionResume(transition: Transition?) {
  23.                     }
  24.                 })
  25.             }
  26.         bottom.isTransitionGroup =  false
  27.         window.returnTransition =
  28.             TransitionInflater.from(this).inflateTransition(R.transition.return_anim)
  29.     }
  30.     override fun onBackPressed() {
  31.         bottom.isTransitionGroup =  true
  32.         super.onBackPressed()
  33.     }
  34. }

通过这种方式,就完成了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

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


   
  1. const val CHANGE_COLOR =  "xys:change_background_color:color"
  2. class ChangeBackgroundColorTransition : Transition() {
  3.     override fun captureStartValues(transitionValues: TransitionValues?) {
  4.         captureValues(transitionValues)
  5.     }
  6.     override fun captureEndValues(transitionValues: TransitionValues?) {
  7.         captureValues(transitionValues)
  8.     }
  9.     private fun captureValues(transitionValues: TransitionValues?) {
  10.          if (transitionValues != null) {
  11.             transitionValues.values[CHANGE_COLOR] = transitionValues.view.background
  12.         }
  13.     }
  14.     override fun createAnimator(
  15.         sceneRoot: ViewGroup?,
  16.         startValues: TransitionValues?,
  17.         endValues: TransitionValues?
  18.     ): Animator? {
  19.          if (startValues == null || endValues == null) {
  20.              return null
  21.         }
  22.         val endView: View = endValues.view
  23.         val startColorDrawable = startValues.values[CHANGE_COLOR] as ColorDrawable?
  24.         val endColorDrawable = endValues.values[CHANGE_COLOR] as ColorDrawable?
  25.          if (startColorDrawable == null || endColorDrawable == null) {
  26.              return super.createAnimator(sceneRoot, startValues, endValues)
  27.         }
  28.         val startColor = startColorDrawable.color
  29.         val endColor = endColorDrawable.color
  30.          return ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor).apply {
  31.             duration =  3000
  32.             addUpdateListener { animation ->
  33.                 val animatedValue = animation.animatedValue as Int
  34.                 endView.setBackgroundColor(animatedValue)
  35.             }
  36.         }
  37.     }
  38. }

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

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


   
  1. TransitionManager.beginDelayedTransition(root, ChangeBackgroundColorTransition())
  2. 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群英传》


转载:https://blog.csdn.net/x359981514/article/details/111399252
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场