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

1.前言

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

有了需求程序員就需要關注如何實施和落地,然而Android應用本身雖然有著完整的動畫框架支持,但是開發復雜、調試耗時,大型的gif或逐幀動畫對于CPU&內存占用都不太理想,所以許多Android的手機應用基本上不怎么有動畫。而且車載HMI上越來越多的開始引入各種光影、粒子效果,如果基于Android的原生控件來實現這些粒子效果,難度非常大,這就需要今天的主角Lottie來實現了。

2.Lottie概述

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

GitHub:https://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)用來移除指定的監聽?;蛘咭部梢允褂?code>removeAllUpdateListeners()移除所有監聽。
binding.animationView.addAnimatorUpdateListener(object : ValueAnimator.AnimatorUpdateListener{
    override fun onAnimationUpdate(animation: ValueAnimator?) {
        
    }
})
  • void addValueCallback(KeyPath keyPath, T property, LottieValueCallback<T> 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,分別設定了不同的文字。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:orientation="horizontal">

    <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"/>
</LinearLayout>

手勢交互效果


該效果展示了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自定義控件高級進階與精彩實例(博文視點出品)》(啟艦)【摘要 書評 試讀】- 京東圖書

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容