美文网首页
Kotlin自定义View - 画一个九宫格密码锁

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

作者: 碧云天EthanLee | 来源:发表于2021-08-30 14:21 被阅读0次
概述

这一次我们来画一个九宫格密码锁界面。这个界面的实现也很简单,归根结底都是绘制然后处理好事件分发就好了。这次还是用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")
    }

相关文章

网友评论

      本文标题:Kotlin自定义View - 画一个九宫格密码锁

      本文链接:https://www.haomeiwen.com/subject/vwoniltx.html