Kotlin自定义View - 画一个九宫格密码锁

概述

这一次我们来画一个九宫格密码锁界面。这个界面的实现也很简单,归根结底都是绘制然后处理好事件分发就好了。这次还是用Kotlin来写,因为这个功能可能比前两期写的控件还简单一点,所以涉及到自定义 View的一些基础的细节就不再重复细讲。

下面先看看效果:


Password.gif

下面分步实现:
1、画 9个空心大圆和 9个空心小圆
2、onTouch事件监听,记录手指划过的路径
3、留对外接口,密码判断逻辑交给调用者

1、画 9个空心大圆和 9个空心小圆

这一步比较简单。在控件测量完成后,根据控件的宽高先计算确定大圆的半径。然后再分别计算出 9个圆的圆心(大圆和小圆是同心圆)保存在列表里面。

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        pointList.clear()
        // 根据控件宽度计算出大圆半径
        radius = (width / 10).toFloat()
       // 以控件中心点的位置,再根据大圆半径计算出第一个大圆圆心
       // 两个大圆边缘距离这里等于半径距离
        firstPoint.x = (width / 2).toFloat() - 3 * radius
        firstPoint.y = (height / 2).toFloat() - 3 * radius
        // 根据上面第一个圆的圆心,计算出剩下的圆的圆心,并保存到列表
        for (i in 0 until 9) {
            val point = PointF()
            point.x = firstPoint.x + (i % 3) * 3 * radius
            point.y = firstPoint.y + (i / 3) * 3 * radius
            pointList.add(point)
        }
    }

    /***
     * 画 9个圆
     * ***/
    private fun drawCircle(canvas: Canvas?) {
            // 画小圆
            canvas?.drawCircle(pointList[item].x, pointList[item].y, radius / 5, circlePaint)
            // 画大圆
            canvas?.drawCircle(pointList[item].x, pointList[item].y, radius, circlePaint)
        }
    }
2、onTouch事件监听,记录手指划过的路径

这一步就是对 onTouch事件的处理。这里的 DOWN事件、MOVE事件和 UP事件都要处理。当手指按下时,上一次输入要清零,圆的颜色要恢复默认。MOVE事件时,有两步要走。第一步要判断手指触点是否在这些大圆的范围内。第二步判断当前大圆是否已入栈。如果MOVE事件手指触点在某个大圆上,并且是第一次落在这个大圆上,那么就把这个圆的记录下来,作为密码的一环。与此同时,在MOVE事件进行时,也要将密码路径用线连起来,最后一段线的末端是手指当前所处位置。然后是UP事件。手指抬起时,将记录到的密码路径通过接口回调给调用者。

    override fun onTouchEvent(event: MotionEvent): Boolean {
        currentPointF.x = event.x
        currentPointF.y = event.y
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 上次清零
                clearSelect()
               // 开始判断手指是否落在某个圆上
                setSelected(event)
            }
            MotionEvent.ACTION_MOVE -> {
              // 开始判断手指是否落在某个圆上
                setSelected(event)
            }
            MotionEvent.ACTION_UP -> {
                currentPointF.x = -1f
                currentPointF.y = -1f
               // 通过接口回调密码路径
               if (selectedList.size > 0) mOnInputCompletedListener?.invoke(selectedList)
            }
        }
        // 要不断重绘
        invalidate()
        return true
    }

    /***
     * 画圆之间的连线
     * ***/
    private fun drawPath(canvas: Canvas?) {
        if (selectedList.size <= 1) return
        var path = Path()
        path.moveTo(pointList[selectedList[0]].x, pointList[selectedList[0]].y)
        for (i in 1 until selectedList.size) {
            path.lineTo(pointList[selectedList[i]].x, pointList[selectedList[i]].y)
        }
        if (!passwordCorrect){
            linePaint.color = Color.RED
        }else{
            linePaint.color = Color.BLUE
        }
        canvas?.drawPath(path, linePaint)
        linePaint.color = Color.BLUE
    }


    /***
     * 画手指连线
     * ***/
    private fun drawLine(canvas: Canvas?){
        if ((selectedList.size <= 0) || (currentPointF.x < 0)) return
        var startX = pointList[selectedList.last()].x
        var startY = pointList[selectedList.last()].y
        canvas?.drawLine(startX,startY, currentPointF.x, currentPointF.y, linePaint)
    }
