Material Components—預(yù)備役選手Transition
Transition是Android Framework在4.4引入的一個全新的動畫框架,可以說是非常古老了,那為什么我現(xiàn)在還要講Transition呢,其實是想通過Transition來引入Material Design Motion。Transition實際上是MD Motion的基礎(chǔ),同時,也是現(xiàn)代化Android開發(fā)動畫的基礎(chǔ)。
國際慣例,官網(wǎng)鎮(zhèn)樓。
https://developer.android.google.cn/training/transitions
基礎(chǔ)概念
其實從當時的設(shè)計來看,Google在提出Transition框架的時候,就已經(jīng)準備通過申明式UI的方式來創(chuàng)建動畫了,Transition框架的一個核心概念就是Scene,它描述的是一個場景,一個動畫的狀態(tài)值,通常情況下,是動畫的起始狀態(tài)值,有了這樣一個起始態(tài),再加上動畫的具體類型,就可以完整的描述一個動畫的執(zhí)行過程,所以,在申明式的UI編程中,一切都是以Scene作為基礎(chǔ)來進行的。
Transition的本質(zhì),實際上就是根據(jù)狀態(tài)差異來生成屬性動畫,它實際上是對屬性動畫的抽象和封裝。
下面通過一個簡單的例子,來演示下如何使用Scene。
創(chuàng)建Scene Layout
首先,創(chuàng)建兩個Scene Layout,用于描述動畫的兩個狀態(tài),這里簡單的創(chuàng)建兩個布局,一個布局在左上角和右下角展示一個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的位置發(fā)生的改變,id不變,這里就不貼重復(fù)代碼了,要記住的是,對于一個元素的動畫來說,在不同的Scene中,只要id不變,元素就不變,元素位置、屬性的改變,這就是動畫效果。
創(chuàng)建Scene Container
一般來說,在一個靜態(tài)布局下,創(chuàng)建具有多個Scene的布局,會將動靜部分分離,將要展示動畫的部分,放置在一個Container中,便于管理,在前面創(chuàng)建好Scene Layout后,下面在主界面的xml中,創(chuàng)建它們的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驅(qū)動動畫
在代碼中,通過Scene.getSceneForLayout來創(chuàng)建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發(fā)生改變時,TransitionManager會自動為其生成相應(yīng)的動畫效果。默認情況下,TransitionManager使用AutoTransition,即漸隱漸顯合并位移動畫,源碼如下所示。

所以這里還可以指定動畫效果,例如我們只指定位置改變的動畫,代碼如下所示。
TransitionManager.go(scene2,?ChangeBounds())
SDK內(nèi)置了很多種類的動畫效果,如圖所示。

