Android自定义九宫格密码解锁

最终效果

Android自定义九宫格密码解锁_第1张图片

相关代码

布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.crystal.view.LockPatternView
        android:id="@+id/lockPattern"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
LockPatternActivity
class LockPatternActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_lock_pattern)
        val context = this
        val lockPattern = findViewById<LockPatternView>(R.id.lockPattern)
        lockPattern.setDefaultPassWord("0124678")
        lockPattern.setLockPatternCallBack(object : LockPatternView.LockPatternCallBack {
            override fun checkSuccess() {
                Toast.makeText(context, "password check success!", Toast.LENGTH_SHORT).show()
            }

            override fun checkError() {
                Toast.makeText(
                    context,
                    "password check error!",
                    Toast.LENGTH_SHORT
                ).show()
            }

        })
    }

}
自定义LockPatternView
package com.crystal.view

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Paint.Style
import android.graphics.Path
import android.graphics.Rect

import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View

/**
 * 九宫格锁屏控件
 * on 2022/10/28
 */
class LockPatternView : View {
    /**
     * 绘制默认状态下画笔
     */
    private lateinit var normalPaint: Paint

    /**
     * 绘制选中状态画笔
     */
    private lateinit var pressPaint: Paint

    /**
     * 绘制错误状态画笔
     */
    private lateinit var errorPaint: Paint

    /**
     * 绘制箭头画笔
     */
    private lateinit var arrowPaint: Paint

    /**
     * 绘制连线画笔
     */
    private lateinit var linePaint: Paint

    /**
     * 绘制选中状态下外圆颜色
     */
    private val outerPressColor = 0xff8cbad8.toInt()

    /**
     * 绘制选中状态下内圆颜色
     */
    private val innerPressColor = 0xff0596f6.toInt()

    /**
     * 绘制默认状态下外圆颜色
     */
    private val outerNormalColor = 0xffd9d9d9.toInt()

    /**
     * 绘制默认状态下内圆颜色
     */
    private val innerNormalColor = 0xff929292.toInt()

    /**
     * 绘制错误状态下外圆颜色
     */
    private val outerErrorColor = 0xff901032.toInt()

    /**
     * 绘制错误状态下内圆颜色
     */
    private val innerErrorColor = 0xff901032.toInt()

    /**
     * 当前密码
     */
    private var setPassword: String? = null

    /**
     * 二维数组
     */
    private var points: Array<Array<Point?>> = Array(3) {
        Array(3) { null }
    }

    /**
     * 外圆的半径
     */
    private var outerRadius: Float = 0f

    /**
     * 按下的时候 是否是按在一个点上
     */
    private var isTouchPoint = false

    /**
     * 选中的点集合
     */
    private var selectPoints = ArrayList<Point>()

