Android自定义控件(八) Android仿招商银行APP手势解锁

2023-11-29 00:10

本文主要是介绍Android自定义控件(八) Android仿招商银行APP手势解锁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

目前大部分APP的登录方式有多种类型,其中手势解锁就是其中比较常见的一种方式,经常使用的招商银行APP(IOS)端的手势解锁体验不错的,就仿照它自定义下手势解锁功能。


说明

1、招行APP手势解锁效果


2、绘制分析

来分析下效果图1和图2中需要绘制的元素。

  1. 未执行解锁操作,需要绘制9个灰色小圆点来形成锁盘
  2. 执行解锁操作,绘制大圆、黑色小圆点、圆之间的连线以及圆到手指所在位置的连线
  3. 松手后重置绘制,校验密码是否正确

上面分析了需要绘制的元素,那么元素的绘制条件有哪些?

  • 灰色小圆不需要触摸条件,默认要绘制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 = 0private var dHeight = 0private 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.FILLcolor = context.getColor(R.color.colorLightGrey)isAntiAlias = trueisDither = true}//大圆画笔private var outerPaint = Paint().apply {style = Paint.Style.STROKEcolor = context.getColor(R.color.colorBlack)isAntiAlias = trueisDither = true}//直线画笔private var linePaint = Paint().apply {style = Paint.Style.FILLcolor = context.getColor(R.color.colorBlack)isAntiAlias = trueisDither = truestrokeWidth = 10fstrokeCap = Paint.Cap.ROUND}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)dWidth = wdHeight = 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 = 0var rNum = 0for(i in 0..8){//横向-对应圆①、圆②、圆③if(i in 0..2){level = 1rNum = 1}//横向-对应圆④、圆⑤、圆⑥if(i in 3..5){level = 2rNum = 3}//横向-对应圆⑦、圆⑧、圆⑨if(i in 6..8){level = 3rNum = 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.ycheckPressPos(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 = truecheckPressPos(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 + landsMarginpoint.y = outerRadius + vertPaddingpointList.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 / 2fpoint.y = outerRadius + vertPaddingpointList.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 -  outerRadiuspoint.y = outerRadius + vertPaddingpointList.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 + outerRadiuspoint.y = vertPadding + vertMargin + 3 * outerRadiuspointList.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 / 2fpoint.y = vertPadding + vertMargin + 3 * outerRadiuspointList.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 -  outerRadiuspoint.y = vertPadding + vertMargin + 3 * outerRadiuspointList.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 + outerRadiuspoint.y = vertPadding + 2 * vertMargin + 5 * outerRadiuspointList.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 / 2fpoint.y = vertPadding + 2 * vertMargin + 5 * outerRadiuspointList.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 -  outerRadiuspoint.y = vertPadding + 2 * vertMargin + 5 * outerRadiuspointList.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){//密码长度为1if(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.PatternLockViewandroid: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>

结尾

新年快乐~

这篇关于Android自定义控件(八) Android仿招商银行APP手势解锁的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/430729

相关文章

C#实现WinForm控件焦点的获取与失去

《C#实现WinForm控件焦点的获取与失去》在一个数据输入表单中,当用户从一个文本框切换到另一个文本框时,需要准确地判断焦点的转移,以便进行数据验证、提示信息显示等操作,本文将探讨Winform控件... 目录前言获取焦点改变TabIndex属性值调用Focus方法失去焦点总结最后前言在一个数据输入表单

macOS怎么轻松更换App图标? Mac电脑图标更换指南

《macOS怎么轻松更换App图标?Mac电脑图标更换指南》想要给你的Mac电脑按照自己的喜好来更换App图标?其实非常简单,只需要两步就能搞定,下面我来详细讲解一下... 虽然 MACOS 的个性化定制选项已经「缩水」,不如早期版本那么丰富,www.chinasem.cn但我们仍然可以按照自己的喜好来更换

React实现原生APP切换效果

《React实现原生APP切换效果》最近需要使用Hybrid的方式开发一个APP,交互和原生APP相似并且需要IM通信,本文给大家介绍了使用React实现原生APP切换效果,文中通过代码示例讲解的非常... 目录背景需求概览技术栈实现步骤根据 react-router-dom 文档配置好路由添加过渡动画使用

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

SpringBoot 自定义消息转换器使用详解

《SpringBoot自定义消息转换器使用详解》本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,感兴趣的朋友一起看看吧... 目录一、前言二、SpringBoot 内容协商介绍2.1 什么是内容协商2.2 内容协商机制深入理解2.2.1 内容

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s