<?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>
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()
}
})
}
}
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的能力得到进一步的提升。
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )