Android 动画车载AndroidAndroid 车载

RE: 从零开始的车载Android HMI(一) - Lott

2022-07-03  本文已影响0人  林栩link

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都是支持的,这部分就不再介绍了。

app:lottie_fileName="HamburgerArrow.json"

如果设定 app:lottie_fileName="other/HamburgerArrow.json",那么lottie就会读取assets/other/HamburgerArrow.json。
void setAnimationFromJson(String jsonString, @Nullable String cacheKey)

app:lottie_rawRes="@raw/name"

void setCacheComposition(boolean cacheComposition)

binding.animationView.addAnimatorUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
    override fun onAnimationUpdate(animation: ValueAnimator?) {

    }
})
binding.animationView.addAnimatorPauseListener(object : Animator.AnimatorPauseListener{
    override fun onAnimationPause(animation: Animator?) {
        
    }

    override fun onAnimationResume(animation: Animator?) {
        
    }
    
})
binding.animationView.addAnimatorUpdateListener(object : ValueAnimator.AnimatorUpdateListener{
    override fun onAnimationUpdate(animation: ValueAnimator?) {
        
    }
})
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自定义控件高级进阶与精彩实例(博文视点出品)》(启舰)【摘要 书评 试读】- 京东图书

上一篇 下一篇

猜你喜欢

热点阅读