RE: 從零開始的車載Android HMI(一) - Lottie
1.前言
多年以前汽車還是以機械儀表主體的年代,各大汽車主機廠商並不十分關注作業系統UI的互動功能,但是隨著車載SOC算力的不斷提高以及主機廠商對汽車座艙競爭的白熱化。座艙的HMI在設計上在強調功能性的同時也開始關注UI的藝術性,HMI的設計師們期望藝術與功能應該協同工作,讓使用者沉浸在“第三空間”的體驗中。
有了需求程式設計師就需要關注如何實施和落地,然而Android應用本身雖然有著完整的動畫框架支援,但是開發複雜、除錯耗時,大型的gif或逐幀動畫對於CPU&記憶體佔用都不太理想,所以許多Android的手機應用基本上不怎麼有動畫。而且車載HMI上越來越多的開始引入各種光影、粒子效果,如果基於Android的原生控制元件來實現這些粒子效果,難度非常大,這就需要今天的主角Lottie來實現了。
2.Lottie概述
Lottie是一種基於JSON的動畫檔案格式,它使設計師能夠在任何平臺上釋出動畫,就像釋出靜態資產一樣簡單。它們是在任何裝置上工作的小檔案,可以在不進行畫素化的情況下放大或縮小。
Lottie在車載HMI中的優勢
適量圖形,不會出現失真
佔用空間比序列幀動畫小
可以修改屬性,動態生成可互動的動畫(使用視訊動畫難以實現互動功能)
節省HMI的開發、除錯時間
可以更輕鬆的實現粒子、光影等特效
Lottie的使用方法
- 在build.gradle中新增依賴
dependencies {
def lottieVersion = "5.2.0"
implementation 'com.airbnb.android:lottie:$lottieVersion'
}
- 使用LottieAnimationView
首先將lottie動畫的json檔案放在assets資料夾下
然後就可以在佈局檔案中使用LottieAnimationView了
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/dynamic_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="HamburgerArrow.json"
app:lottie_autoPlay="true"
app:lottie_loop="true"/>
然後執行APP就可以看到動畫效果
3.Lottie的常用屬性&API
LottieAnimationView
繼承自AppCompatImageView,所以ImageView支援的屬性,LottieAnimationView
都是支援的,這部分就不再介紹了。
- lottie_fileName
設定lottie動畫所對應的json檔案地址。json檔案預設需要放置在assets下,設定時不需要再強調assets
app:lottie_fileName="HamburgerArrow.json"
如果設定 app:lottie_fileName="other/HamburgerArrow.json",那麼lottie就會讀取assets/other/HamburgerArrow.json。
void setAnimationFromJson(String jsonString, @Nullable String cacheKey)
- lottie_rawRes
設定lottie動畫的json檔案地址。json檔案除了可以放置assets資料夾下,還可以放在raw資料夾下。使用時需要注意,利用lottie_rawRes引入資源時,json檔名前需要加上@raw,並且檔名不帶.json字尾。
app:lottie_rawRes="@raw/name"
\
- lottie_autoPlay
設定是否自動播放,取值為true | false
- lottie_loop
設定是否迴圈播放,取值為true | false
- lottie_url
當需要載入線上資源時,就可以使用lottie_url
void setAnimationFromUrl(String url)
void setAnimationFromUrl(String url, @Nullable String cacheKey)
- lottie_fallbackRes
設定一個drawable,如果lotticomposition由於任何原因未能載入,則將呈現該drawable。
如果這是網路動畫,可以使用它向用戶顯示錯誤,也可以新增一個失敗的監聽器重試下載。
void setFallbackResource(@DrawableRes int fallbackResource)
- lottie_repeatMode
設定迴圈播放的順序。取值為restart | reverse 。restart表示正常迴圈播放,reverse表示倒序播放
void setRepeatMode(@LottieDrawable.RepeatMode int mode)
int getRepeatMode()
- lottie_repeatCount
設定迴圈播放次數,取值為整數型別。
void setRepeatCount(int count)
int getRepeatCount()
- lottie_imageAssetsFolder
設定圖片檔案在assets資料夾下的訪問路徑。有的時候使用AE匯出lottie的json時也會匯出一些圖片,這時候就需要該屬性設定圖片的地址。
void setImageAssetsFolder(String imageAssetsFolder)
String getImageAssetsFolder()
- void setFrame(int frame)
將進度設定為指定的幀。將進度設定為指定的幀。如果尚未設定合成,則進度將在設定時設定為幀。
通過int getFrame()
可以獲取當前渲染的幀。
- void setMaxFrame(int endFrame)
設定播放或迴圈時動畫將結束的最大幀。
該值將被鉗制到合成邊界。例如,設定整數最大值將產生與合成相同的結果。
通過float getMaxFrame()
可以獲取當前設定的最大幀
- void setMinFrame(int startFrame)
設定播放或迴圈時動畫開始的最小幀。
設定最大、最小幀可以只播放lottie動畫中的一部分,例如下面的兩張圖,第一張是完整的從0播放到183幀,第二張則是從60播放到100幀。
- lottie_progress
設定動畫初次顯示時的進度,型別為float。取值範圍0.0 ~ 1.0
void setProgress(@FloatRange(from = 0f, to = 1f) float progress)
float getProgress()
- lottie_speed
設定播放速度,取值型別為float。當速度<1時,動畫會慢放,當速度<0時,可以實現倒序播放。
void setSpeed(float speed)
float getSpeed()
void reverseAnimationSpeed()
:反轉當前動畫速度。這不會播放動畫。
速度是一個比較重要的屬性,與progress、frame等屬性一起靈活運用,我們就可以輕鬆地在HMI上實現炫酷而複雜的儀表盤效果,這對車載HMI尤為重要。
- lottie_enableMergePathsForKitKatAndAbove
設定是否開啟MergePath屬性,取值為true | false。預設為false
void enableMergePathsForKitKatAndAbove(boolean enable)
boolean isMergePathsEnabledForKitKatAndAbove()
- void playAnimation()
從頭開始播放動畫。如果速度<0,它將從終點開始,並向起點播放。必須在主執行緒中呼叫。
- void cancelAnimation()
取消動畫,必須在主執行緒中呼叫。
- void pauseAnimation()
暫停動畫,必須在主執行緒中呼叫。
- void resumeAnimation()
從當前位置繼續播放動畫。如果速度<0,它將從當前位置向後播放。必須在主執行緒中呼叫。
- long getDuration()
獲取動畫的播放時長。
- void setTextDelegate(TextDelegate textDelegate)
設定此選項可在執行時用自定義文字替換動畫文字
- lottie_cacheComposition
設定是否開啟快取,取值 true | false,預設開啟。開啟快取可以提升動畫的載入效率。
void setCacheComposition(boolean cacheComposition)
- lottie_ignoreDisabledSystemAnimations
允許忽略系統動畫設定,因此即使禁用動畫,也允許執行動畫。取值 true | false,預設為false。
void setIgnoreDisabledSystemAnimations(boolean ignore)
- lottie_clipToCompositionBounds
設定lottie是否應剪輯到原始動畫合成邊界。設定為true時,父檢視可能需要禁用clipChildren,以便Lottie可以在LottieAnimationView邊界之外進行渲染。預設為true。
void setClipToCompositionBounds(boolean clipToCompositionBounds)
- lottie_renderMode
設定渲染模式,取值為 automatic | hardware | software。設定渲染模式為hardware時,可以顯著提升動畫的渲染效率,但是有些系統函式可能並不支援硬體加速,實際使用時需要結合除錯時的效果選擇是否開啟。
void setRenderMode(RenderMode renderMode)
RenderMode getRenderMode()
- void addAnimatorListener(Animator.AnimatorListener listener)
新增動畫的屬性監聽。
對應也提供了removeUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener)
用來移除指定的監聽。或者也可以使用removeAllAnimatorListeners()
移除所有監聽。
``` binding.animationView.addAnimatorUpdateListener(object : ValueAnimator.AnimatorUpdateListener { override fun onAnimationUpdate(animation: ValueAnimator?) {
}
}) ```
- void addAnimatorPauseListener(Animator.AnimatorPauseListener listener)
新增動畫暫停/恢復監聽。
對應也提供了removeAnimatorPauseListener(Animator.AnimatorPauseListener listener)
用來移除指定的監聽。
``` binding.animationView.addAnimatorPauseListener(object : Animator.AnimatorPauseListener{ override fun onAnimationPause(animation: Animator?) {
}
override fun onAnimationResume(animation: Animator?) {
}
}) ```
- void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener)
新增動畫發生更新時的監聽
對應也提供了removeUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener)
用來移除指定的監聽。或者也可以使用removeAllUpdateListeners()
移除所有監聽。
``` binding.animationView.addAnimatorUpdateListener(object : ValueAnimator.AnimatorUpdateListener{ override fun onAnimationUpdate(animation: ValueAnimator?) {
}
}) ```
- void addValueCallback(KeyPath keyPath, T property, LottieValueCallback
callback)
監聽lottie動畫json中某個片段的屬性。
此keypath
可以解析為多個內容,在這種情況下,回撥的值將應用於所有回撥。在內部會首先檢查是否已使用resolveKeyPath(KeyPath)解析keypath,如果尚未解析,則將對其進行解析。
Lottie動畫的Json中屬性都是英文簡寫,我們很難把json中key與實際的屬性對應起來,所以有了第二個引數LottieProperty
,它的內部定義了大量的屬性,當我們需要修改json時,只需要傳入LottieProperty
中屬性即可。
例如,需要監聽json中LeftArmWave的持續時間,就可以這麼寫
``` animationView.addValueCallback(KeyPath("LeftArmWave"), LottieProperty.TIME_REMAP) { frameInfo ->
} ```
4.Lottie的常見用法
Lottie的Demo中內建了很多官方自己開發的動畫效果,目的是為我們展示Lottie的常見用法,作為開發者我們必須掌握,並在適當的時候運用到我們的應用中。
動態屬性效果
該效果展示了lottie支援動態修改json,讓動畫中的一小部分屬性發生改變。
- 修改區域性動畫的速度
binding.animationView.addValueCallback(KeyPath("LeftArmWave"), LottieProperty.TIME_REMAP) { frameInfo ->
2 * speed.toFloat() * frameInfo.overallProgress
}
KeyPath
中的LeftArmWave是Json中的一個屬性
修改的效果如下。注意看右手的擺動頻率X3後比X1高,以至於錄製的GIF直接丟幀了。
- 修改區域性動畫的顏色
``` val shirt = KeyPath("Shirt", "Group 5", "Fill 1") val leftArm = KeyPath("LeftArmWave", "LeftArm", "Group 6", "Fill 1") val rightArm = KeyPath("RightArm", "Group 6", "Fill 1")
binding.animationView.addValueCallback(shirt, LottieProperty.COLOR) { COLORS[colorIndex] } binding.animationView.addValueCallback(leftArm, LottieProperty.COLOR) { COLORS[colorIndex] } binding.animationView.addValueCallback(rightArm, LottieProperty.COLOR) { COLORS[colorIndex] } ```
修改後的效果如下:
- 修改區域性動畫的運動範圍
``` val point = PointF() binding.animationView.addValueCallback( KeyPath("Body"), LottieProperty.TRANSFORM_POSITION ) { frameInfo -> val startX = frameInfo.startValue.x var startY = frameInfo.startValue.y var endY = frameInfo.endValue.y
if (startY > endY) {
startY += EXTRA_JUMP[extraJumpIndex]
} else if (endY > startY) {
endY += EXTRA_JUMP[extraJumpIndex]
}
point.set(startX, lerp(startY, endY, frameInfo.interpolatedKeyframeProgress))
point
} ```
修改後的效果如下
動畫文字效果
該效果展示了動畫文字效果。這個效果實現起來其實不難,從程式中捕獲輸入的字母,再替換成lottie的資原始檔即可。
val letter = "" + Character.toUpperCase(event.unicodeChar.toChar())
val fileName = "Mobilo/$letter.json"
LottieCompositionFactory.fromAsset(context, fileName)
.addListener { addComposition(it) }
動態文字效果
該效果展示動態替換動畫中的文字。使用setTextDelegate
就可以在動畫執行中修改lottie動畫中的文字
``` val textDelegate = TextDelegate(binding.dynamicTextView) binding.nameEditText.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable?) { textDelegate.setText("NAME", s.toString()) }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}) binding.dynamicTextView.setTextDelegate(textDelegate) ```
注意,這裡其實用了兩個lottieView,分別設定了不同的文字。
```
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/originalTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="16dp"
app:lottie_rawRes="@raw/name"
app:lottie_autoPlay="true"
app:lottie_loop="true"/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/dynamicTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_rawRes="@raw/name"
app:lottie_autoPlay="true"
app:lottie_loop="true"/>
```
手勢互動效果
該效果展示了Lottie的手勢互動。其實和第一個效果實現思路相同,都是通過addValueCallback
修改json中的屬性來實現的。
``` override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
val largeValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
binding.animationView.addValueCallback(KeyPath("First"), LottieProperty.TRANSFORM_POSITION, largeValueCallback)
val mediumValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
binding.animationView.addValueCallback(KeyPath("Fourth"), LottieProperty.TRANSFORM_POSITION, mediumValueCallback)
val smallValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
binding.animationView.addValueCallback(KeyPath("Seventh"), LottieProperty.TRANSFORM_POSITION, smallValueCallback)
var totalDx = 0f
var totalDy = 0f
val viewDragHelper = ViewDragHelper.create(binding.containerView, object : ViewDragHelper.Callback() {
override fun tryCaptureView(child: View, pointerId: Int) = child == binding.targetView
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
return top
}
override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
return left
}
override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
totalDx += dx
totalDy += dy
smallValueCallback.setValue(getPoint(totalDx, totalDy, 1.2f))
mediumValueCallback.setValue(getPoint(totalDx, totalDy, 1f))
largeValueCallback.setValue(getPoint(totalDx, totalDy, 0.75f))
}
})
binding.containerView.viewDragHelper = viewDragHelper
} ```
在RecyclerView中使用
該效果展示通過監聽點選事件來播放不同的lottie動畫。這個效果最常見,APP中的點贊效果大多都是這樣的實現思路。
5.總結
在車載HMI開發中往往我們會在實現、除錯UI上花費大量的時間,如果能夠靈活的運用Lottie,就可以顯著節省程式的開發時間。例如,光影、粒子等特效雖然可以也考慮用Kanzi等3D引擎實現,但是3D引擎會消耗成倍的SOC效能,實際開發過程中,簡單的特效使用Lottie實現,可以極大的優化應用的效能,給使用者一個更優秀的體驗。
當然這一切的前提是,UI設計師願意為程式設計師切出一套Lottie的動畫(F**K!)
本篇很多內容參考了《Android自定義控制元件高階進階與精彩例項(博文視點出品)》(啟艦)【摘要 書評 試讀】- 京東圖書 這本書的內容,寫得相當不錯,非常值得認真閱讀。
下一篇來講講車載HMI開發時都會用到的一個系統元件 - Widget
參考資料
- Android車載應用開發與分析(1) - Android Automotive概述與編譯
- 【Android R】車載 Android 核心服務 - CarService 解析
- 【Android R】車載 Android 核心服務 - CarPropertyService
- 車載Android程式設計師的2022年終總結與轉行建議
- 從應用工程師的角度再談車載 Android 系統
- Android 車載應用開發與分析(12) - SystemUI (一)
- RE: 從零開始的車載Android HMI(三) - SurfaceView
- RE: 從零開始的車載Android HMI(二) - Widget
- RE: 從零開始的車載Android HMI(一) - Lottie
- Android 車載應用開發與分析 (3)- 構建 MVVM 架構(Java版)
- Android 車載應用開發與分析 (4)- 編寫基於AIDL 的 SDK