android自定义View: 九宫格解锁
Posted 史大拿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android自定义View: 九宫格解锁相关的知识,希望对你有一定的参考价值。
本系列自定义View全部采用kt
系统:mac
android studio: 4.1.3
kotlin version1.5.0
gradle: gradle-6.5-bin.zip
废话不多说,先来看今天要完成的效果:
3X3 (样式1) | 4*4(样式2) | 5*5(样式3) |
---|---|---|
Tips:不止3X3 或者 5X5 ,如果你想,甚至可以设置10*10
画圆
先以3*3的九宫格来介绍!
我们要画成这样的效果, 画的是有一点丑,但是没关系.
首先来分析一下怎么花,这9个点的位置如何确定:
- 我们为了平均分, 单个圆的外层矩形 宽 = view.width / 3
- 高 = 宽
- 1号圆的圆心位置 = 0个矩形的宽度 = view.width / (3 * 2) + ( view.width / 3 ) * 0
- 2号圆的圆心位置 = 1号圆的圆心位置 + 1个矩形的宽度 = view.width / (3 * 2) + (view.width / 3) * 1
- 3号圆的圆心位置 = 1号圆的圆心位置 + 2个矩形的宽度 = view.width / (3 * 2) + (view.width / 3) * 2
高坐标的计算也是如此
来看看目前的代码:
class BlogUnLockView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply
//
strokeJoin = Paint.Join.BEVEL
// 大圆半径
private val bigRadius by lazy width / (NUMBER * 2) * 0.7f
// 小圆半径
private val smallRadius by lazy bigRadius * 0.2f
companion object
const val NUMBER = 3
private val unLockPoints = arrayListOf<ArrayList<UnLockBean>>()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int)
super.onSizeChanged(w, h, oldw, oldh)
// 矩形直径
val diameter = width / NUMBER
//
val ratio = (NUMBER * 2f)
var index = 1
// 循环每一行行
for (i in 0 until NUMBER)
val list = arrayListOf<UnLockBean>()
// 循环每一列
for (j in 0 until NUMBER)
list.add(
UnLockBean(
width / ratio + diameter * j,
height / ratio + diameter * i,
index++
)
)
unLockPoints.add(list)
override fun onDraw(canvas: Canvas)
canvas.drawColor(Color.YELLOW)
unLockPoints.forEach
it.forEach data ->
// 绘制大圆
paint.alpha = (255 * 0.6).toInt()
canvas.drawCircle(data.x, data.y, bigRadius, paint)
// 绘制小圆
paint.alpha = 255
canvas.drawCircle(data.x, data.y, smallRadius, paint)
当前效果:
目前问题:
- 整个view占满了屏幕,需要测量
测量代码比较简单,就是让宽和高一样即可
此时改变number变量,就可以设置几行几列:
例如这样:
5*5 | 10*10 |
---|---|
接下来我们就处理手势事件,按下滑动,抬起等,来改变选中
onTouchEvent事件处理
在事件处理之前先来分析一下需要几种事件,对于解锁功能来说:
- ORIGIN 刚开始,还没有触摸
- DOWN 正在触摸中(输入密码)
- UP 触摸结束 (输入密码正确)
- ERROR 触摸结束 (输入密码错误)
那么就先定义4种颜色,来表示这4种状态:
companion object
// 原始颜色
private var ORIGIN_COLOR = Color.parseColor("#D8D9D8")
// 按下颜色
private var DOWN_COLOR = Color.parseColor("#3AD94E")
// 抬起颜色
private var UP_COLOR = Color.parseColor("#57D900")
// 错误颜色
private var ERROR_COLOR = Color.parseColor("#D9251E")
接下来挨个处理事件
DOWN(按下)
首先需要思考,在按下的时候要做什么事情:
- 判断是否选中
/*
* TODO 判断是否选中某个圆
* @param x,y: 点击坐标位置
*/
private fun isContains(x: Float, y: Float) = let
unLockPoints.forEach
it.forEach data ->
// 循环所有坐标 判断两个位置是否相同
if (PointF(x, y).contains(PointF(data.x, data.y), bigRadius))
return@let data
return@let null
// 判断一个点是否在另一个点范围内
fun PointF.contains(b: PointF, bPadding: Float = 0f): Boolean
val isX = this.x <= b.x + bPadding && this.x >= b.x - bPadding
val isY = this.y <= b.y + bPadding && this.y >= b.y - bPadding
return isX && isY
思路: 通过比较 按下位置和所有位置,判断是否有相同的
- 如果有相同的,那么就返回对应坐标
- 如果没有相同的,那么就返回null
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean
when (event.action)
MotionEvent.ACTION_DOWN ->
// 判断是否选中
val pointF = isContains(event.x, event.y)
pointF?.let
// 将当前类型变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
...
invalidate()
return true
override fun onDraw(canvas: Canvas)
// canvas.drawColor(Color.YELLOW)
unLockPoints.forEach
it.forEach data ->
// 根据类型设置颜色
paint.color = getTypeColor(data.type)
// 绘制大圆
paint.alpha = (255 * 0.6).toInt()
canvas.drawCircle(data.x, data.y, bigRadius, paint)
// 绘制小圆
paint.alpha = 255
canvas.drawCircle(data.x, data.y, smallRadius, paint)
/// TODO 获取类型对应颜色
private fun getTypeColor(type: JiuGonGeUnLockView.Type): Int
return when (type)
JiuGonGeUnLockView.Type.ORIGIN -> ORIGIN_COLOR
JiuGonGeUnLockView.Type.DOWN -> DOWN_COLOR
JiuGonGeUnLockView.Type.UP -> UP_COLOR
JiuGonGeUnLockView.Type.ERROR -> ERROR_COLOR
当前效果:
MOVE(移动)
move事件和down事件的逻辑是一样的,滑动的过程中判断点是否选中,然后绘制点
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean
when (event.action)
MotionEvent.ACTION_DOWN ->
val pointF = isContains(event.x, event.y)
pointF?.let
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
MotionEvent.ACTION_MOVE ->
val pointF = isContains(event.x, event.y)
pointF?.let
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
....
invalidate()
return true
当前效果:
可以看出,效果是基本完成了,但是还有一个小错误
通常我们在九宫格的时候,一般都是先按下一个点才能滑动, 否则是不能滑动的,
现在的问题是,直接就可以滑动,所以还需要调整一下
那么我们就需要在down事件中标记一下是否按下,然后在move事件中判断一下
// 是否按下
private var isDOWN = false
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean
when (event.action)
MotionEvent.ACTION_DOWN ->
val pointF = isContains(event.x, event.y)
pointF?.let
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
isDOWN = true // 表示按下
MotionEvent.ACTION_MOVE ->
if (!isDOWN)
return super.onTouchEvent(event)
val pointF = isContains(event.x, event.y)
pointF?.let
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP ->
isDOWN = false // 标记没有按下
invalidate()
return true
此时效果:
UP(抬起)
思路分析:
抬起的时候要做很多事情
-
判断输入密码是否正确
- 密码输入正确,那么就改变为深绿色
- 密码输入错误,就改变为红色
-
完成之后,还需要吧所有的状态清空
在这里的时候,先不判断密码是否成功, 默认都是成功的,
- 先吧输入的密码toast出来
- 并且吧状态清空
等结尾的时候再来判断密码.
那么此时肯定是需要将所有选中的都记录下来, 然后在up事件中操作即可
// 记录选中的坐标
private val recordList = arrayListOf<UnLockBean>()
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean
when (event.action)
MotionEvent.ACTION_DOWN ->
val pointF = isContains(event.x, event.y)
pointF?.let
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
isDOWN = true
recordList.add(it)
MotionEvent.ACTION_MOVE ->
if (!isDOWN)
return super.onTouchEvent(event)
val pointF = isContains(event.x, event.y)
pointF?.let
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
// 这里会重复调用,所以需要判断是否包含,如果不包含才添加
if (!recordList.contains(it))
recordList.add(it)
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP ->
// 将结果打印
recordList.map
it.index
.toList() toast context
clear()
invalidate()
return true
/// 清空所有状态
private fun clear()
recordList.forEach
// 将所有选中状态还原
it.type = JiuGonGeUnLockView.Type.ORIGIN
recordList.clear()
isDOWN = false // 标记没有按下
invalidate()
当前效果:
画连接线
还是以这张图来说:
假设现在需要连接 1,5,6,9
那么可以通过Path()来画线
在DOWN事件中,通过moveTo()移动到1的位置
在MOVE事件中,通过lineTo()画5,6,9的位置 即可
private val path = Path()
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean
when (event.action)
MotionEvent.ACTION_DOWN ->
val pointF = isContains(event.x, event.y)
pointF?.let
/// 隐藏部分代码
path.moveTo(it.x, it.y)
MotionEvent.ACTION_MOVE ->
val pointF = isContains(event.x, event.y)
pointF?.let
/// 隐藏部分代码
// 这里会重复调用,所以需要判断是否包含,如果不包含才添加
if (!recordList.contains(it))
recordList.add(it)
path.lineTo(it.x, it.y) // 连接到移动的位置
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP ->
// 将结果打印
recordList.map
it.index
.toList() toast context
clear()
invalidate()
return true
/*
* 作者:史大拿
* 创建时间: 9/14/22 1:38 PM
* TODO 用来清空标记
*/
private fun clear()
path.reset() // 重置
recordList.forEach
// 将所有选中状态还原
it.type = JiuGonGeUnLockView.Type.ORIGIN
recordList.clear()
isDOWN = false // 标记没有按下
override fun onDraw(canvas: Canvas)
paint.style = Paint.Style.FILL
unLockPoints.forEach
/// 隐藏部分代码
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp
paint.color = DOWN_COLOR // 默认按下颜色
canvas.drawPath(path, paint)
当前效果:
可以看出,已经完成了画连接线,但是还缺少一条指示当前手指位置的线,
我叫他移动线, (好土的名字)
移动线就2个坐标
- 开始位置 (最后一个选中的位置)
- 结束位置 (当前手指按下的位置)
private val line = Pair(PointF(), PointF())
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean
when (event.action)
MotionEvent.ACTION_DOWN ->
val pointF = isContains(event.x, event.y)
pointF?.let
/// 隐藏代码
line.first.x = it.x
line.first.y = it.y
MotionEvent.ACTION_MOVE ->
val pointF = isContains(event.x, event.y)
pointF?.let
if (!recordList.contains(it))
隐藏代码
// 最后一个选中的位置
line.first.x = it.x
line.first.y = it.y
// 手指的位置
line.second.x = event.x
line.second.y = event.y
....
invalidate()
return true
override fun onDraw(canvas: Canvas)
paint.style = Paint.Style.FILL
unLockPoints.forEach
/// 隐藏代码
// 绘制连接线
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp
paint.color = DOWN_COLOR // 默认按下颜色
canvas.drawPath(path, paint)
// 绘制移动线
if (line.first.x != 0f && line.second.x != 0f
)
canvas.drawLine(
line.first.x,
line.first.y,
line.second.x,
line.second.y,
paint
)
当前效果:
此时效果就差不多了,画笔默认是实心圆, 来看看空心效果
空心效果
空心效果很简单,只需要调整画笔的style即可
override fun onDraw(canvas: Canvas)
// 实心效果
// paint.style = Paint.Style.FILL
// 空心效果
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp
// canvas.drawXXX()
当前效果
可以看出,此时的效果和我们想的一样,但是画线的时候从小圆圆心穿过了,不太好看
有没有一种办法,让线不从圆心穿过
那么就先来分析一下:
假设现在是从7移动到2
那么就需要连接C点和F点,只需要计算出C点和F点的坐标即可
先来分析现在的已知条件:
- dx = end.x - start.x
- dy = end.y - start.y
- d = (dx平方 + dy平方) 开根号
- 小圆半径 = smallRadius
那么就可以算出当前的偏移量:
- offsetX = dx * (smallRadius / d)
- offsetY = dy * (smallRadius / d)
知道偏移量,就可以算出C和F的坐标:
那么C的坐标为:
- C.x = start.x + offsetX
- C.y = start.y + offsetY
那么F的坐标为:
- F.x = end.x + offsetX
- F.y = end.
以上是关于android自定义View: 九宫格解锁的主要内容,如果未能解决你的问题,请参考以下文章