其中幾種比較常用的解析如下。
ChangeBounds:檢測view的位置邊界,創(chuàng)建移動和縮放動畫 ChangeTransform:檢測view的scale和rotation,創(chuàng)建縮放和旋轉(zhuǎn)動畫 ChangeClipBounds:檢測view的剪切區(qū)域的位置邊界,和ChangeBounds類似,ChangeBounds指定的是剪切區(qū)域setClipBound中的rect ChangeImageTransform:檢測ImageView的大小、位置以及ScaleType,并創(chuàng)建相應(yīng)動畫 ChangeScroll:檢測ViewGroup的Scroll,創(chuàng)建Scroll動畫 Fade、Slide、Explode:檢測View的Visibility,創(chuàng)建漸入、滑動、爆炸動畫
創(chuàng)建Transition動畫的幾種方式
不論是transition的哪種使用方式,transition動畫都有以下幾種創(chuàng)建方式。
通過xml創(chuàng)建Transition動畫
在res/transition下創(chuàng)建一個transitionSet的描述文件,代碼如下所示。
"1.0"?encoding="utf-8"?>
????
????
????
在代碼中,就可以通過類似LayoutInflater的方式來創(chuàng)建Transition,代碼如下所示。
TransitionManager.go(
????scene2,
????TransitionInflater.from(this).inflateTransition(R.transition.transition_from_xml)
)
除了創(chuàng)建transitionSet的復(fù)合動畫效果,創(chuàng)建單個的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進行創(chuàng)建。
通過代碼創(chuàng)建
對于單個的transition動畫,可以通過下面的方式進行創(chuàng)建。
Slide().apply?{
????duration?=?200
????slideEdge?=?Gravity.BOTTOM
}
對于復(fù)合的transition動畫,可以通過下面的方式進行創(chuàng)建。
TransitionSet().apply?{
????addTransition(Fade())
????addTransition(Slide())
}
前面提到到AutoTransition,也是繼承的TransitionSet實現(xiàn)的復(fù)合動畫。
不論是怎么使用transition動畫,這些創(chuàng)建transition的方式都是可以混用的。
beginDelayedTransition
在前面的講解中,TransitionManager.go是基于場景Scene切換而產(chǎn)生的動畫效果。而Transition框架還提供一種類似自動檢測的動畫機制,這就是通過beginDelayedTransition來實現(xiàn)的。
下面通過代碼來演示下。
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
}
當我們調(diào)用TransitionManager.beginDelayedTransition后,相當于在當前狀態(tài)下打了個tag,將當前狀態(tài)下的View屬性,創(chuàng)建為初始Scene,在此之后View發(fā)生的屬性改變,都將被生成新的Scene,從而產(chǎn)生動畫效果,這也就是beginDelayedTransition這個API命名的原因。
在上面的代碼中,在初始場景下,調(diào)用了beginDelayedTransition,創(chuàng)建的動畫是changeBounds和explode,在這之后,修改了4個ImageView的屬性——尺寸和visibility,并被作用了changeBounds和explode的動畫效果,最后效果如下所示。

