什么是給 layout 變化加上動(dòng)畫?
其實(shí)很好理解,就是在 layout,也就是我們說的布局內(nèi)容發(fā)生變化的時(shí)候,給它添加上一個(gè)過渡的動(dòng)畫,使其看起來更顯得自然一些。
其實(shí) Android 自帶的 framework 已經(jīng)為我們提供了這么一個(gè)能力,它可以在兩個(gè) layout 變化的時(shí)候加上動(dòng)畫,使 layout 的變化更加自然。這么好的功能為什么不用呢?而且實(shí)際上,它的使用也并不復(fù)雜,只需短短的幾行代碼,就可以提升用戶的操作體驗(yàn),何樂而不為。
about transition framework
- Group-level animations: Apply one or more animation effects to all of the views in a view hierarchy.
- Built-in animations: Use predefined animations for common effects such as fade out or movement.
- Resource file support: Load view hierarchies and built-in animations from layout resource files.
- Lifecycle callbacks: Receive callbacks that provide control over the animation and hierarchy change process.
根據(jù)官網(wǎng)文檔所示,transition framework 有幾個(gè)重要的特性:
- 組級別動(dòng)畫:支持運(yùn)用一個(gè)或多個(gè)動(dòng)畫到視圖層級上。
- 內(nèi)建動(dòng)畫:framework 已經(jīng)為我們提供了很多的內(nèi)置動(dòng)畫,比如你可以使用淡入淡入、位移動(dòng)畫等。
- 支持資源文件:構(gòu)建場景 scene 時(shí),支持資源文件直接導(dǎo)入,當(dāng)然,同時(shí)它也是支持 view 導(dǎo)入的。
- 生命周期回調(diào):可以監(jiān)聽到動(dòng)畫執(zhí)行的聲明周期,提供 start、resume、pause 等。
那么,transition framework 是怎么如何實(shí)現(xiàn) layout 變化動(dòng)畫效果的?layout、scene、transition 他們之間的關(guān)系又是什么樣的?我們看一張官方貼圖。

首先,我們將起始 layout 以及結(jié)束 layout 轉(zhuǎn)化成相對應(yīng)的場景。
然后,創(chuàng)建想要執(zhí)行的 transition。
最后,通過 TransitionManager 將場景和 transition 綁定在一起執(zhí)行。
這樣三步,就可以實(shí)現(xiàn)從場景一到場景二的動(dòng)畫轉(zhuǎn)變了。
如何給 layout 變化加上動(dòng)畫

