近日来对Kotlin的运用频率越来越高, 也对自己近年来写过的Kotlin代码测验进行一个简略的收拾. 翻到了自己五年前第一次运用Kotlin来完结的一个项目(贝塞尔曲线), 一时兴起, 又用发展到现在的Kotlin和Compose再次完结了这个项目. 也一遍来看看这几年我都在Kotlin中学到了什么.
关于贝塞尔曲线, 这儿就不多赘述了. 简略来说, 针对每一个线段, 某个点到两头的份额都是相同的, 而贝塞尔曲线便是这个进程的中线段两头都在同一方位的线段(点)进程的集合.
如图, AD和AB的份额, BE和BC的份额还有DF和DE的份额都是相同的.这个份额从0到1, F点的方位连成线, 便是ABC这三个点的贝塞尔曲线.
两次完结的感触
尽管时隔五年, 可是对这个项目的印象仍是比较深刻的(究竟其时找啥材料都不好找).
其时的项目还用的是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年来哪一方改变的更大.
贝塞尔曲线工具
先来看一下具有的功用, 首要的功用便是制作贝塞尔曲线(可制作恣意阶数), 显示核算进程(辅助线的制作), 关键点的调整, 以及新增的制作进展手动调整. 为了更本质的显示制作的成果, 此次并没有对最终成果点进行显示优化, 所以在短时间改变方位大的情况下, 或许呈现不连续的现象.
代码的比较
既然是相同的功用, 不同的代码, 即使是由不同时期所完结的, 将其相互比较一下仍是有必定含义的. 当然比较的内容都尽量提供相同完结的部分.
屏幕接触事情监测层
首要在于对屏幕的触碰事情的监测
初版代码:
override fun onTouchEvent(event: MotionEvent): Boolean {
touchX = event.x
touchY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
toFindChageCounts = true
findPointChangeIndex = -1
//增加点前点击的点到屏幕中
if (controlIndex < maxPoint || isMore == true) {
addPoints(BezierCurveView.Point(touchX, touchY))
}
invalidate()
}
MotionEvent.ACTION_MOVE ->{
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
}
二次代码:
Canvas(
...
.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.x.value + 50 &&
position.y > 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的具体事情, 然后导致新的代码少许多的符号位变量.
而我之前一度以为是语法糖的特性来给我带来了不小的惊喜.
比如这儿查找点击方位最近的有效的点的办法,
初版代码:
//判断其时触碰的点邻近是否有制作过的点
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
}
而二次代码:
mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}
和Java的Stream相似, 链式结构看起来愈加的易于理解.
贝塞尔曲线制作层
首要的贝塞尔曲线是经过递归完结的
初版代码:
//经过递归办法制作贝塞尔曲线
private fun drawBezier(canvas: Canvas, per: Float, points: MutableList<Point>) {
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<Point> = 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)
}
二次代码:
{
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<BezierPoint>) {
if (parentList.size > 1) {
val childList = mutableListOf<BezierPoint>()
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添加到现有的项目中也变得越来越能被我们所承受. 也期待能够协助到更多的人.
相关代码地址:
初度代码
二次代码