前言
目前大部分APP
的登录方式有多种类型,其中手势解锁就是其中比较常见的一种方式,经常使用的招商银行APP(IOS
)端的手势解锁体验不错的,就仿照它自定义下手势解锁功能。
说明
1、招行APP手势解锁效果
2、绘制分析
来分析下效果图1和图2中需要绘制的元素。
- 未执行解锁操作,需要绘制9个灰色小圆点来形成锁盘
- 执行解锁操作,绘制大圆、黑色小圆点、圆之间的连线以及圆到手指所在位置的连线
- 松手后重置绘制,校验密码是否正确
上面分析了需要绘制的元素,那么元素的绘制条件有哪些?
- 灰色小圆不需要触摸条件,默认要绘制9个点
- 元素之间的位置由大圆的半径
outerRadius
、横向间距landsMargin
、纵向间距vertMargin
决定 - 当手指所在范围位于
A、B
时,触发大圆以及中间黑色的圆绘制。 C
是手指当前所在的位置
再细化下手势解锁的业务逻辑,首先,没有手势操作时,绘制9个灰色小圆作为锁屏面板。
假设,当手指在锁屏面板按下坐标在A
或者B
的范围内,触发绘制大圆以及黑色的小圆(这是触发大圆绘制的条件),这种状态暂且叫它选中状态。
且以⑦
为例,当手指再在⑦内**按下**,滑动手指到C,此时以圆
⑦为中心绘制到
C`的直线。
若是手指滑动的过程中坐标在⑦
范围内,将⑧
和⑦
的中心直线连接起来,再连接到C
。
在整个过程中,若是已经选中状态(如①、②、⑤、⑥
等),就不在进行连线处理,这里可以使用一个集合来管理选中的元素。
实现
分析了整个View
绘制的要素,下面就来实现它,首先绘制的是解锁面板。
class PatternLockView(context: Context,attributeSet: AttributeSet):View(context) {
private var dWidth = 0
private var dHeight = 0
private var ctx = context
//上下内边距,优化最上和最下圆显示不全问题
private val vertPadding = 5f
//纵向间距
private val vertMargin = 200f
//横向间距
private val landsMargin = 180f
//小圆半径
private val innerRadius = 20f
//大圆半径
private val outerRadius = 60f
//小圆画笔
private var innerPaint = Paint().apply {
style = Paint.Style.FILL
color = context.getColor(R.color.colorLightGrey)
isAntiAlias = true
isDither = true
}
//大圆画笔
private var outerPaint = Paint().apply {
style = Paint.Style.STROKE
color = context.getColor(R.color.colorBlack)
isAntiAlias = true
isDither = true
}
//直线画笔
private var linePaint = Paint().apply {
style = Paint.Style.FILL
color = context.getColor(R.color.colorBlack)
isAntiAlias = true
isDither = true
strokeWidth = 10f
strokeCap = Paint.Cap.ROUND
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
dWidth = w
dHeight = h
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
//绘制解锁面板
drawPatternLock(this)
//解锁过程-绘制大圆
drawGestureUnlock(this)
//解锁过程-绘制直线
if(isMove){ drawUnlockPath(this) }
}
}
/**
* 绘制9个灰色小圆点、大圆和黑色小圆
*/
private fun drawPatternLock(canvas: Canvas) {
innerPaint.color = context.getColor(R.color.colorLightGrey)
var level = 0
var rNum = 0
for(i in 0..8){
//横向-对应圆①、圆②、圆③
if(i in 0..2){
level = 1
rNum = 1
}
//横向-对应圆④、圆⑤、圆⑥
if(i in 3..5){
level = 2
rNum = 3
}
//横向-对应圆⑦、圆⑧、圆⑨
if(i in 6..8){
level = 3
rNum = 5
}
//纵向-对应圆①、圆④、圆⑦
if(i % 3 == 0){
canvas.drawCircle(outerRadius + landsMargin,vertPadding + rNum * outerRadius + (level - 1) * vertMargin,innerRadius,innerPaint)
}
//纵向-对应圆②、圆⑤、圆⑧
if(i % 3 == 1){
canvas.drawCircle(dWidth / 2f,vertPadding + rNum * outerRadius + (level - 1) * vertMargin,innerRadius,innerPaint)
}
//纵向-对应圆③、圆⑥、圆⑨
if(i % 3 == 2){
canvas.drawCircle(dWidth - landsMargin - outerRadius,vertPadding + rNum * outerRadius + (level - 1) * vertMargin,innerRadius,innerPaint)
}
}
}
}
绘制小圆还是比较简单的,只要计算好各个圆之间的位置,横向上的元素纵坐标相同,纵向上的元素横坐标相同,处理下即可绘制。下面着重介绍下手势解锁过程中的绘制。
看到上面onDraw方法中已经调用绘制解锁drawGestureUnlock
和绘制解锁路线drawUnlockPath
方法,在分析两个方法之前,我们要考虑两个问题:
1、既然是手势解锁,手势如何生成密码,密码如何管理
2、绘制手势路径
//手指当前的x坐标
private var moveX = 0f
//手指当前的y坐标
private var moveY = 0f
//当前手指是否是移动状态
private var isMove = false
//密码管理集合
private var pwList:ArrayList<Int> = ArrayList()
//密码管理临时集合,用于生成密码字符串
private var pwTempList:ArrayList<Int> = ArrayList()
//用于管理绘制路径坐标点的集合
private var pointList:ArrayList<PointF> = ArrayList()
override fun onTouchEvent(event: MotionEvent): Boolean {
when(event.action){
MotionEvent.ACTION_DOWN ->{
//手指按下的x坐标
val downX = event.x
//手指按下的y坐标
val downY = event.y
checkPressPos(downX,downY)
//手指不是移动状态
isMove = false
}
MotionEvent.ACTION_MOVE ->{
//手指移动x坐标
moveX = event.x
//手指移动y坐标
moveY = event.y
//手指移动的标准是正切大于6个像素点
if(sqrt(abs(moveX).pow(2) + abs(moveY).pow(2)) > 6f){
isMove = true
checkPressPos(moveX,moveY)
}
}
MotionEvent.ACTION_UP ->{
//手指不是移动状态
isMove = false
//点集合置空
pointList.clear()
//临时密码管理集合新增密码
pwTempList.clear()
pwTempList.addAll(pwList)
//密码管理集合置空
pwList.clear()
invalidate()
checkPassword()
}
}
return true
}
/**
* 判断手指按下与滑动过程中位置是否在大圆内
*/
private fun checkPressPos(x: Float, y: Float) {
val point = PointF()
if(x > landsMargin && x < landsMargin + 2 * outerRadius && y > vertPadding && y < vertPadding + 2 * outerRadius){
//判断是坐标点否在圆①内
if(!pwList.contains(1)){
pwList.add(1)
point.x = outerRadius + landsMargin
point.y = outerRadius + vertPadding
pointList.add(point)
}
}else if(x > dWidth / 2 - outerRadius && x < dWidth / 2 + outerRadius && y > vertPadding && y < vertPadding + 2 * outerRadius){
//判断是坐标点否在圆②内
if(!pwList.contains(2)){
pwList.add(2)
point.x = dWidth / 2f
point.y = outerRadius + vertPadding
pointList.add(point)
}
}else if(x > dWidth - landsMargin - 2 * outerRadius && x < dWidth - landsMargin && y > vertPadding && y < vertPadding + 2 * outerRadius){
//判断是坐标点否在圆③内
if(!pwList.contains(3)){
pwList.add(3)
point.x = dWidth - landsMargin - outerRadius
point.y = outerRadius + vertPadding
pointList.add(point)
}
} else if(x > landsMargin && x < landsMargin + 2 * outerRadius && y > vertPadding + vertMargin + 2 * outerRadius && y < vertPadding + vertMargin + 4 * outerRadius){
//判断是坐标点否在圆④内
if(!pwList.contains(4)){
pwList.add(4)
point.x = landsMargin + outerRadius
point.y = vertPadding + vertMargin + 3 * outerRadius
pointList.add(point)
}
}else if(x > dWidth / 2 - outerRadius && x < dWidth / 2 + outerRadius && y > vertPadding + vertMargin + 2 * outerRadius && y < vertPadding + vertMargin + 4 * outerRadius){
//判断是坐标点否在圆⑤内
if(!pwList.contains(5)){
pwList.add(5)
point.x = dWidth / 2f
point.y = vertPadding + vertMargin + 3 * outerRadius
pointList.add(point)
}
}else if(x > dWidth - landsMargin - 2 * outerRadius && y > vertPadding + vertMargin + 2 * outerRadius && y < vertPadding + vertMargin + 4 * outerRadius){
//判断是坐标点否在圆⑥内
if(!pwList.contains(6)){
pwList.add(6)
point.x = dWidth - landsMargin - outerRadius
point.y = vertPadding + vertMargin + 3 * outerRadius
pointList.add(point)
}
}else if(x > landsMargin && x < landsMargin + 2 * outerRadius && y > vertPadding + 2 * vertMargin + 4 * outerRadius && y < vertPadding + 2 * vertMargin + 6 * outerRadius){
//判断是坐标点否在圆⑦内
if(!pwList.contains(7)){
pwList.add(7)
point.x = landsMargin + outerRadius
point.y = vertPadding + 2 * vertMargin + 5 * outerRadius
pointList.add(point)
}
}else if(x > dWidth / 2 - outerRadius && x < dWidth / 2 + outerRadius && y > vertPadding + 2 * vertMargin + 4 * outerRadius && y < vertPadding + 2 * vertMargin + 6 * outerRadius){
//判断是坐标点否在圆⑧内
if(!pwList.contains(8)){
pwList.add(8)
point.x = dWidth / 2f
point.y = vertPadding + 2 * vertMargin + 5 * outerRadius
pointList.add(point)
}
}else if(x > dWidth - landsMargin - 2 * outerRadius && y > vertPadding + 2 * vertMargin + 4 * outerRadius && y < vertPadding + 2 * vertMargin + 6 * outerRadius){
//判断是坐标点否在圆⑨内
if(!pwList.contains(9)){
pwList.add(9)
point.x = dWidth - landsMargin - outerRadius
point.y = vertPadding + 2 * vertMargin + 5 * outerRadius
pointList.add(point)
}
}
invalidate()
}
/**
* 手势滑动解锁过程-绘制大圆和黑色小圆
*/
private fun drawGestureUnlock(canvas: Canvas) {
innerPaint.color = context.getColor(R.color.colorBlack)
//密码集合包含圆①,绘制对应的大圆和黑色小圆
if(pwList.contains(1)){
canvas.drawCircle(outerRadius + landsMargin,vertPadding + 1 * outerRadius,innerRadius,innerPaint)
canvas.drawCircle(outerRadius + landsMargin,vertPadding + 1 * outerRadius,outerRadius,outerPaint)
}
//密码集合包含圆②,绘制对应的大圆和黑色小圆
if(pwList.contains(2)){
canvas.drawCircle(dWidth / 2f,vertPadding + 1 * outerRadius,outerRadius,outerPaint)
canvas.drawCircle(dWidth / 2f,vertPadding + 1 * outerRadius,innerRadius,innerPaint)
}
//密码集合包含圆③,绘制对应的大圆和黑色小圆
if(pwList.contains(3)){
canvas.drawCircle(dWidth - landsMargin - outerRadius,vertPadding + 1 * outerRadius,outerRadius,outerPaint)
canvas.drawCircle(dWidth - landsMargin - outerRadius,vertPadding + 1 * outerRadius,innerRadius,innerPaint)
}
//密码集合包含圆④,绘制对应的大圆和黑色小圆
if(pwList.contains(4)){
canvas.drawCircle(outerRadius + landsMargin,vertPadding + vertMargin + 3 * outerRadius,innerRadius,innerPaint)
canvas.drawCircle(outerRadius + landsMargin,vertPadding + vertMargin + 3 * outerRadius,outerRadius,outerPaint)
}
//密码集合包含圆⑤,绘制对应的大圆和黑色小圆
if(pwList.contains(5)){
canvas.drawCircle(dWidth / 2f,vertPadding + vertMargin + 3 * outerRadius,innerRadius,innerPaint)
canvas.drawCircle(dWidth / 2f,vertPadding + vertMargin + 3 * outerRadius,outerRadius,outerPaint)
}
//密码集合包含圆⑥,绘制对应的大圆和黑色小圆
if(pwList.contains(6)){
canvas.drawCircle(dWidth - landsMargin - outerRadius,vertPadding + vertMargin + 3 * outerRadius,innerRadius,innerPaint)
canvas.drawCircle(dWidth - landsMargin - outerRadius,vertPadding + vertMargin + 3 * outerRadius,outerRadius,outerPaint)
}
//密码集合包含圆⑦,绘制对应的大圆和黑色小圆
if(pwList.contains(7)){
canvas.drawCircle(outerRadius + landsMargin,vertPadding + 2 * vertMargin + 5 * outerRadius,innerRadius,innerPaint)
canvas.drawCircle(outerRadius + landsMargin,vertPadding + 2 * vertMargin + 5 * outerRadius,outerRadius,outerPaint)
}
//密码集合包含圆⑧,绘制对应的大圆和黑色小圆
if(pwList.contains(8)){
canvas.drawCircle(dWidth / 2f,vertPadding + 2 * vertMargin + 5 * outerRadius,innerRadius,innerPaint)
canvas.drawCircle(dWidth / 2f,vertPadding + 2 * vertMargin + 5 * outerRadius,outerRadius,outerPaint)
}
//密码集合包含圆⑨,绘制对应的大圆和黑色小圆
if(pwList.contains(9)){
canvas.drawCircle(dWidth - landsMargin - outerRadius,vertPadding + 2 * vertMargin + 5 * outerRadius,innerRadius,innerPaint)
canvas.drawCircle(dWidth - landsMargin - outerRadius,vertPadding + 2 * vertMargin + 5 * outerRadius,outerRadius,outerPaint)
}
}
/**
* 绘制解锁连线
*/
private fun drawUnlockPath(canvas: Canvas) {
for(i in 0 until pointList.size){
//密码长度为1
if(pointList.size == 1){
canvas.drawLine(pointList[0].x,pointList[0].y,moveX,moveY,linePaint)
}
//处理最后一个点
if(i == pointList.size - 1){
canvas.drawLine(pointList[i].x,pointList[i].y,moveX,moveY,linePaint)
}else{
//连接所有的点
canvas.drawLine(pointList[i].x,pointList[i].y,pointList[i+1].x,pointList[i+1].y,linePaint)
}
}
}
手势操作的三个阶段:
-
手指按下: 手势操作触发
onTouchEvent
,当手指按下时调用checkPressPos
校验当前手指按下的位置在不在9个大圆的坐标范围内,如果在范围内,将圆对应的标识1-9
添加到密码管理集合pwList
中,将选中的圆的圆心坐标pointF
添加到pointList
中,后期用于绘制连线path
,是否移动标志位isMove
置为false
,表示非移动状态。 -
手指滑动: 当手指在锁盘上滑动时也调用
checkPressPos
,所以在方法中增加了判断,如果手指滑动选中的圆已经被选中了,那么将不再重复添加到集合中,是否移动标志位isMove
标志位置为true
。 -
手指抬起: 当手指抬起时,校验密码,清空两个集合数据后调用
invalidate()
触发系统重新绘制。
校验密码逻辑相对简单,这里使用pwTempList
临时集合来生成密码字符串,在手指抬起时候pwList
已经清空,需要重新绘制UI
。
/**
* 校验手势密码
*/
private fun checkPassword() {
if(pwTempList.size in 1..5){
ctx.toast(ctx.getString(R.string.at_least_6_dots),Toast.LENGTH_SHORT)
return
}
if(pwTempList.size >= 6){
val sb = StringBuilder()
pwTempList.forEach { sb.append(it) }
if(sb.toString() == "1478965"){
ctx.toast(ctx.getString(R.string.pw_correct),Toast.LENGTH_SHORT)
}else{
ctx.toast(ctx.getString(R.string.pw_error),Toast.LENGTH_SHORT)
}
}
}
如果连接点不超过6个提示最少6个点,如果超过6个点则生成密码字符串进行对比,我们看下自定义后的效果。
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<com.ho.customview.widget.PatternLockView
android:layout_width="match_parent"
android:layout_height="342dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
结尾
新年快乐~