今天就以這個(gè)為例子說一下如何給 layout 變化加上平滑的動(dòng)畫。
從圖中可以看到,點(diǎn)擊機(jī)器人的時(shí)候,layout 改變了,機(jī)器人變大,并且名字也移動(dòng)到右邊,同時(shí),詳情介紹也出現(xiàn)了。返回的時(shí)候,layout 中的元素也平滑的回到之前的狀態(tài)。這就是用 scene 和 transition 實(shí)現(xiàn)的。
1. 創(chuàng)建場景 scene
- 創(chuàng)建 layouts
layout_first.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_container"
android:layout_width="wrap_content" android:layout_height="wrap_content">
<ImageView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:src="@mipmap/ic_launcher"
android:id="@+id/image_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
app:layout_constraintLeft_toRightOf="@id/image_icon"
app:layout_constraintTop_toTopOf="@id/image_icon"
app:layout_constraintBottom_toBottomOf="@id/image_icon"
android:layout_marginLeft="16dp"
android:textSize="20sp"
android:id="@+id/text_name"
android:text="頭像"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</android.support.constraint.ConstraintLayout>
layout_second.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_marginTop="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:minHeight="200dp"
android:minWidth="200dp"
android:src="@mipmap/ic_launcher"
android:id="@+id/image_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
app:layout_constraintLeft_toLeftOf="@id/image_icon"
app:layout_constraintRight_toRightOf="@id/image_icon"
app:layout_constraintTop_toBottomOf="@id/image_icon"
android:layout_marginTop="16dp"
android:textSize="28sp"
android:id="@+id/text_name"
android:text="頭像"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/text_detail"
android:layout_marginTop="20dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_name"
android:layout_width="wrap_content"
android:textSize="18sp"
android:padding="16dp"
android:text="這是詳情這是詳情這是詳情這是詳情
這是詳情這是詳情這是詳情這是詳情這是詳情這是詳情
這是詳情這是詳情這是詳情這是詳情這是詳情這是詳情
這是詳情這是詳情這是詳情這是詳情這是詳情這是詳情
這是詳情這是詳情這是詳情"
android:layout_height="wrap_content"/>
</android.support.constraint.ConstraintLayout>
注意:這里兩個(gè)場景 layout 的相應(yīng)控件的 id 需要設(shè)置成一樣的。
activity.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LayoutWithSActivity">
<FrameLayout
android:id="@+id/layout_container"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="0dp">
</FrameLayout>
</android.support.constraint.ConstraintLayout>
- 根據(jù) layout 生成相應(yīng)的 scene
生成 scene 的方式有兩種,可以通過 view 生成一個(gè)場景 scene,也可以直接加載布局文件生成 scene。
通過布局文件直接生成 scene
val sceneFirst = Scene.getSceneForLayout(layout_container, R.layout.scene_first, this)
val sceneSecond = Scene.getSceneForLayout(layout_container, R.layout.scene_second, this)
通過 view 直接生成 scene
val firstView = layoutInflater.inflate(R.layout.scene_first, null)
val secondView = layoutInflater.inflate(R.layout.scene_second, null)
val sceneFirst = Scene(layout_container, firstView)
val sceneSecond = Scene(layout_container, secondView)
第一種方式方便,直接加載布局文件就可以了,但是不易控制里面的元素。拿本例來說,因?yàn)橐刂莆淖值母淖?,所以采取第二種通過 view 的方式來生成。
2. 創(chuàng)建 transition
創(chuàng)建 transition 有兩種方式,可以在 xml 中定義,也可以只直接在代碼中定義。
- 在 xml 中定義 transition
在 res/transition 下定義 transition.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<fade/>
<slide android:slideEdge="right"/>
</transitionSet>
transitionSet 是一個(gè)組合,可以同時(shí)定義多個(gè) transition
然后再代碼中加載:
val inflateTransition = TransitionInflater.from(this).inflateTransition(R.transition.layout_transform)
- 在代碼中直接生成
al transition = ChangeBounds()
內(nèi)置的 transition 大概有這么多個(gè):

AutoTransition 是默認(rèn)的transition,F(xiàn)ade out,move and resize,and fade in views。
ChangeBounds 改變邊界,栗子中我們用的就是這種。
其他的大家自己可以去查看下源碼。
2. 應(yīng)用 transition
應(yīng)用 transition 非常的簡單
TransitionManager.go(scene, transition)
在栗子中,點(diǎn)擊機(jī)器人就會(huì)轉(zhuǎn)變到場景 2,并且文字也發(fā)生變化,那么我們可以這么寫
firstView.setOnClickListener {
secondView.findViewById<TextView>(R.id.text_name).text = "從前置過來"
TransitionManager.go(sceneSecond, transition)
}
One More Thing
事實(shí)上,還有一種,不需要借助 scene 來實(shí)現(xiàn)的變化,那就是通過TransitionManager.beginDelayedTransition()。在執(zhí)行TransitionManager.beginDelayedTransition()之后,系統(tǒng)會(huì)保存一個(gè)當(dāng)前視圖樹狀態(tài)的場景,之后當(dāng)我們改變了View的屬性之后(比如重新設(shè)置了View位置、縮放、clipe等等)。在下一次繪制時(shí),系統(tǒng)會(huì)自動(dòng)對比之前保存的視圖樹,然后執(zhí)行相應(yīng)動(dòng)畫。

實(shí)現(xiàn)的代碼很簡單
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_transiton_without_s)
val inflateTransition = TransitionInflater.from(this).inflateTransition(R.transition.layout_transform)
btn_add.setOnClickListener {
val text = TextView(this).apply {
text = "I am new"
}
TransitionManager.beginDelayedTransition(layout_items_container, Fade())
layout_items_container.addView(text)
}
btn_remove.setOnClickListener {
TransitionManager.beginDelayedTransition(layout_items_container, inflateTransition)
if (layout_items_container.childCount == 0) return@setOnClickListener
layout_items_container.removeViewAt(0)
}
}
寫在最后
文中若有表述不恰當(dāng)?shù)牡胤剑埡侠碇赋?,共勉?/p>