3、留对外接口,密码判断逻辑交给调用者

最后一步要完善一下对外接口。根据面向对象的原则,我们要将判断密码是否正确的逻辑交给调用者实现。我们只需要在手指抬起时将用户的输入通过接口回调给调用者就好:

    // 回调,Lambda表达式
    private var mOnInputCompletedListener: ((MutableList<Int>) -> Unit)? = null

    fun setOnInputCompletedListener(onInputCompletedListener: ((MutableList<Int>) -> Unit)?){
        this.mOnInputCompletedListener = onInputCompletedListener
    }

    /***
     * 密码是否正确,给调用者调用的方法
     * ***/
    fun setPasswordCorrect(correct: Boolean){
        this.passwordCorrect = correct
        postInvalidate()
    }

下面是完整代码:

class PasswordView : View {
    // 大圆半径
    private var radius = 0f
    // 第一个点的圆心
    private var firstPoint: PointF = PointF()
    // 所有点的圆心
    private var pointList: MutableList<PointF> = ArrayList()
    // 圆的画笔
    private var circlePaint: Paint = Paint()
    // 线的画笔
    private var linePaint: Paint = Paint()
    // 已选中的圆的集合
    private var selectedList: MutableList<Int> = ArrayList(9)
    // 当前手指触点位置
    private var currentPointF = PointF()
    // 回调,Lambda表达式
    private var mOnInputCompletedListener: ((MutableList<Int>) -> Unit)? = null

    private var passwordCorrect = true

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

    constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context,
        attributeSet,
        defStyle) {
        init(context, attributeSet, defStyle)
    }

    private fun init(context: Context, attributeSet: AttributeSet?, defStyle: Int) {
        circlePaint.color = Color.GRAY
        circlePaint.style = Paint.Style.STROKE
        circlePaint.isAntiAlias = true
        circlePaint.isDither = true
        circlePaint.strokeWidth = dpToPx(3f)

        linePaint.color = Color.BLUE
        linePaint.style = Paint.Style.STROKE
        linePaint.isAntiAlias = true
        linePaint.isDither = true
        linePaint.strokeWidth = dpToPx(10f)

    }

    fun setOnInputCompletedListener(onInputCompletedListener: ((MutableList<Int>) -> Unit)?){
        this.mOnInputCompletedListener = onInputCompletedListener
    }

    /***
     * 密码是否正确,给调用者调用的方法
     * ***/
    fun setPasswordCorrect(correct: Boolean){
        this.passwordCorrect = correct
        postInvalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        var width = MeasureSpec.getSize(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        var height = MeasureSpec.getSize(heightMeasureSpec)
        if (widthMode == MeasureSpec.AT_MOST) width = dpToPx(300f).toInt()
        if (heightMode == MeasureSpec.AT_MOST) height = dpToPx(300f).toInt()
        setMeasuredDimension(width, height)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        pointList.clear()
        radius = (width / 10).toFloat()
        firstPoint.x = (width / 2).toFloat() - 3 * radius
        firstPoint.y = (height / 2).toFloat() - 3 * radius
        for (i in 0 until 9) {
            val point = PointF()
            point.x = firstPoint.x + (i % 3) * 3 * radius
            point.y = firstPoint.y + (i / 3) * 3 * radius
            pointList.add(point)
        }
    }

    override fun onDraw(canvas: Canvas?) {
        drawCircle(canvas)
        drawPath(canvas)
        drawLine(canvas)
    }

    /***
     * 画 9个圆
     * ***/
    private fun drawCircle(canvas: Canvas?) {
        for (item in 0 until pointList.size) {
            if (isPointSelected(item)) {
                circlePaint.color = Color.BLUE
                circlePaint.style = Paint.Style.FILL
                if (!passwordCorrect) circlePaint.color = Color.RED
            } else {
                circlePaint.color = Color.GRAY
                circlePaint.style = Paint.Style.STROKE
            }
            canvas?.drawCircle(pointList[item].x, pointList[item].y, radius / 5, circlePaint)
            circlePaint.style = Paint.Style.STROKE
            canvas?.drawCircle(pointList[item].x, pointList[item].y, radius, circlePaint)
        }
    }

    /***
     * 画圆之间的连线
     * ***/
    private fun drawPath(canvas: Canvas?) {
        if (selectedList.size <= 1) return
        var path = Path()
        path.moveTo(pointList[selectedList[0]].x, pointList[selectedList[0]].y)
        for (i in 1 until selectedList.size) {
            path.lineTo(pointList[selectedList[i]].x, pointList[selectedList[i]].y)
        }
        if (!passwordCorrect){
            linePaint.color = Color.RED
        }else{
            linePaint.color = Color.BLUE
        }
        canvas?.drawPath(path, linePaint)
        linePaint.color = Color.BLUE
    }


    /***
     * 画手指连线
     * ***/
    private fun drawLine(canvas: Canvas?){
        if ((selectedList.size <= 0) || (currentPointF.x < 0)) return
        var startX = pointList[selectedList.last()].x
        var startY = pointList[selectedList.last()].y
        canvas?.drawLine(startX,startY, currentPointF.x, currentPointF.y, linePaint)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        currentPointF.x = event.x
        currentPointF.y = event.y
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                clearSelect()
                setSelected(event)
            }
            MotionEvent.ACTION_MOVE -> {
                setSelected(event)
            }
            MotionEvent.ACTION_UP -> {
                currentPointF.x = -1f
                currentPointF.y = -1f
               if (selectedList.size > 0) mOnInputCompletedListener?.invoke(selectedList)
            }
        }
        invalidate()
        return true
    }

    /***
     * 当前点是否已被选中
     * ***/
    private fun isPointSelected(item: Int): Boolean {
        if (selectedList.size <= 0) return false
        for (i in 0 until selectedList.size) {
            if (item == selectedList[i]) return true
        }
        return false
    }

    /***
     * 清空
     * ***/
    private fun clearSelect() {
        selectedList.clear()
        passwordCorrect = true
    }

    /***
     * 收集数字
     * ***/
    private fun setSelected(event: MotionEvent) {
        for (i in 0 until pointList.size) {
            Log.d("contains",
                "contains = " + selectedList.contains(i) + "-size = " + selectedList.size)
            if ((isOnThePoint(pointList[i], event, radius)) && (!selectedList.contains(i))) {
                selectedList.add(i)
            }
        }
    }

    /***
     * 触点是否落在某个大圆上
     * ***/
    private fun isOnThePoint(pointF: PointF, event: MotionEvent, theRadius: Float): Boolean {
        return sqrt((event.x - pointF.x).toDouble()
            .pow(2) + (event.y - pointF.y).pow(2)) < theRadius
    }

    private fun dpToPx(dip: Float): Float {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, resources.displayMetrics)
    }
}

最后记录一下 Kotlin语法问题
接口回调:

    // 定义回调,Lambda表达式
    private var mOnInputCompletedListener: ((MutableList<Int>) -> Unit)? = null
    // 赋值
    fun setOnInputCompletedListener(onInputCompletedListener: ((MutableList<Int>) -> Unit)?){
        this.mOnInputCompletedListener = onInputCompletedListener
    }
    // 接口调用
    mOnInputCompletedListener?.invoke(selectedList)

   // 使用
    passwordView.setOnInputCompletedListener { password ->
            Log.d(TAG, "password = $password")
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容