類似的,你還可以設(shè)置Slide這樣的visibility動畫效果,實現(xiàn)滑動的切換效果。
動畫效果進階
Slide
和Fade效果類似,它們都是繼承自Visibility,它比Fade多了一些屬性,除了可以設(shè)置屬性動畫的一些常見屬性外,還可以設(shè)置Slide方向等屬性。
Explode
Explode與Slide十分相似,但是元素將根據(jù)Transition Epicenter,輻射狀移動,這個Epicenter可以通過setEpicenterCallback來設(shè)置。
在Explode中,動畫通過TransitionPropagation計算每個動畫的開始延遲,例如,默認情況下Explode使用CircularPropagation,動畫的延遲取決于元素和Epicenter之間的距離,在代碼中,可以通過setPropagation來設(shè)置自定義的TransitionPropagation,示例代碼如下所示。
//?確定Explode中心點坐標
val?viewRect?=?Rect()
clickedView.getGlobalVisibleRect(viewRect)
//?設(shè)置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,可以設(shè)置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可以設(shè)置minimumHorizontalAngle、minimumVerticalAngle、maximumAngle這樣的屬性來設(shè)置路徑的具體形態(tài)。
當然,你也可以通過patternPathMotion來設(shè)置類似SVG的自定義路徑。
setTransitionName
在使用beginDelayedTransition執(zhí)行Transition動畫時,可以通過設(shè)置transitionName來指定動畫場景起始的相同元素,并讓這些元素執(zhí)行transition動畫,例如為當前界面中的N個元素setTransitionName,當移除界面上全部元素后,只要setTransitionName的值相同,這些元素依然可以執(zhí)行動畫效果。
Transition界面切換
同樣的,官網(wǎng)鎮(zhèn)樓。
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界面確定要執(zhí)行動畫的view(非共享元素view),執(zhí)行
A.exitTransition()前A界面會獲取界面的start scene(view 處于VISIBLE狀態(tài)),然后將所有的要執(zhí)行動畫的view設(shè)置為INVISIBLE,并獲取此時的end scene(view 處于INVISIBLE狀態(tài)).根據(jù)transition分析差異的不同創(chuàng)建執(zhí)行動畫。B.enterTransition Transition框架會先遍歷B界面,確定要執(zhí)行動畫的view,設(shè)置為INVISIBLE。執(zhí)行
B.enterTransition()前獲取此時的start scene(view 處于INVISIBLE狀態(tài)),然后將所有的要執(zhí)行動畫的view設(shè)置為VISIBLE,并獲取此時的end scene(view 處于VISIBLE狀態(tài)).根據(jù)transition分析差異的不同創(chuàng)建執(zhí)行動畫。
同理,在從B -> A,返回到A時,流程類似,只不過調(diào)用的方法不同。
A.reenterTransition B.returnTransition
界面切換動畫是建立在visibility的改變的基礎(chǔ)上的,所以
getWindow().setEnterTransition(transition);中的參數(shù)一般傳的是Fade,Slide,Explode類的實例(因為這三個類是通過分析visibility不同創(chuàng)
簡而言之。
A.exitTransition(): 從A->B時,A的退出動畫
B.enterTransition(): 從A->B時,B的進場動畫
B.returnTransition(): 從B->A時,B的退出動畫
A.reenterTransition(): 從B->A時,A的進場動畫
一般來說,如果不設(shè)置returnTransition和reenterTransition,那么這兩個場景的動畫,會使用exitTransition和enterTransition的反轉(zhuǎn)動畫。
下面就通過一個例子來演示下如何設(shè)置界面切換的動畫效果。
要注意的是,Transition的實現(xiàn)有兩個版本,platform版和AndroidX版,他們的差異在于,AndroidX版的Transition是后續(xù)會持續(xù)迭代的版本,但是不支持Activity和Window間的動畫(至于為什么要這樣設(shè)計,我在之前的文章中已經(jīng)解釋過了),platform版支持,但是后續(xù)不再維護。
首先,在Theme中設(shè)置Transition開關(guān),如下所示。

如果你使用的是Material Design Theme,那么這個值默認為true。
在代碼中,可以設(shè)置如下所示。
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
設(shè)置Transition切換的兩種方式
Transition的切換設(shè)定,可以在代碼或者Theme中進行設(shè)置。
在Theme中,可以設(shè)置如下。

在代碼中,可以如下所示。
window.exitTransition?=?Explode()
window.reenterTransition?=?Slide()
一般來說,如果是針對全局的設(shè)置,可以放在Theme中,但是在代碼中設(shè)置,會更加靈活。
動畫默認的持續(xù)時間,也是可以設(shè)置的,代碼如下所示。
window.transitionBackgroundFadeDuration?=?3000
Transition View & Transition Group
前面在講解Content Transition的執(zhí)行過程的時候,提到了在動畫開始前,系統(tǒng)會調(diào)用ViewGroup.captureTransitioningViews函數(shù),來獲取需要進行Transition處理的View,如圖所示。

在默認情況下,Transition Group的判斷如下所示。

另外,在代碼中,還可以通過View.setTransitionGroup(boolean)來主動將一部分View設(shè)置為Transition Group,從而在整體上執(zhí)行動畫。
為什么會有這樣一個需求呢?其實很明顯,Transition會遍歷頁面中的所有View,包括Toolbar、StatusBar這類的可能通用的組件,那么這個時候,在生成Transition切換動畫的時候,就會產(chǎn)生一些不和諧的畫面,比如這些通用組件的錯位,所以,Transition框架提供了addTarget和excludeTarget方法來指定需要執(zhí)行Transition切換動畫的元素。
在代碼中,設(shè)置如下。
window.returnTransition?=?Slide().apply?{
????slideEdge?=?Gravity.BOTTOM
????excludeTarget(android.R.id.statusBarBackground,?true)
????excludeTarget(androidx.appcompat.R.id.action_bar_container,?true)
}
這樣就可以在執(zhí)行Transition動畫的時候,排除StatusBar和默認的ToolBar的動畫效果,在xml中,可以在具體的Transition動畫標簽中設(shè)置,如下所示。
????"@android:id/statusBarBackground"?/>
Transition Overlap
默認情況下,Transition的動畫執(zhí)行不是線性的,即并非A界面的退出動畫執(zhí)行完畢后才會執(zhí)行B界面的進入動畫,它們的執(zhí)行是有一定的并行時間的(即默認為true),稱之為Overlap,在代碼中可以對這個行為進行控制,如下所示。
- "android:windowAllowEnterTransitionOverlap">false
- "android:windowAllowReturnTransitionOverlap">false
在代碼中,可以設(shè)置如下。
window.allowEnterTransitionOverlap?=?true
window.allowReturnTransitionOverlap?=?true
啟動Transition
在啟動新的Activity時,需要傳入一個特殊的Bundle對象,代碼如下所示。
startActivity(
????Intent(this,?AnotherActivity::class.java),
????ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
)
用這個方法替換傳統(tǒng)的startActivity方法,就可以啟動Transition切換動畫了。
Shared Element Transition
對于Transition來說,Content Transition單純的是兩個頁面間的切換動畫,每個頁面間都是單獨的執(zhí)行動畫過程,而Shared Element Transition則不同,它標記了兩個界面切換時需要共享動畫效果的元素,讓某些指定的元素,動畫效果更佳豐富。
而對于執(zhí)行過程中,Content Transition和Shared Element Transition的流程是一致的,只不過為了區(qū)分這兩種不同的Transition類型,在原有命名的基礎(chǔ)上,增加了sharedElement前綴,如下所示。
window.sharedElementExitTransition
不過一般情況下,sharedElementXXXXXTransition不用設(shè)置,因為默認是創(chuàng)建類似ChangeBounds的位移和尺寸改變動畫。對于Content Transition來說,通常會使用Fade、Slide、Explode這類繼承Visibility的Transition動畫,而對于Shared Element Transition來說,動畫執(zhí)行前,需要指定要共享的元素的ID,并分析AB界面中,指定ID的元素的屬性變化,從而生成屬性動畫,所以說,即使是Shared Element Transition,所有的動畫效果實際上都是發(fā)生在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())
延遲共享元素動畫
在某些情況下,共享元素動畫需要延遲一部分時間再執(zhí)行,例如需要等布局渲染完畢,或者網(wǎng)絡(luò)圖片加載完成后再執(zhí)行動畫。這種場景下,就需要使用延遲加載的方式了,主要涉及的API有兩個,即postponeEnterTransition()和startPostponedEnterTransition(),在需要延遲的場景下,先使用postponeEnterTransition暫停動畫的執(zhí)行過程,再在合適的場景下(例如在ViewTree渲染完成或者圖片加載完成后),使用startPostponedEnterTransition恢復(fù)動畫的執(zhí)行。
這個API也經(jīng)常用來解決Transition動畫切換過程中閃爍的一些問題,例如在進入B界面的時候先暫停動畫,在ViewTreeObserver中渲染完畢后再開啟Transition動畫執(zhí)行。
SharedElementCallback
考慮這樣一個場景,A界面通過RecyclerView展示數(shù)據(jù)列表,點擊Item后跳轉(zhuǎn)B界面,B界面通過ViewPager展示詳細數(shù)據(jù),當在B界面滑動數(shù)據(jù)后,回到A界面,A界面應(yīng)該刷新數(shù)據(jù)到B界面訪問到的數(shù)據(jù),這里就需要用到Shared Element Transition提供的SharedElementCallback了。
在上面的場景下,給A界面設(shè)置setExitSharedElementCallback(SharedElementCallback),給B界面設(shè)置setEnterSharedElementCallback(SharedElementCallback),這樣就可以實現(xiàn)更新的回調(diào)。
setExitSharedElementCallback(SharedElementCallback):在Activity exit和reenter時都會觸發(fā) setEnterSharedElementCallback(SharedElementCallback):在Activity enter和return時都會觸發(fā)。
使用Transition動畫的一般方式
先來看下這樣一個效果,如圖所示。

