> 近日來對Kotlin的使用頻率越來越高, 也對自己近年來寫過的Kotlin代碼嘗試進行一個簡單的整理. 翻到了自己五年前第一次使用Kotlin來完成的一個專案([貝塞爾曲線](https://juejin.cn/post/6844903556173004807)), 一時興起, 又用發展到現在的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年來哪一方變化的更大??.
# 貝塞爾曲線工具 先來看一下具有的功能, 主要的功能就是繪制貝塞爾曲線(可繪制任意階數), 顯示計算程序(輔助線的繪制), 關鍵點的調整, 以及新增的繪制進度手動調整. 為了更本質的顯示繪制的結果, 此次并沒有對最終結果點進行顯示優化, 所以在短時間變化位置大的情況下, 可能出現不連續的現象.




# 代碼的比較 既然是同樣的功能, 不同的代碼, 即使是由不同時期所完成的, 將其相互比較一下還是有一定意義的. 當然比較的內容都盡量提供相同實作的部分.
## 螢屏觸摸事件監測層 主要在于對螢屏的觸碰事件的監測
初版代碼: ```kotlin 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 } ```
二次代碼:
```kotlin 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的詳細事件, 從而導致新的代碼少許多的標記位變數.
而我之前一度認為是語法糖的特性來給我帶來了不小的驚喜.
譬如這里查找點擊位置最近的有效的點的方法,
初版代碼: ```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.x.value + 50 && position.y > 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<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)
}
```
二次代碼: ```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<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添加到現有的專案中也變得越來越能被大家所接受. 也期待可以幫助到更多的人.
### 相關代碼地址: [初次代碼](https://github.com/clwater/BezierCurve)
[二次代碼](https://github.com/clwater/AndroidComposeCanvas/tree/master/app/src/main/java/com/clwater/compose_canvas/bezier)
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/551878.html
標籤:Android
下一篇:返回列表