RE: 從零開始的車載Android HMI(一) - Lottie

語言: CN / TW / HK

1.前言

多年以前汽車還是以機械儀表主體的年代,各大汽車主機廠商並不十分關注作業系統UI的互動功能,但是隨著車載SOC算力的不斷提高以及主機廠商對汽車座艙競爭的白熱化。座艙的HMI在設計上在強調功能性的同時也開始關注UI的藝術性,HMI的設計師們期望藝術與功能應該協同工作,讓使用者沉浸在“第三空間”的體驗中。

有了需求程式設計師就需要關注如何實施和落地,然而Android應用本身雖然有著完整的動畫框架支援,但是開發複雜、除錯耗時,大型的gif或逐幀動畫對於CPU&記憶體佔用都不太理想,所以許多Android的手機應用基本上不怎麼有動畫。而且車載HMI上越來越多的開始引入各種光影、粒子效果,如果基於Android的原生控制元件來實現這些粒子效果,難度非常大,這就需要今天的主角Lottie來實現了。

2.Lottie概述

Lottie是一種基於JSON的動畫檔案格式,它使設計師能夠在任何平臺上釋出動畫,就像釋出靜態資產一樣簡單。它們是在任何裝置上工作的小檔案,可以在不進行畫素化的情況下放大或縮小。

GitHub:http://github.com/airbnb/lottie-android

官方文件:http://airbnb.io/lottie/

Lottie在車載HMI中的優勢

適量圖形,不會出現失真

佔用空間比序列幀動畫小

可以修改屬性,動態生成可互動的動畫(使用視訊動畫難以實現互動功能)

節省HMI的開發、除錯時間

可以更輕鬆的實現粒子、光影等特效

Lottie的使用方法

  1. 在build.gradle中新增依賴

dependencies { def lottieVersion = "5.2.0" implementation 'com.airbnb.android:lottie:$lottieVersion' }

  1. 使用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,讓動畫中的一小部分屬性發生改變。

  1. 修改區域性動畫的速度

binding.animationView.addValueCallback(KeyPath("LeftArmWave"), LottieProperty.TIME_REMAP) { frameInfo -> 2 * speed.toFloat() * frameInfo.overallProgress }

KeyPath中的LeftArmWave是Json中的一個屬性

修改的效果如下。注意看右手的擺動頻率X3後比X1高,以至於錄製的GIF直接丟幀了。

  1. 修改區域性動畫的顏色

``` 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] } ```

修改後的效果如下:

  1. 修改區域性動畫的運動範圍

``` 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

參考資料

還不知道什麼是汽車HMI設計?進來帶你快速瞭解

《Android自定義控制元件高階進階與精彩例項(博文視點出品)》(啟艦)【摘要 書評 試讀】- 京東圖書