結(jié)合這樣一個例子,我們來看下一般如何處理transition動畫,首先,要對動畫過程進行拆解,無論做什么動畫,這都是第一步。
在使用Transition動畫時,大部分的場景都是Content Transition和Shared Element Transition同時使用的,這個例子也是這樣,我們可以發(fā)現(xiàn),Image和Text,使用的是Shared Element Transition,而界面B的其它部分,使用的是Content Transition,而界面A,通常不用設(shè)置Transition。
Shared Element Transition部分
sharedElementEnterTransition通常也不用設(shè)置,默認會使用ChangeBounds,當然,你也可以修改ChangeBounds的默認行為,例如interpolator,arcMotion等。這里需要執(zhí)行共享元素的Item,就是Image和Text,所以在B界面的XML中,需要指定對應(yīng)的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部分
下面的內(nèi)容和中間的文本,使用的是Content Transition,只需要針對這些元素,做相應(yīng)的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,都可以設(shè)置TransitionListener來監(jiān)聽其執(zhí)行過程,代碼如下所示。
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結(jié)束后,執(zhí)行其他的屬性動畫等等。
退出動畫
在B界面退出的時候,我這里使用了新的動畫效果,即設(shè)置了returnTransition,并非默認效果,而且這里有一點需要注意,那就是enterTransition時,是針對單獨的元素設(shè)置的,而returnTransition,則是分成了上下兩個部分進行動畫(主要是下部分),所以這里需要使用到前面提到的TransitionGroup的概念。在enterTransition的時候,TransitionGroup要設(shè)置為false,在returnTransition的時候,TransitionGroup要設(shè)置為true(因為ViewGroup只要設(shè)置了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"?/>
????????
????
組裝動畫
在分解完這些動畫后,就可以將整個過程串聯(lián)起來了,界面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動畫的一般開發(fā)過程,總結(jié)一下,主要就是下面幾個步驟。
拆解動畫:將過渡動畫拆分成Content Transition和Shared Element Transition 針對Content Transition,對每個元素編寫相應(yīng)的動畫 針對Shared Element Transition,確定好TransitionGroup后,指定兩個頁面之間的transitionName 組裝動畫:借助生命周期回調(diào)等狀態(tài),將動畫串聯(lián)起來
自定義Transition
https://developer.android.google.cn/training/transitions/custom-transitions
官網(wǎng)中其實已經(jīng)給我們提供了非常詳細的說明,同時,參考默認的Slide、Fade這些SDK默認Transition的實現(xiàn),我們可以很方便的自定義,下面就以一個改變background的Transition為例進行講解。
首先,需要繼承Transition,實現(xiàn)下面三個方法。
captureStartValues captureEndValues createAnimator
前面兩個方法基本都是設(shè)置需要自定義的屬性值,重要的是最后一個方法,創(chuàng)建屬性動畫。
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)
????????????}
????????}
????}
}
在前面兩個函數(shù)中,TransitionValues起到了一個容器的作用,保存了values和views,指定了需要作用的對象和值
使用和系統(tǒng)默認的Transition一樣,示例代碼如下所示。
TransitionManager.beginDelayedTransition(root,?ChangeBackgroundColorTransition())
textview.background?=?ColorDrawable(Color.parseColor("#bebebe"))
可以發(fā)現(xiàn),實際上Transition就是為不同的屬性創(chuàng)建屬性動畫而已,從自定義Transition就可以看出它的本質(zhì)。
開源庫
最后,推薦幾個自定義Transition開源庫。
https://github.com/HJ-Money/MTransition
https://github.com/ImmortalZ/TransitionHelper
https://github.com/lgvalle/Material-Animations
https://github.com/andkulikov/Transitions-Everywhere
預(yù)備役選手?
好了,終于到最后了,講了這么多Transition的使用方法,那么為什么我還叫他預(yù)備役選手呢?這就是因為,在Material Design Component中,對Motion進行了進一步的封裝,即:
Container transform Shared axis Fade through Fade
這樣四種封裝好的Motion,而Transition,則正是它們的基礎(chǔ)原理。
所以,Transition現(xiàn)在雖然用的不多,但是掌握了它的原理,才能更好的開啟MDC Motion之旅。
最后,介紹下我的網(wǎng)站:https://xuyisheng.top/? 點擊原文,一鍵直達
Flutter & Android 關(guān)注 《Android群英傳》