    /**
     * 是否已初始化
     */
    private var isInit: Boolean = false

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context, attrs, defStyleAttr
    )

    private fun initPaint() {
        normalPaint = Paint()
        normalPaint.isAntiAlias = true
        normalPaint.style = Style.STROKE
        normalPaint.strokeWidth = outerRadius / 9


        pressPaint = Paint()
        pressPaint.isAntiAlias = true
        pressPaint.style = Style.STROKE
        pressPaint.strokeWidth = outerRadius / 6


        errorPaint = Paint()
        errorPaint.isAntiAlias = true
        errorPaint.style = Style.STROKE
        errorPaint.strokeWidth = outerRadius / 6

        arrowPaint = Paint()
        arrowPaint.isAntiAlias = true
        arrowPaint.style = Style.FILL
        arrowPaint.color = innerPressColor


        linePaint = Paint()
        linePaint.isAntiAlias = true
        linePaint.style = Style.STROKE
        linePaint.color = innerPressColor
        linePaint.strokeWidth = outerRadius / 9

    }


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (!isInit) {
            //避免多次初始化
            isInit = true
            initPoints()
            initPaint()
        }
    }

    private fun initPoints() {
        //九宫格布局为match_parent,计算显示位置距离屏幕上方距离offset,同时需要考虑横屏情况
        var width = this.measuredWidth
        var height = this.measuredHeight
        var offsetX = 0f
        var offsetY = 0f
        if (height > width) {
            //当前为竖屏
            offsetY = (height - width) / 2f
            height = width
        } else {
            offsetX = (width - height) / 2f
            //设置九宫格宽度和高度一致
            width = height
        }
        //每一个方块的宽以及高
        val squareWidth = width / 3
        //则第一个中心点的坐标为【offsetX + squareWidth /2 ,offsetY+ squareWidth /2】 后续坐标依次类推
        points[0][0] = Point(offsetX + squareWidth / 2, offsetY + squareWidth / 2, 0)
        points[0][1] = Point(offsetX + squareWidth * 3 / 2, offsetY + squareWidth / 2, 1)
        points[0][2] = Point(offsetX + squareWidth * 5 / 2, offsetY + squareWidth / 2, 2)

        points[1][0] = Point(offsetX + squareWidth / 2, offsetY + squareWidth * 3 / 2, 3)
        points[1][1] = Point(offsetX + squareWidth * 3 / 2, offsetY + squareWidth * 3 / 2, 4)
        points[1][2] = Point(offsetX + squareWidth * 5 / 2, offsetY + squareWidth * 3 / 2, 5)

        points[2][0] = Point(offsetX + squareWidth / 2, offsetY + squareWidth * 5 / 2, 6)
        points[2][1] = Point(offsetX + squareWidth * 3 / 2, offsetY + squareWidth * 5 / 2, 7)
        points[2][2] = Point(offsetX + squareWidth * 5 / 2, offsetY + squareWidth * 5 / 2, 8)
        /**
         * 外圆的大小为宽度的1/12
         */
        outerRadius = width / 12f

    }


    override fun onDraw(canvas: Canvas) {

        for (i in 0..2) {
            for (point in points[i]) {
                if (point!!.isNormalStatus()) {
                    //普通状态 绘制外圆
                    normalPaint.color = outerNormalColor
                    canvas.drawCircle(point.centerX, point.centerY, outerRadius, normalPaint)
                    //普通状态 绘制内圆
                    normalPaint.color = innerNormalColor
                    canvas.drawCircle(point.centerX, point.centerY, outerRadius / 6, normalPaint)
                }

                if (point!!.isPressStatus()) {
                    // 选中状态 绘制外圆
                    pressPaint.color = outerPressColor
                    canvas.drawCircle(point.centerX, point.centerY, outerRadius, pressPaint)

                    //选中状态 绘制内圆
                    pressPaint.color = innerPressColor
                    canvas.drawCircle(point.centerX, point.centerY, outerRadius / 6, pressPaint)
                }

                if (point!!.isErrorStatus()) {
                    // 错误状态 绘制外圆
                    errorPaint.color = outerErrorColor
                    canvas.drawCircle(point.centerX, point.centerY, outerRadius, errorPaint)

                    //选中状态 绘制内圆
                    errorPaint.color = innerErrorColor
                    canvas.drawCircle(point.centerX, point.centerY, outerRadius / 6, errorPaint)
                }
            }
        }
        //绘制两个点之间的连线以及箭头
        drawLineAndArrow(canvas)
    }

    private fun drawLineAndArrow(canvas: Canvas) {
        if (selectPoints.size < 1) {
            //没有选中的点,清除画布重绘
            canvas.save()
            canvas.clipRect(Rect(0, 0, width, height))
            canvas.restore()
            return
        }
        var lastPoint = selectPoints[0]
        for (i in 1 until selectPoints.size) {
            //两个点之间绘制一条线
            drawLine(lastPoint, selectPoints[i], canvas, linePaint)
            //两个点之间绘制一个箭头
            drawArrow(canvas, arrowPaint, lastPoint, selectPoints[i], outerRadius / 5.0, 38)
            lastPoint = selectPoints[i]
        }

        //绘制最后一个点到手指当前位置的连线,同时如果手指在圆内就不需要绘制
        val isInnerPoint =
            MathUtil.checkInRound(
                lastPoint.centerX,
                lastPoint.centerY,
                outerRadius / 4,
                moveX,
                moveY
            )
        if (!isInnerPoint && isTouchPoint) {
            drawLine(lastPoint, Point(moveX, moveY, -1), canvas, linePaint)
        }


    }

    private fun drawArrow(
        canvas: Canvas,
        arrowPaint: Paint,
        start: LockPatternView.Point,
        end: LockPatternView.Point,
        arrowHeight: Double,
        angle: Int
    ) {
        val d = MathUtil.distance(
            start.centerX.toDouble(),
            start.centerY.toDouble(),
            end.centerX.toDouble(),
            end.centerY.toDouble()
        )
        val sinB = ((end.centerX - start.centerX) / d).toFloat()
        val cosB = ((end.centerY - start.centerY) / d).toFloat()
        val tanA = Math.tan(Math.toRadians(angle.toDouble())).toFloat()
        val h = (d - arrowHeight - outerRadius * 1.1).toFloat()
        val l = arrowHeight * tanA
        val a = l * sinB
        val b = l * cosB
        val x0 = h * sinB
        val y0 = h * cosB
        val x1 = start.centerX + (h + arrowHeight) * sinB
        val y1 = start.centerY + (h + arrowHeight) * cosB
        val x2 = start.centerX + x0 - b
        val y2 = start.centerY + y0 + a
        val x3 = start.centerX + x0 + b
        val y3 = start.centerY + y0 - a
        val path = Path()
        path.moveTo(x1.toFloat(), y1.toFloat())
        path.lineTo(x2.toFloat(), y2.toFloat())
        path.lineTo(x3.toFloat(), y3.toFloat())
        path.close()
        canvas.drawPath(path, arrowPaint)
    }

    /**
     * 绘制连线
     */
    private fun drawLine(
        startPoint: LockPatternView.Point,
        endPoint: LockPatternView.Point,
        canvas: Canvas,
        linePaint: Paint
    ) {
        //两个点之间的距离
        val pointDistance = MathUtil.distance(
            startPoint.centerX.toDouble(),
            startPoint.centerY.toDouble(),
            endPoint.centerX.toDouble(),
            endPoint.centerY.toDouble()
        )
        // 圆心距离
        val dx = endPoint.centerX - startPoint.centerX
        val dy = endPoint.centerY - startPoint.centerY
        // rx = sin@ * 内圆半径
        val rx = (dx / pointDistance * outerRadius / 6.0).toFloat()
        // ry = cos@ * 内圆半径
        val ry = (dy / pointDistance * outerRadius / 6.0).toFloat()
        canvas.drawLine(
            startPoint.centerX + rx,
            startPoint.centerY + ry,
            endPoint.centerX - rx,
            endPoint.centerY - ry,
            linePaint
        )

    }


    //手指按下的位置
    private var moveX = 0f
    private var moveY = 0f
    override fun onTouchEvent(event: MotionEvent): Boolean {
        moveX = event.x
        moveY = event.y
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                //判断手指是否是在9个点里面
                if (point != null) {
                    isTouchPoint = true
                    selectPoints.add(point!!)
                    point!!.setStatusPress()
                }


            }
            MotionEvent.ACTION_MOVE -> {
                if (isTouchPoint) {
                    //按下的时候一定要先选中一个点,手指不断移动的时候,不断去找其他的点
                    if (point != null) {
                        if (!selectPoints.contains(point)) {
                            selectPoints.add(point!!)
                        }
                        point!!.setStatusPress()
                    }
                }
            }
            MotionEvent.ACTION_UP -> {
                isTouchPoint = false
                //判断密码是否与设置密码一致
                checkPasswordRight()
            }
        }
        invalidate()
        return true
    }

    /**
     * 检查密码是否正确
     */
    private fun checkPasswordRight() {
        Log.e("TAG", "${getSelectPassword()} --> $setPassword")
        if (getSelectPassword() == setPassword) {
            lockPatternCallBack?.checkSuccess()

        } else {
            lockPatternCallBack?.checkError()
            for (i in 0 until selectPoints.size) {
                selectPoints[i].setStatusError()
            }
            invalidate()
            delayResetStatus()

        }

    }

    /**
     * 延迟重置状态
     */
    private fun delayResetStatus() {
        handler.postDelayed(
            {
                for (i in 0 until selectPoints.size) {
                    selectPoints[i].setStatusNormal()
                }
                selectPoints.clear()
                invalidate()
            }, 1000
        )
    }


    /**
     * 获取按下的点
     */
    private val point: Point?
        get() {
            for (i in 0..2) {
                for (point in points[i]) {
                    //for循环 判断手指是否在9个点里面
                    if (MathUtil.checkInRound(
                            point!!.centerX, point!!.centerY, outerRadius, moveX, moveY
                        )
                    ) {
                        return point
                    }
                }
            }
            return null
        }

    /**
     * 各个圆心坐标点
     * @centerX 中心点横坐标
     * @centerY 中心点纵坐标
     * index 点对应的下标 用于后续密码校验
     */
    private inner class Point(var centerX: Float, var centerY: Float, var index: Int) {
        //默认状态
        private val STATUS_NORMAL = 1

        //选中状态
        private val STATUS_PRESS = 2

        //错误状态
        private val STATUS_ERROR = 3

        private var status = STATUS_NORMAL

        /**
         * 设置为选中状态
         */
        fun setStatusPress() {
            status = STATUS_PRESS
        }

        /**
         * 设置为选中状态
         */
        fun setStatusNormal() {
            status = STATUS_NORMAL
        }

        /**
         * 设置为选中状态
         */
        fun setStatusError() {
            status = STATUS_ERROR
        }

        /**
         * 是否为选中状态
         */
        fun isPressStatus(): Boolean {
            return status == STATUS_PRESS
        }

        fun isNormalStatus(): Boolean {
            return status == STATUS_NORMAL
        }


        fun isErrorStatus(): Boolean {
            return status == STATUS_ERROR
        }

    }

    /**
     * 设置默认密码
     */
    fun setDefaultPassWord(password: String) {
        setPassword = password
    }

    /**
     * 获取选中的密码
     */
    private fun getSelectPassword(): String {
        var pass = ""
        for (i in 0 until selectPoints.size) {
            pass += selectPoints[i].index
        }
        return pass
    }

    private var lockPatternCallBack: LockPatternCallBack? = null

    fun setLockPatternCallBack(lockPatternCallBack: LockPatternCallBack) {
        this.lockPatternCallBack = lockPatternCallBack
    }

    /**
     * 密码检查回调
     */
    interface LockPatternCallBack {
        fun checkSuccess()

        fun checkError()
    }


}
工具类
package com.crystal.view;

public class MathUtil {
	/**
	 * 
	 * @param x1
	 * @param y1
	 * @param x2
	 * @param y2
	 * @return
	 */
	public static double distance(double x1, double y1, double x2, double y2) {
		return Math.sqrt(Math.abs(x1 - x2) * Math.abs(x1 - x2)
				+ Math.abs(y1 - y2) * Math.abs(y1 - y2));
	}

	/**
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public static double pointTotoDegrees(double x, double y) {
		return Math.toDegrees(Math.atan2(x, y));
	}
	
	public static boolean checkInRound(float sx, float sy, float r, float x,
			float y) {
		// x的平方 + y的平方 开根号 < 半径
		return Math.sqrt((sx - x) * (sx - x) + (sy - y) * (sy - y)) < r;
	}
}

总结

实现九宫格密码解锁效果,进一步学习了事件分发的处理,自定义View的能力得到进一步的提升。

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

你可能感兴趣的:(自定义view学习,kotlin,android)