> 近日来对Kotlin的使用频率越来越高, 也对自己近年来写过的Kotlin代码尝试进行一个简单的整理. 翻到了自己五年前第一次使用Kotlin来完成的一个项目([贝塞尔曲线](https://juejin.cn/post/6844903556173004807)), 一时兴起, 又用发展到现在的Kotlin和Compose再次完成了这个项目. 也一遍来看看这几年我都在Kotlin中学到了什么.
关于贝塞尔曲线, 这里就不多赘述了. 简单来说, 针对每一个线段, 某个点到两端的比例都是一样的, 而贝塞尔曲线就是这个过程的中线段两端都在同一位置的线段(点)过程的集合.
如图, AD和AB的比例, BE和BC的比例还有DF和DE的比例都是一样的.这个比例从0到1, F点的位置连成线, 就是ABC这三个点的贝塞尔曲线.
![Bezier](https://clwater-obsidian.oss-cn-beijing.aliyuncs.com/img/449809-20191009163226592-1802036977.png)

# 两次完成的感受
虽然时隔五年, 但是对这个项目的印象还是比较深刻的(毕竟当时找啥资料都不好找).
当时的项目还用的是Kotlin Synthetic来进行数据绑定(虽然现在已经被弃用了), 对于当时还一直用findViewById和@BindView的我来说, 这是对我最大的惊喜. 是的, 当时用Kotlin最大惊喜就是这个. 其它的感觉就是这个”语法糖”看起来还挺好用的. 而现在, 我可以通过Compose来完成页面的布局. 最直观的结果是代码量的减少, 初版功能代码(带xml)大概有800行, 而这次完成整个功能大概只需要450行.
在使用过程中对”Compose is function”理念的理解更深了一步, 数据就是数据. 将数据作为一个参数放到Compose这个function中, 在数据变化的时候重新调用function, 达到更新UI的效果. 显而易见的事情是我们不需要的额外的持有UI的对象了, 我们不必考虑UI中某个元素和另一个元素直接的关联, 不必考虑某个元素响应什么样的操作. 我们只需要考虑某个Compose(function) 在什么样的情况下(入参)需要表现成什么样子.
比如Change Point按钮点下时, 会更改`mInChange`的内容, 从而影响许多其它元素的效果, 如果通过View来实现, 我需要监听Change Point的点击事件, 然后依次修改影响到的元素(这个过程中需要持有大量其它View的对象). 不过当使用Compose后, 虽然我们仍要监听Change Point的点击事件, 但是对对应Change Point的监听动作来说, 它只需要修改`mInChange`的内容就行了, 修改这个值会发生什么变化它不需要处理也不要知道. 真正需要变化的Compose来处理就可以了(可以理解为参数变化了, 重新调用了这个function)
特性的部分使用的并不多, 比较项目还是比较小, 很多特性并没有体现出来.
最令我感到开心的是, 再一次完成同样的功能所花费的时间仅仅只有半天多, 而5年前完成类似的功能大概用了一个多星期的时间. 也不知道我和Kotlin这5年来哪一方变化的更大?.
# 贝塞尔曲线工具先来看一下具有的功能, 主要的功能就是绘制贝塞尔曲线(可绘制任意阶数), 显示计算过程(辅助线的绘制), 关键点的调整, 以及新增的绘制进度手动调整. 为了更本质的显示绘制的结果, 此次并没有对最终结果点进行显示优化, 所以在短时间变化位置大的情况下, 可能出现不连续的现象.

![3_point_bezier](https://clwater-obsidian.oss-cn-beijing.aliyuncs.com/img/bezier_1.gif)
![more_point_bezier](https://clwater-obsidian.oss-cn-beijing.aliyuncs.com/img/202305061905728.gif)

![bizier_change](https://clwater-obsidian.oss-cn-beijing.aliyuncs.com/img/202305061932923.gif)

![bezier_progress](https://clwater-obsidian.oss-cn-beijing.aliyuncs.com/img/202305061926327.gif)
# 代码的比较既然是同样的功能, 不同的代码, 即使是由不同时期所完成的, 将其相互比较一下还是有一定意义的. 当然比较的内容都尽量提供相同实现的部分.
## 屏幕触摸事件监测层主要在于对屏幕的触碰事件的监测
初版代码: “`kotlinoverride fun onTouchEvent(event: MotionEvent): Boolean {

touchX = event.x touchY = event.y when (event.action) { MotionEvent.ACTION_DOWN -> { toFindChageCounts = true findPointChangeIndex = -1 //增加点前点击的点到屏幕中 if (controlIndex { checkLevel++ //判断当前是否需要检测更换点坐标 if (inChangePoint){ //判断当前是否长按 用于开始查找附件的点 if (touchX == lastPoint.x && touchY == lastPoint.y){ changePoint = true lastPoint.x = -1F lastPoint.y = -1F }else{ lastPoint.x = touchX lastPoint.y = touchY } //开始查找附近的点 if (changePoint){ if (toFindChageCounts){ findPointChangeIndex = findNearlyPoint(touchX , touchY) } }
//判断是否存在附近的点 if (findPointChangeIndex == -1){ if (checkLevel > 1){ changePoint = false }
}else{ //更新附近的点的坐标 并重新绘制页面内容 points[findPointChangeIndex].x = touchX points[findPointChangeIndex].y = touchY toFindChageCounts = false invalidate() } }
} MotionEvent.ACTION_UP ->{ checkLevel = -1 changePoint = false toFindChageCounts = false }
} return true}“`
二次代码:
“`kotlinCanvas( … .pointerInput(Unit) { detectDragGestures( onDragStart = { model.pointDragStart(it) }, onDragEnd = { model.pointDragEnd() } ) { _, dragAmount -> model.pointDragProgress(dragAmount) } } .pointerInput(Unit) { detectTapGestures { model.addPoint(it.x, it.y) } } ) …
/** * change point position start, check if have point in range */ fun pointDragStart(position: Offset) { if (!mInChange.value) { return } if (mBezierPoints.isEmpty()) { return } mBezierPoints.firstOrNull() { position.x > it.x.value – 50 && position.x it.y.value – 50 && position.y < it.y.value + 50 }.let { bezierPoint = it } }
/** * change point position end */ fun pointDragEnd() { bezierPoint = null }
/** * change point position progress */ fun pointDragProgress(drag: Offset) { if (!mInChange.value || bezierPoint == null) { return } else { bezierPoint!!.x.value += drag.x bezierPoint!!.y.value += drag.y calculate() } }“`
可以看到由于Compose提供了Tap和Drag的详细事件, 从而导致新的代码少许多的标记位变量.
而我之前一度认为是语法糖的特性来给我带来了不小的惊喜.
譬如这里查找点击位置最近的有效的点的方法,
初版代码:“`kotlin//判断当前触碰的点附近是否有绘制过的点private fun findNearlyPoint(touchX: Float, touchY: Float): Int { Log.d(“bsr” , “touchX: ${touchX} , touchY: ${touchY}”) var index = -1 var tempLength = 100000F for (i in 0..points.size – 1){ val lengthX = Math.abs(touchX – points[i].x) val lengthY = Math.abs(touchY – points[i].y) val length = Math.sqrt((lengthX * lengthX + lengthY * lengthY).toDouble()).toFloat() if (length < tempLength){ tempLength = length
if (tempLength < minLength){ toFindChageCounts = false index = i } } }
return index}
“`
而二次代码:“`kotlin mBezierPoints.firstOrNull() { position.x > it.x.value – 50 && position.x it.y.value – 50 && position.y < it.y.value + 50 }.let { bezierPoint = it }“`
和Java的Steam类似, 链式结构看起来更加的易于理解.

## 贝塞尔曲线绘制层
主要的贝塞尔曲线是通过递归实现的

初版代码:
“`kotlin//通过递归方法绘制贝塞尔曲线private fun drawBezier(canvas: Canvas, per: Float, points: MutableList) {
val inBase: Boolean
//判断当前层级是否需要绘制线段 if (level == 0 || drawControl){ inBase = true }else{ inBase = false }

//根据当前层级和是否为无限制模式选择线段及文字的颜色 if (isMore){ linePaint.color = 0x3F000000 textPaint.color = 0x3F000000 }else { linePaint.color = colorSequence[level].toInt() textPaint.color = colorSequence[level].toInt() }
//移动到开始的位置 path.moveTo(points[0].x , points[0].y)
//如果当前只有一个点 //根据贝塞尔曲线定义可以得知此点在贝塞尔曲线上 //将此点添加到贝塞尔曲线点集中(页面重新绘制后之前绘制的数据会丢失 需要重新回去前段的曲线路径) //将当前点绘制到页面中 if (points.size == 1){ bezierPoints.add(Point(points[0].x , points[0].y)) drawBezierPoint(bezierPoints , canvas) val paint = Paint() paint.strokeWidth = 10F paint.style = Paint.Style.FILL canvas.drawPoint(points[0].x , points[0].y , paint) return }

val nextPoints: MutableList = ArrayList()
//更新路径信息 //计算下一级控制点的坐标 for (index in 1..points.size – 1){ path.lineTo(points[index].x , points[index].y)
val nextPointX = points[index – 1].x -(points[index – 1].x – points[index].x) * per val nextPointY = points[index – 1].y -(points[index – 1].y – points[index].y) * per
nextPoints.add(Point(nextPointX , nextPointY)) }
//绘制控制点的文本信息 if (!(level !=0 && (per==0F || per == 1F) )) { if (inBase) { if (isMore && level != 0){ canvas.drawText(“0:0”, points[0].x, points[0].y, textPaint) }else { canvas.drawText(“${charSequence[level]}0”, points[0].x, points[0].y, textPaint) } for (index in 1..points.size – 1){ if (isMore && level != 0){ canvas.drawText( “${index}:${index}” ,points[index].x , points[index].y , textPaint) }else { canvas.drawText( “${charSequence[level]}${index}” ,points[index].x , points[index].y , textPaint) } } } }
//绘制当前层级 if (!(level !=0 && (per==0F || per == 1F) )) { if (inBase) { canvas.drawPath(path, linePaint) } } path.reset()
//更新层级信息 level++
//绘制下一层 drawBezier(canvas, per, nextPoints)
}

“`
二次代码:“`kotlin{ lateinit var preBezierPoint: BezierPoint val paint = Paint() paint.textSize = mTextSize.toPx()
for (pointList in model.mBezierDrawPoints) { if (pointList == model.mBezierDrawPoints.first() || (model.mInAuxiliary.value && !model.mInChange.value) ) { for (point in pointList) { if (point != pointList.first()) { drawLine( color = Color(point.color), start = Offset(point.x.value, point.y.value), end = Offset(preBezierPoint.x.value, preBezierPoint.y.value), strokeWidth = mLineWidth.value ) } preBezierPoint = point
drawCircle( color = Color(point.color), radius = mPointRadius.value, center = Offset(point.x.value, point.y.value) ) paint.color = Color(point.color).toArgb() drawIntoCanvas { it.nativeCanvas.drawText( point.name, point.x.value – mPointRadius.value, point.y.value – mPointRadius.value * 1.5f, paint ) } } } }
… }

/** * calculate Bezier line points */ private fun calculateBezierPoint(deep: Int, parentList: List) { if (parentList.size > 1) { val childList = mutableListOf() for (i in 0 until parentList.size – 1) { val point1 = parentList[i] val point2 = parentList[i + 1] val x = point1.x.value + (point2.x.value – point1.x.value) * mProgress.value val y = point1.y.value + (point2.y.value – point1.y.value) * mProgress.value if (parentList.size == 2) { mBezierLinePoints[mProgress.value] = Pair(x, y) return } else { val point = BezierPoint( mutableStateOf(x), mutableStateOf(y), deep + 1, “${mCharSequence.getOrElse(deep + 1){“Z”}}$i”, mColorSequence.getOrElse(deep + 1) { 0xff000000 } ) childList.add(point) } } mBezierDrawPoints.add(childList) calculateBezierPoint(deep + 1, childList) } else { return } }“`
初版开发的时候受个人能力限制, 递归方法中既包含了绘制的功能也包含了计算下一层的功能. 而二次编码的时候受Compose的设计影响, 尝试将所有的点状态变为Canvas的入参信息. 代码的编写过程就变得更加的流程.
当然, 现在的我和五年前的我, 开发的能力一定是不一样的. 即便如此, 随着Kotlin的不断发展, 即使是同样用Kotlin完成的项目, 随着新的概念的提出, 更多更适合新的开发技术的出现, 我们仍然从Kotlin和Compose收获更多.

# 我和Kotlin的小故事
初次认识Kotlin是在2017的5月, 当时Kotlin还不是Google所推荐的Android开发语言. 对我来说, Kotlin更多的是个新的技术, 在实际的工作中也无法进行使用.
即使如此, 我也尝试开始用Kotlin去完成更多的内容, 所幸如此, 不然这篇文章就无法完成了, 我也错过了一个更深层次了解Kotlin的机会.
但是即便2018年Google将Kotlin作为Android的推荐语言, 但Kotlin在当时仍不是一个主流的选择. 对我来说以下的一些问题导致了我在当时对Kotlin的使用性质不高. 一是新语言, 社区构建不完善, 有许多的内容需要大家填充, 带来就是在实际的使用情况中会遇到各种的问题, 这些问题在网站中没有找到可行的解决方案. 二是可以和Java十分便捷互相使用的特性, 这个特性是把双刃剑,虽然可以让我更加无负担的使用Kotlin(不行再用Java写呗.). 但也使得我认为Kotlin是个Java++或者Java–. 三是无特殊性, Kotlin并没有带来什么新的内容, Kotlin能完成的事情Java都能做完成, (空值和data class之类的在我看来更多的是一个语法糖.) 那么我为什么要用一种新的不熟悉的技术来完成我都需求?
所幸的是, 还是有更多的人在不断的推进和建设Kotlin. 也吸引了越来越多的人加入. 近年来越来越多的项目中都开始有着Kotlin的踪迹, 我将Kotlin添加到现有的项目中也变得越来越能被大家所接受. 也期待可以帮助到更多的人.

### 相关代码地址:[初次代码](https://github.com/clwater/BezierCurve)

[二次代码](https://github.com/clwater/AndroidComposeCanvas/tree/master/app/src/main/java/com/clwater/compose_canvas/bezier)