当我再次用Kotlin完成五年前已经通过Kotlin完成的项目后

 
> 近日来对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)

# 代码的比较
既然是同样的功能, 不同的代码, 即使是由不同时期所完成的, 将其相互比较一下还是有一定意义的. 当然比较的内容都尽量提供相同实现的部分.

## 屏幕触摸事件监测层
主要在于对屏幕的触碰事件的监测

初版代码:
```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.cnblogs.com/clwater/p/17379236.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:当我再次用Kotlin完成五年前已经通过Kotlin完成的项目后 - Python技术站

(0)
上一篇 2023年5月6日
下一篇 2023年5月8日

相关文章

  • 预约直播|3月30日下周四,应用促活大揭秘,快速提升DAU

    【导读】 日常运营过程中,消息推送是App触达用户的常用手段,无论是新功能的通知,还是活动的提醒,都可以通过推送告知用户。千人千面的个性化推送,对于提升用户粘性、用户转化等指标都具有明显正向效果。越来越多的应用选择华为Push用户增长服务作为拉新、促活的首选渠道,他们选择的理由是什么?他们又是如何持续提升用户月活量级并有效促进后端转化的? 本期直播《高效促活…

    Android 2023年4月17日
    00
  • 应用内支付服务现网、沙盒环境下常见关键事件的对比与总结

    在集成和调试订阅型商品时,我们会依赖沙盒环境来进行模拟实际场景。 订阅型商品的购买流程和一次性商品的购买流程类似,但订阅还有其他细节场景,比如续订成功或失败,续订周期时长等。沙盒环境下的订阅续订时间会比正常情况更快,引入“时光机”概念帮助您快速测试您应用的订阅场景。比如订阅周期为1周,商品在3分钟后发生续期,此时订阅型商品有效期延长了3分钟。 下面对沙盒环境…

    Android 2023年4月18日
    00
  • Opengl ES之踩坑记

    前因 最近在尝试使用Opengl ES实现一些LUT滤镜效果,在实现这些滤镜效果的时候遇到一些兼容性的坑,踩过这些坑后我希望把这几个坑分享给读者朋友们,希望同在学习Opengl ES的朋友们能少走弯路。 关于LUT滤镜相关的介绍,也是这个Opengl ES系列入门教程的一项内容,在后面的文章中会专门介绍,这里暂时不展开讲解,后续大家敬请期待。 踩坑详情 1、…

    Android 2023年4月18日
    00
  • RxJava 异常时堆栈显示不正确?解决方法都在这里

    本文首发我的博客,github 地址 大家好,我是徐公,今天为大家带来的是 RxJava 的一个血案,一行代码 return null 引发的。 前阵子,组内的同事反馈说 RxJava 在 debug 包 crash 了,捕获到的异常信息不全。(即我们捕获到的堆栈没有包含我们自己代码,都是一些系统或者 RxJava 框架的代码) 典型的一些 error 信息…

    Android 2023年4月18日
    00
  • android开发Android Studio Electric Eel版本开始支持手机投屏啦

    android开发Android Studio Electric Eel可以手机投屏啦 在Android Studio Electric Eel版本之前,我们需要进行手机投屏,一般使用Vysor等软件,这还是付费的哦,而且还不是很稳定 Android Studio Electric Eel版本开始有投屏功能了,使用起来就像模拟器一样,投屏的位置就是在模拟器窗…

    Android 2023年4月17日
    00
  • 【FAQ】关于华为推送服务因营销消息频次管控导致服务通讯类消息下发失败的解决方案

    一. 问题描述 使用华为推送服务下发IM消息时,下发消息请求成功且code码为80000000,但是手机总是收不到消息; 在华为推送自助分析(Beta)平台查看发现,消息发送触发了频控。 二. 问题原因及背景 2023年1月05日起,华为推送服务对咨询营销类消息做了单个设备每日推送数量上限管理,具体数量上限可以查看如下文档:不同应用类别的推送数量上限要求。 …

    Android 2023年4月19日
    00
  • Android报”NetworkOnMainThreadException”如何解决?

    Android开发中经常会遇到一个错误 android.os.NetworkOnMainThreadException,这意味着当前线程试图访问网络资源,Android系统禁止在主线程中进行网络操作,因为这样会使UI线程阻塞,导致应用程序的响应变慢,让用户感到不满。 以下是该异常原因和解决办法的详细说明: 1. 异常原因 Android系统做了这个限制是为了…

    Android 2023年4月3日
    00
  • Viu联合华为HMS生态,共创影音娱乐新体验

    华为HMS生态携手流媒体平台Viu,为海外消费者打造精品移动娱乐应用体验,并助力提升流量变现能力。Viu在中东非、东南亚等16个国家及地区提供广告合作和付费会员服务,支持优质视频内容高清点播和直播。自2019年起,Viu在中东非区域与华为HMS生态开展一系列紧密合作,并在2022年实现47%的用户增长。 本次,华为邀请Viu中东非区域首席业务官Rohit D…

    Android 2023年4月22日
    00
合作推广
合作推广
分享本页
返回顶部