如有侵权,请联系邮箱删除。
如果你了解新能源汽车,那你一定知道蔚来。而Nomi正是蔚来(NIO)汽车推出的车载 智能语音助手 / 人机交互系统,也被称为“车载人工智能伙伴”。它是当前智能座舱的一部分,被广泛用于提升用户在车内的智能交互体验。
Nomi 是全球首个有拟人形象的 AI 语音助手,如上,是Nomi的各种表情。
基于Nomi的形象,我使用Compose在Android平台开发了一套2D的 “类Nomi”形象。
相对于使用帧动画对于内存的占用,使用Canvas绘制表情能够不仅更流畅,而且能够避免表情之间硬切的割裂感。
核心结构
我们常常需要对“眨眼”“移动”“缩放”“旋转”等多种属性进行统一管理,并能动态驱动它们的平滑过渡。RobotStatus
就是一种 “全局状态容器”,它把与眼部和动作相关的所有可过渡属性(TransitionProperty
)集中到一个对象里,方便后续在 Compose 动画或其他渲染管线中统一取值和更新。
TransitionProperty
所有需要过渡的数值一律用此类型包装,简化参数一致性和复用逻辑。
@Serializable
data class TransitionProperty(
val initialValue: Float = 0F,
val targetValue: Float = 0F,
val duration: Int = 200,
val infinite: Boolean = false,
val repeatMode: RepeatMode = RepeatMode.Restart,
val centerPivotLevel: PivotLevel = PivotLevel.Center,
@Transient val easing: Easing = LinearEasing,
@Transient var animationSpec: AnimationSpec<Float>? = null
)
RobotStatus
动画驱动值一目了然,后端逻辑或 UI 只需要读取对应字段即可,无需分散在各处维护。
open class RobotStatus(
/**
* 左眼边缘角度
*/
val leftEyeCornerRadius: TransitionProperty = TransitionProperty(initialValue = 30F, targetValue = 30F),
/**
* 右眼边缘角度
*/
val rightEyeCornerRadius: TransitionProperty = TransitionProperty(initialValue = 30F, targetValue = 30F),
/**
* 左眼皮水平半径
*/
val leftEyeHorizontalRadius: TransitionProperty = TransitionProperty(initialValue = 30F, targetValue = 30F),
/**
* 右眼皮水平半径
*/
val rightEyeHorizontalRadius: TransitionProperty = TransitionProperty(initialValue = 30F, targetValue = 30F),
/**
* 左上眼皮垂直半径
*/
val leftTopEyeRadius: TransitionProperty = TransitionProperty(initialValue = 60F, targetValue = 60F),
/**
* 左下眼皮垂直半径
*/
val leftBottomEyeRadius: TransitionProperty = TransitionProperty(initialValue = 60F, targetValue = 60F),
/**
* 右上眼皮垂直半径
*/
val rightTopEyeRadius: TransitionProperty = TransitionProperty(initialValue = 60F, targetValue = 60F),
/**
* 右下眼皮垂直半径
*/
val rightBottomEyeRadius: TransitionProperty = TransitionProperty(initialValue = 60F, targetValue = 60F),
/**
* 眼部填充颜色
*/
val eyesFillColor: String = "#00000000",
/**
* 眼部旋转角度
*/
val eyesRotate: TransitionProperty = TransitionProperty(),
/**
* 眼部水平位移
*/
val eyesHorizontalTransition: TransitionProperty = TransitionProperty(),
/**
* 眼部垂直位移
*/
val eyesVerticalTransition: TransitionProperty = TransitionProperty(),
/**
* 眼部X方向缩放
*/
val eyesScaleX: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 眼部Y方向缩放
*/
val eyesScaleY: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 左眼旋转角度
*/
val leftEyeRotate: TransitionProperty = TransitionProperty(),
/**
* 左眼水平位移
*/
val leftEyeHorizontalTransition: TransitionProperty = TransitionProperty(),
/**
* 左眼垂直位移
*/
val leftEyeVerticalTransition: TransitionProperty = TransitionProperty(),
/**
* 左眼X方向缩放
*/
val leftEyeScaleX: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 左眼Y方向缩放
*/
val leftEyeScaleY: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 右眼旋转角度
*/
val rightEyeRotate: TransitionProperty = TransitionProperty(),
/**
* 右眼水平位移
*/
val rightEyeHorizontalTransition: TransitionProperty = TransitionProperty(),
/**
* 右眼垂直位移
*/
val rightEyeVerticalTransition: TransitionProperty = TransitionProperty(),
/**
* 右眼X方向缩放
*/
val rightEyeScaleX: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 右眼Y方向缩放
*/
val rightEyeScaleY: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 动作标识符
*/
val actionSample: ActionSample = ActionSample(),
/**
* 动作标识符自身的旋转角度(center)
*/
val actionSampleRotate: TransitionProperty = TransitionProperty(),
/**
* 动作标识符的旋转角度
*/
val actionRotate: TransitionProperty = TransitionProperty(),
/**
* 动作标识符自身的缩放,不建议设置为可变值
*/
val actionScale: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 动作画布X方向的缩放
*/
val actionScaleX: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 动作画布Y方向的缩放
*/
val actionScaleY: TransitionProperty = TransitionProperty(initialValue = 1F, targetValue = 1F),
/**
* 动作画布水平位移
*/
val actionHorizontalTransition: TransitionProperty = TransitionProperty(),
/**
* 动作画布垂直位移
*/
val actionVerticalTransition: TransitionProperty = TransitionProperty()
)
示例
使用抽象后的数据类,可以通过序列化string,完成动作的下发,从而远程控制形象的变化,如 Focus 的表情,可以通过配置如下字符串并在本地序列化完成形象演示。
{
"leftEyeCornerRadius": {
"targetValue": 50.0
},
"rightEyeCornerRadius": {
"targetValue": 50.0
},
"leftEyeHorizontalRadius": {
"targetValue": 50.0
},
"rightEyeHorizontalRadius": {
"targetValue": 50.0
},
"leftTopEyeRadius": {
"targetValue": 10.0
},
"leftBottomEyeRadius": {
"targetValue": 50.0
},
"rightTopEyeRadius": {
"targetValue": 10.0
},
"rightBottomEyeRadius": {
"targetValue": 50.0
},
"eyesFillColor": "#FF0000",
"eyesVerticalTransition": {
"targetValue": 10.0,
"duration": 600,
"infinite": true,
"repeatMode": "Reverse"
},
"leftEyeRotate": {
"targetValue": 15.0,
"duration": 300
},
"rightEyeRotate": {
"targetValue": -15.0,
"duration": 300
},
"actionSample": {
"sample": "🫵"
},
"actionHorizontalTransition": {
"targetValue": -80.0
},
"actionVerticalTransition": {
"initialValue": 90.0,
"targetValue": 100.0,
"duration": 600,
"infinite": true,
"repeatMode": "Reverse"
}
}
DrawEyes
绘制眼部
使用Compose原生Canvas,绘制左右眼。眼部整体和单个眼睛支持旋转、平移和缩放。
translate(left = eyesHorizontalTransition, top = eyesVerticalTransition) {
rotate(
degrees = eyesRotatedAngle,
pivot = eyesRotate.centerPivotLevel.calculateRotateCenterPivot(center, size.width / 2)
) {
scale(
scaleX = eyesScaleX,
scaleY = eyesScaleY,
pivot = eyesCenterPivot
) {
drawEye(
center = leftEyeCenter,
horizontalRadius = leftEyeHorizontalRadius,
upperEyelidRadius = leftUpperEyelidRadius,
lowerEyelidRadius = leftLowerEyelidRadius,
cornerRadius = leftEyeCornerRadius,
horizontalTranslation = leftEyeHorizontalTransition,
verticalTranslation = leftEyeVerticalTransition,
rotateAngle = leftEyeRotatedAngle,
scaleX = leftEyeScaleX,
scaleY = leftEyeScaleY,
fillColor = hexStringToColor(eyesFillColor)
)
drawEye(
center = rightEyeCenter,
horizontalRadius = rightEyeHorizontalRadius,
upperEyelidRadius = rightUpperEyelidRadius,
lowerEyelidRadius = rightLowerEyelidRadius,
cornerRadius = rightEyeCornerRadius,
horizontalTranslation = rightEyeHorizontalTransition,
verticalTranslation = rightEyeVerticalTransition,
rotateAngle = rightEyeRotatedAngle,
scaleX = rightEyeScaleX,
scaleY = rightEyeScaleY,
fillColor = hexStringToColor(eyesFillColor)
)
}
}
}
DrawAction
绘制手部动作
简化开发,渲染标准unicode并通过位移、旋转和缩放展示动作。
Canvas(
modifier = modifier
) {
val textStyle = TextStyle(
fontSize = 40.sp * scale,
color = Color.White,
fontFamily = FontFamily.Default,
fontWeight = FontWeight.ExtraBold,
)
val textLayoutResult = textMeasurer.measure(
text = actionSample.sample,
style = textStyle
)
translate(left = horizontalTransition, top = verticalTransition) {
/**
* action画布旋转角度
*/
rotate(
degrees = rotate,
pivot = actionRotate.centerPivotLevel.calculateRotateCenterPivot(center, size.width / 2)
) {
/**
* Sample自身旋转角度
*/
rotate(
degrees = sampleRotate,
pivot = center
) {
scale(scaleX, scaleY) {
drawText(
textLayoutResult = textLayoutResult,
topLeft = Offset(
center.x - (textLayoutResult.size.width) / 2,
center.y - (textLayoutResult.size.height) / 2
),
drawStyle = Stroke(width = 3F)
)
}
}
}
}
}
预设表情
基础表情
Ordinary
: 普通表情Blink
: 眨眼Sadness
: 伤心Happiness
: 开心Angry
: 生气
特殊表情
Music
: 音乐表情Rainy
: 下雨表情Sunny
: 晴天表情Cloudy
: 多云表情SparkLight
: 闪光表情Coffee
: 咖啡表情SunGlasses
: 墨镜表情TakePhoto
: 拍照表情Focus
: 专注表情