在汽车控制类应用中,灯光距离调节是一个常见的交互场景 —— 用户通过调节滑块或按钮,实时看到前灯光照范围的变化,直观反馈操作效果。本文将基于实际项目代码,详解如何通过自定义 View 实现这一效果,包括光照区域绘制、平滑动画过渡及性能优化等核心技术点。
一、功能需求与效果展示
需求分析
汽车前灯的光照范围调节需要实现:
- 显示前灯的光照区域(含渐变效果,模拟真实灯光衰减)
- 支持 5 种光照距离(从近到远,光照范围逐渐扩大)
- 切换距离时,光照边缘需平滑动画过渡
- 光照区域需有边框线,增强视觉边界感
最终效果
光照区域随调节等级变化,从近到远逐渐扩大,切换时有流畅的动画,光照颜色从光源处向外逐渐变淡(模拟真实灯光衰减)。
二、核心实现:自定义 CarHeadLightView
自定义 View 是实现这一效果的核心,需处理形状绘制、渐变效果、属性动画三大核心问题。
2.1 基础结构与初始化
自定义 View 的基础结构包括属性定义、画笔初始化、关键数据准备(如光照区域的顶点坐标):
// 光照区域画笔(填充渐变)
private val mLightPaint by lazyPaint {
style = Paint.Style.FILL
isAntiAlias = true // 抗锯齿,避免边缘毛刺
}
// 边框画笔(边框渐变)
private val mBorderLinePaint by lazyPaint {
style = Paint.Style.STROKE
strokeWidth = 2f // 边框宽度
isAntiAlias = true
}
// 光照区域路径(核心:定义光照的形状)
private val mPath by lazy { Path() }
// 光照区域的顶点集合(动态更新,决定光照范围)
private var mCurrentPoints = mutableListOf<PointF>()
// 存储5种光照距离对应的顶点坐标(key:等级0-4,value:顶点集合)
private val mAllPointsArray: SparseArray<List<PointF>> = SparseArray<List<PointF>>().apply {
put(0, listOf(PointF(1329.6f, 464f), PointF(837.2f, 464f))) // 最近距离
put(1, listOf(PointF(1314.6f, 464f), PointF(765.3f, 464f)))
put(2, listOf(PointF(1301.4f, 464f), PointF(678.3f, 464f)))
put(3, listOf(PointF(1279.2f, 464f), PointF(518.4f, 464f)))
put(4, listOf(PointF(1258.8f, 464f), PointF(333.9f, 464f))) // 最远距离
}
// 固定的光源起点(前灯位置,不随距离变化)
private val initPoints = listOf(PointF(1228.2f, 226.4f), PointF(1395.4f, 283.4f))
init {
// 开启硬件加速,提升绘制性能
setLayerType(LAYER_TYPE_HARDWARE, null)
// 初始化光照区域的弧形路径(仅计算一次,缓存结果)
mArcPathParams = buildArcPath()
}
// 懒加载画笔的工具方法
private inline fun lazyPaint(crossinline block: Paint.() -> Unit): Lazy<Paint> =
lazy { Paint().apply(block) }
- 光照区域由多个顶点定义,其中initPoints是固定的光源位置(前灯所在点),mAllPointsArray存储不同距离对应的终点(光照最远点),通过组合这些点形成完整的光照范围。
- 使用lazy初始化画笔,避免重复创建;通过setLayerType(LAYER_TYPE_HARDWARE)开启硬件加速,适合频繁绘制的场景。
三、光照区域绘制:Path 与渐变效果
光照区域的视觉效果是核心,需要通过Path构建不规则形状,并结合渐变 shader 模拟灯光衰减。
3.1 构建光照区域的路径(Path)
汽车前灯的光照区域通常是 “扇形” 或 “类梯形”,由固定的光源点和可变的终点连接而成,且顶部边缘为弧形(模拟灯光的扩散效果)。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (mCurrentPoints.size < 4) { // 至少需要4个点(2个光源点+2个终点)
return
}
drawLightArea(canvas) // 绘制光照填充区域
drawBorderLine(canvas) // 绘制边框线
}
/** 绘制光照填充区域 */
private fun drawLightArea(canvas: Canvas) {
mPath.apply {
reset()
// 从第一个光源点开始
moveTo(mCurrentPoints[0].x, mCurrentPoints[0].y)
// 绘制顶部弧形边缘(连接两个光源点的弧形)
val (startAngle, sweepAngle) = mArcPathParams?.first ?: return
val rect = mArcPathParams?.second ?: return
arcTo(rect, startAngle, sweepAngle)
// 连接到第一个终点,再连接到第二个终点,最后闭合路径
lineTo(mCurrentPoints[2].x, mCurrentPoints[2].y)
lineTo(mCurrentPoints[3].x, mCurrentPoints[3].y)
close() // 闭合路径(自动连接回起点)
}
// 用带渐变的画笔绘制路径
canvas.drawPath(mPath, mLightPaint)
}
/** 计算弧形路径的参数(圆心、半径、角度) */
private fun buildArcPath(): Pair<FloatArray, RectF>? {
// 基于固定光源点计算弧形的圆心、半径、起始角度
val (centerX, centerY, radius) = calculateArcParams() ?: return null
val startAngle = calculateStartAngle(centerX, centerY) // 起始角度
val sweepAngle = calculateSweepAngle(centerX, centerY) // 扫过的角度
// 弧形所在的矩形(用于arcTo方法)
val rect = RectF(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
)
return Pair(floatArrayOf(startAngle, sweepAngle), rect)
}
/** 计算弧形的圆心和半径(基于3个点:2个光源点+1个中间点) */
private fun calculateCenterAndRadius(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float): FloatArray {
// 基于三点求圆的数学公式(计算圆心(xc,yc)和半径)
val ma = (y2 - y1) / (x2 - x1) // 第一条线的斜率
val mb = (y3 - y2) / (x3 - x2) // 第二条线的斜率
// 计算圆心x坐标
val xc = (ma * mb * (y1 - y3) + mb * (x1 + x2) - ma * (x2 + x3)) / (2 * (mb - ma))
// 计算圆心y坐标
val yc = -1 * (xc - (x1 + x2)