扩展函数与高阶函数在自定义View中的应用
自定义 View 是 Android 开发中绕不开的话题。说实话,早期我做自定义 View 时,代码总是又臭又长——Canvas 操作一堆重复代码,Paint 配置散落在各处,触摸事件处理更是让人头疼。直到我把扩展函数和高阶函数用进去,整个代码风格才变得清爽起来。
今天我们就聊聊这三样东西怎么组合使用:Canvas 的扩展函数、Paint 的 DSL 配置、触摸事件的简化。我保证,看完你也会爱上这种写法。
Canvas 扩展函数:告别重复的 save/restore
先看一个最常见的场景。每次用 Canvas 做平移、旋转、裁剪时,我们都要手动调用 save() 和 restore()。写多了真的很烦。
// 传统写法
canvas.save()
canvas.translate(100f, 100f)
canvas.rotate(45f)
// 画点什么
canvas.restore()
我个人习惯用扩展函数把这个过程封装起来。核心思路就是:把 save/restore 的逻辑交给高阶函数去处理。
// 定义一个 Canvas 的扩展函数
fun Canvas.withSave(block: Canvas.() -> Unit) {
save()
block()
restore()
}
// 使用
canvas.withSave {
translate(100f, 100f)
rotate(45f)
drawCircle(0f, 0f, 50f, paint)
}
你看,withSave 接收一个 Canvas.() -> Unit 类型的高阶函数。在这个 lambda 内部,this 就是 Canvas 本身,所以可以直接调用 translate、rotate、drawCircle 这些方法。save 和 restore 完全被隐藏了。
还可以继续扩展。比如加上裁剪功能:
fun Canvas.withClip(
left: Float, top: Float, right: Float, bottom: Float,
block: Canvas.() -> Unit
) {
save()
clipRect(left, top, right, bottom)
block()
restore()
}
说白了,这就是把「固定流程」抽象成扩展函数,把「变化的部分」用高阶函数暴露出去。你想想看,是不是很优雅?
Paint 的 DSL 配置:让属性设置更直观
Paint 的配置项很多——颜色、粗细、抗锯齿、样式、字体……传统写法是这样的:
val paint = Paint().apply {
color = Color.RED
strokeWidth = 5f
isAntiAlias = true
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
嗯,apply 已经比直接 setter 好很多了。但如果我们想更进一步,可以自己写一个 DSL 构建器:
fun paint(block: Paint.() -> Unit): Paint {
return Paint().apply(block)
}
// 使用
val myPaint = paint {
color = Color.BLUE
strokeWidth = 3f
isAntiAlias = true
style = Paint.Style.FILL
}
这其实就是一个简单的 DSL。但真正的妙用在于——我们可以把 Paint 的配置和 Canvas 的绘制结合起来。
fun Canvas.drawCircle(
cx: Float, cy: Float, radius: Float,
paintBlock: Paint.() -> Unit
) {
val p = Paint().apply(paintBlock)
drawCircle(cx, cy, radius, p)
}
// 使用
canvas.drawCircle(200f, 200f, 100f) {
color = Color.RED
style = Paint.Style.STROKE
strokeWidth = 8f
}
这样做的好处是:Paint 的配置和绘制逻辑写在一起,可读性大大提升。我曾经在一个复杂图表项目中大量使用这种模式,每个图元的 Paint 配置都内联在绘制调用中,代码量减少了将近 40%。
触摸事件的简化:用高阶函数解耦
自定义 View 的触摸事件处理,往往是代码最乱的地方。各种 when 分支、状态判断、坐标计算混在一起。我们可以用高阶函数把「事件处理逻辑」和「事件分发逻辑」拆开。
先定义一个触摸事件的 DSL 构建器:
class TouchHandler {
var onDown: ((MotionEvent) -> Boolean)? = null
var onMove: ((MotionEvent) -> Boolean)? = null
var onUp: ((MotionEvent) -> Boolean)? = null
fun handle(event: MotionEvent): Boolean {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> onDown?.invoke(event) ?: false
MotionEvent.ACTION_MOVE -> onMove?.invoke(event) ?: false
MotionEvent.ACTION_UP -> onUp?.invoke(event) ?: false
else -> false
}
}
}
fun View.onTouch(block: TouchHandler.() -> Unit) {
val handler = TouchHandler().apply(block)
setOnTouchListener { _, event -> handler.handle(event) }
}
然后在 Activity 或 Fragment 中使用:
myView.onTouch {
onDown = { event ->
// 处理按下
true
}
onMove = { event ->
// 处理移动
true
}
onUp = { event ->
// 处理抬起
true
}
}
你可能会问:这跟直接写 setOnTouchListener 有什么区别?区别在于——我们把事件类型和对应的处理逻辑显式地对应起来了。代码结构更清晰,也更容易复用。
还可以更进一步,加入手势识别:
class TouchHandler {
private var startX = 0f
private var startY = 0f
var onDown: ((MotionEvent) -> Boolean)? = null
var onMove: ((MotionEvent) -> Boolean)? = null
var onUp: ((MotionEvent) -> Boolean)? = null
var onSwipe: ((direction: SwipeDirection) -> Boolean)? = null
fun handle(event: MotionEvent): Boolean {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
startY = event.y
onDown?.invoke(event) ?: false
}
MotionEvent.ACTION_MOVE -> {
onMove?.invoke(event) ?: false
}
MotionEvent.ACTION_UP -> {
val dx = event.x - startX
val dy = event.y - startY
if (Math.abs(dx) > 100) {
onSwipe?.invoke(
if (dx > 0) SwipeDirection.RIGHT else SwipeDirection.LEFT
)
}
onUp?.invoke(event) ?: false
}
else -> false
}
}
}
enum class SwipeDirection { LEFT, RIGHT, UP, DOWN }
我曾经在一个自定义轮播图组件中用了这个模式。触摸滑动、点击跳转、边界回弹——所有逻辑都通过 DSL 配置,主类代码非常干净。
三者结合:一个完整的例子
把 Canvas 扩展、Paint DSL、触摸 DSL 组合起来,写一个可拖动的圆形控件:
class DragCircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var cx = 200f
private var cy = 200f
private val radius = 80f
init {
onTouch {
onDown = { true }
onMove = { event ->
cx = event.x
cy = event.y
invalidate()
true
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.withSave {
canvas.drawCircle(cx, cy, radius) {
color = Color.parseColor("#4A90D9")
style = Paint.Style.FILL
isAntiAlias = true
}
canvas.drawCircle(cx, cy, radius) {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 4f
}
}
}
}
你看,整个 View 的代码非常紧凑。绘制部分用 withSave 和 drawCircle DSL 配置,触摸部分用 onTouch DSL 配置。没有冗余的 save/restore,没有散落的 Paint 配置,没有杂乱的触摸分支。
知识体系总览
下面这张图展示了本章的核心逻辑:
从图中可以看出,三个分支都围绕同一个核心——用扩展函数和高阶函数简化代码结构。Canvas 扩展解决的是「流程封装」问题,Paint DSL 解决的是「配置内联」问题,触摸 DSL 解决的是「事件解耦」问题。三者各司其职,组合起来威力巨大。
嗯,这一章的内容就到这里。代码不多,但思路值得反复品味。下次写自定义 View 时,不妨试试这种风格——你会发现,原来 Kotlin 的扩展函数和高阶函数,真的能让 Android 开发变得更有趣。