Android九宫格手势密码
xinhengqq 人气:0介绍下自己编写的九宫格手势密码。先见图
思路:首先是9个格子,接着是格子连线;那么我们的步骤就有了。
1.手势监听,进行连线
2.格子的状态未连接(初始状态)、已连接的(没有结果前)、错误状态(有结果后)。(先这三个,可扩展,比如按下状态)
3.自定义viewgroup作为九宫格的容器,里面包含9个view(小格子)
一、先从简单的说起吧,9个小格子以及状态
为了扩展性,不自定义view,将三个状态和有关属性提取
1.提取属性,代码如下:
class NineChildInf { /** * 当前所在9宫格的位置 * 从1开始 */ var index = 0 /** * 是否被点亮 */ var isLight = false /** * 中心点所在父类容器内的坐标 */ var centerX = 0.toFloat() var centerY = 0.toFloat() fun setContent(index: Int, centerX: Float, centerY: Float) { this.index = index this.centerX = centerX this.centerY = centerY } constructor() fun updateCenterPoint(x: Float, y: Float) { this.centerX = x this.centerY = y } fun reset() { this.index = 0 this.centerX = 0f this.centerY = 0f this.isLight = false } override fun toString(): String { return "NineChildInf(index=$index, isLight=$isLight, centerX=$centerX, centerY=$centerY)" } }
2.三个状态,代码如下
/** * Created by XinHeng on 2019/02/27. * describe:9宫格子view必须实现此接口 */ abstract class NineChildParent<T : View>(var view: T) { protected open var context = view.context.applicationContext val NINE_CHILD_INF = NineChildInf() /** * 密码错误时的显示 */ abstract fun setErrorStatue() /** * 被选中时的显示 */ abstract fun setLightStatue() /** * 默认显示 */ abstract fun setDefaultStatue() }
二、自定义九宫格容器,NineViewGroup。
既然是九宫格,那自然少不了这些属性,水平间隔、垂直间隔、最小有效连接数、当前状态、密码是否设置完成等。还需要将开启viewgroup的onDraw()方法。具体代码如下:
class NineViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) { /** * 水平间的间隔 */ private var paddingH = 60 /** * 垂直间的间隔 */ private var paddingV = 60 /** * 连线最小有效数字 */ var minEffectiveSize = 4 /** * 小格子的宽高 */ private var childSlide: Int = 30 private val ERROR_STATUE = 2 private val LINKING_STATUE = 1 private val DEFAULT_STATUE = 0 /** * 当前状态 * 0->最初状态 DEFAULT_STATUE * 1->正在连线中 LINKING_STATUE * 2->错误状态 ERROR_STATUE */ private var nowStatue = DEFAULT_STATUE /** * 一次密码设置完成标志 */ private var complete = false /** * 线条宽度 */ private var lineWidth = 5 private var lineColor = Color.parseColor("#33b5e5") private var errorLineColor = Color.RED private var childViews = ArrayList<NineChildParent<*>>(9) init { //使能调用onDraw()方法 setWillNotDraw(false) var array = context.obtainStyledAttributes(attrs, R.styleable.NineViewGroup, defStyleAttr, 0) (0..array.indexCount).forEach { var index = array.getIndex(it) when (index) { R.styleable.NineViewGroup_nine_child_size -> childSlide = array.getDimensionPixelSize(index, childSlide) R.styleable.NineViewGroup_nine_line_color -> lineColor = array.getColor(index, lineColor) R.styleable.NineViewGroup_nine_error_line_color -> errorLineColor = array.getColor(index, errorLineColor) R.styleable.NineViewGroup_nine_effective_size -> minEffectiveSize = array.getInt(index, minEffectiveSize) R.styleable.NineViewGroup_nine_padding_h -> paddingH = array.getDimensionPixelSize(index, paddingH) R.styleable.NineViewGroup_nine_padding_v -> paddingV = array.getDimensionPixelSize(index, paddingV) R.styleable.NineViewGroup_nine_line_width -> lineWidth = array.getDimensionPixelSize(index, lineWidth) } } array.recycle() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { var width = childSlide * 3 + paddingLeft + paddingRight + paddingH * 2 var height = childSlide * 3 + paddingTop + paddingBottom + paddingV * 2 setMeasuredDimension(width, height) //又忘了计算子view的大小了。。。 measureChildren(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { var childView: View var top: Int = paddingTop var left: Int = paddingLeft var right: Int var bottom: Int if (childCount > 0) { (0 until childCount).forEach { childView = getChildAt(it) right = left + childView.measuredWidth bottom = top + childView.measuredHeight //Log.e("TAG", "onLayout: $left $top $right $bottom") var nineChildInf = (childViews[it]).NINE_CHILD_INF nineChildInf.setContent(it + 1, (left + right) / 2f, (top + bottom) / 2f) //Log.e("TAG", "onLayout: child=$nineChildInf") childView.layout(left, top, right, bottom) if ((it + 1) % 3 == 0) { left = paddingLeft top = bottom + paddingV } else { left = right + paddingH } } } } }
三、手势监听、连线
1.手势监听,重写onTouchEvent()方法,必要需要时重写onInterceptTouchEvent()方法进行拦截(跟情况而定,这里就不多说了)。简单的三个手势状态按下、移动、抬起。在各个状态下,记录坐标,并且更新子view(小格子)的ui,还有线条。代码片段如下:
override fun onTouchEvent(event: MotionEvent): Boolean { if (childCount == 0 || complete) { return super.onTouchEvent(event) } when (event.action) { MotionEvent.ACTION_DOWN -> { //记录落点 lastX = event.x lastY = event.y downUpdateChild(lastX, lastY) } MotionEvent.ACTION_MOVE -> { lastX = event.x lastY = event.y moveUpdateChild(lastX, lastY) } MotionEvent.ACTION_UP -> { complete = true //统计 upUpdateChild() } } return true }
2.连线,在容器的onDraw()方法,进行画线操作,代码片段如下:
override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (!showLine) { return } paint.color = when (nowStatue) { ERROR_STATUE -> errorLineColor else -> lineColor } if (points.size > 1) { (1 until points.size).forEach { var pointXYStart = points[it - 1].NINE_CHILD_INF var pointXYEnd = points[it].NINE_CHILD_INF canvas.drawLine(pointXYStart.centerX, pointXYStart.centerY, pointXYEnd.centerX, pointXYEnd.centerY, paint) } } if (lastX > 0 && points.size > 0) { var pointXY = points[points.size - 1].NINE_CHILD_INF canvas.drawLine(pointXY.centerX, pointXY.centerY, lastX, lastY, paint) } }
四、进行到这一步,大致的步骤就是这了。
但是还有一些细节:比如连线中需要判断中间是否含有小格子、判断触点是否在小格子上、连接完成后的回调、错误状态显示、恢复初始状态等。粘出部分代码片段(这些只是能实现效果,还可以优化,交给大家了):
1.判断触点是否在小格子上
private fun childContains(x: Float, y: Float): Boolean { (0 until childCount).forEach { var childAt = getChildAt(it) //这一句,循环判断,是否属于其范围 if (x >= childAt.left && x < childAt.right && y >= childAt.top && y < childAt.bottom) { return if (!childViews[it].NINE_CHILD_INF.isLight) { if (points.size > 0) { checkMiddleChild(points[points.size - 1], childViews[it])?.run { if (!NINE_CHILD_INF.isLight) { buffer.append(NINE_CHILD_INF.index) changeLightStatue(this) } } } buffer.append(it + 1) //TODO 改变子view的UI状态 changeLightStatue(childViews[it]) true } else { false } } } return false }
2.判断中间是否含有小格子
private fun checkMiddleChild(nineChildParent: NineChildParent<*>, nineChildParent1: NineChildParent<*>): NineChildParent<*>? { var index = nineChildParent.NINE_CHILD_INF.index var index1 = nineChildParent1.NINE_CHILD_INF.index var sum = index + index1 if (sum == 10) { return childViews[4] } else if (index % 2 != 0 && index1 % 2 != 0) { if ((sum == 4 || sum == 16) || (sum == 8 && (index == 1 || index1 == 1))||(sum == 12 && (index == 3 || index1 == 3))) return childViews[sum / 2 - 1] } return null }
五、如有bug欢迎留言指出,下面粘出九宫格容器的全部代码。
/** * Created by XinHeng on 2019/01/29. * describe:九宫格的容器 */ class NineViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) { /** * 水平间的间隔 */ private var paddingH = 60 /** * 垂直间的间隔 */ private var paddingV = 60 /** * 是否有第一个选中 */ private var firstSelect = true private val ERROR_STATUE = 2 private val LINKING_STATUE = 1 private val DEFAULT_STATUE = 0 /** * 是否显示线条 */ var showLine = false /** * 连线最小有效数字 */ var minEffectiveSize = 4 /** * 当前状态 * 0->最初状态 DEFAULT_STATUE * 1->正在连线中 LINKING_STATUE * 2->错误状态 ERROR_STATUE */ private var nowStatue = DEFAULT_STATUE /** * 一次密码设置完成标志 */ private var complete = false /** * 线条宽度 */ private var lineWidth = 5 private var lastX: Float = 0f private var lastY: Float = 0f private var buffer = StringBuilder() private var points = ArrayList<NineChildParent<*>>(9) private var childViews = ArrayList<NineChildParent<*>>(9) /** * 小格子的宽高 */ private var childSlide: Int = 30 private var lineColor = Color.parseColor("#33b5e5") private var errorLineColor = Color.RED var onNineViewGroupListener: OnNineViewGroupListener? = null set(value) { field = value value?.let { setChildMode(it) } } private val paint = Paint().apply { isAntiAlias = true isDither = true } init { //使能调用onDraw()方法 setWillNotDraw(false) var array = context.obtainStyledAttributes(attrs, R.styleable.NineViewGroup, defStyleAttr, 0) (0..array.indexCount).forEach { var index = array.getIndex(it) when (index) { R.styleable.NineViewGroup_nine_child_size -> childSlide = array.getDimensionPixelSize(index, childSlide) R.styleable.NineViewGroup_nine_line_color -> lineColor = array.getColor(index, lineColor) R.styleable.NineViewGroup_nine_error_line_color -> errorLineColor = array.getColor(index, errorLineColor) R.styleable.NineViewGroup_nine_effective_size -> minEffectiveSize = array.getInt(index, minEffectiveSize) R.styleable.NineViewGroup_nine_padding_h -> paddingH = array.getDimensionPixelSize(index, paddingH) R.styleable.NineViewGroup_nine_padding_v -> paddingV = array.getDimensionPixelSize(index, paddingV) R.styleable.NineViewGroup_nine_show_line -> showLine = array.getBoolean(index, showLine) R.styleable.NineViewGroup_nine_line_width -> lineWidth = array.getDimensionPixelSize(index, lineWidth) } } array.recycle() paint.strokeWidth = lineWidth.toFloat() } private fun setChildMode(onNineViewGroupListener: OnNineViewGroupListener) { removeAllViews() childViews.clear() (0..8).forEach { var mode = onNineViewGroupListener.getChildMode() mode.NINE_CHILD_INF.index = it + 1 mode.setDefaultStatue() addView(mode.view, getLp()) childViews.add(mode) } } private fun getLp(): LayoutParams { return LayoutParams(childSlide, childSlide) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { var width = childSlide * 3 + paddingLeft + paddingRight + paddingH * 2 var height = childSlide * 3 + paddingTop + paddingBottom + paddingV * 2 setMeasuredDimension(width, height) //又忘了计算子view的大小了。。。 measureChildren(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { var childView: View var top: Int = paddingTop var left: Int = paddingLeft var right: Int var bottom: Int if (childCount > 0) { (0 until childCount).forEach { childView = getChildAt(it) right = left + childView.measuredWidth bottom = top + childView.measuredHeight //Log.e("TAG", "onLayout: $left $top $right $bottom") var nineChildInf = (childViews[it]).NINE_CHILD_INF nineChildInf.setContent(it + 1, (left + right) / 2f, (top + bottom) / 2f) //Log.e("TAG", "onLayout: child=$nineChildInf") childView.layout(left, top, right, bottom) if ((it + 1) % 3 == 0) { left = paddingLeft top = bottom + paddingV } else { left = right + paddingH } } } } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { return true } override fun onTouchEvent(event: MotionEvent): Boolean { if (childCount == 0 || complete) { return super.onTouchEvent(event) } when (event.action) { MotionEvent.ACTION_DOWN -> { //记录落点 lastX = event.x lastY = event.y downUpdateChild(lastX, lastY) } MotionEvent.ACTION_MOVE -> { lastX = event.x lastY = event.y moveUpdateChild(lastX, lastY) } MotionEvent.ACTION_UP -> { complete = true //统计 upUpdateChild() } } return true } private fun downUpdateChild(x: Float, y: Float) { firstSelect = childContains(x, y) } private fun moveUpdateChild(x: Float, y: Float) { if (firstSelect) { moveUpdateLineAndChildView(x, y) } else { downUpdateChild(x, y) } } private fun moveUpdateLineAndChildView(x: Float, y: Float) { if (points.size != childCount) childContains(x, y) invalidate() } private fun upUpdateChild() { var effective = points.size >= minEffectiveSize onNineViewGroupListener?.complete(effective, buffer.toString()) } /** * 错误状态展示 */ fun showErrorStatue() { nowStatue = ERROR_STATUE points.forEach { it.setErrorStatue() } invalidate() resetStatueDelayed(500) } /** * 恢复初始状态 */ private fun resetStatue() { points.clear() firstSelect = false lastX = 0f lastY = 0f buffer.clear() nowStatue = DEFAULT_STATUE (0 until childCount).forEach { var nineChildParent = childViews[it] nineChildParent.setDefaultStatue() nineChildParent.NINE_CHILD_INF.isLight = false } invalidate() complete = false } fun resetStatueDelayed(time: Int) { postDelayed({ resetStatue() }, time.toLong()) } private fun childContains(x: Float, y: Float): Boolean { (0 until childCount).forEach { var childAt = getChildAt(it) if (x >= childAt.left && x < childAt.right && y >= childAt.top && y < childAt.bottom) { return if (!childViews[it].NINE_CHILD_INF.isLight) { if (points.size > 0) { checkMiddleChild(points[points.size - 1], childViews[it])?.run { if (!NINE_CHILD_INF.isLight) { buffer.append(NINE_CHILD_INF.index) changeLightStatue(this) } } } buffer.append(it + 1) //TODO 改变子view的UI状态 changeLightStatue(childViews[it]) true } else { false } } } return false } private fun changeLightStatue(childParent: NineChildParent<*>) { childParent.NINE_CHILD_INF.isLight = true childParent.setLightStatue() points.add(childParent)//记录 } private fun checkMiddleChild(nineChildParent: NineChildParent<*>, nineChildParent1: NineChildParent<*>): NineChildParent<*>? { var index = nineChildParent.NINE_CHILD_INF.index var index1 = nineChildParent1.NINE_CHILD_INF.index var sum = index + index1 if (sum == 10) { return childViews[4] } else if (index % 2 != 0 && index1 % 2 != 0) { if ((sum == 4 || sum == 16) || (sum == 8 && (index == 1 || index1 == 1))||(sum == 12 && (index == 3 || index1 == 3))) return childViews[sum / 2 - 1] } return null } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (!showLine) { return } paint.color = when (nowStatue) { ERROR_STATUE -> errorLineColor else -> lineColor } if (points.size > 1) { (1 until points.size).forEach { var pointXYStart = points[it - 1].NINE_CHILD_INF var pointXYEnd = points[it].NINE_CHILD_INF canvas.drawLine(pointXYStart.centerX, pointXYStart.centerY, pointXYEnd.centerX, pointXYEnd.centerY, paint) } } if (lastX > 0 && points.size > 0) { var pointXY = points[points.size - 1].NINE_CHILD_INF canvas.drawLine(pointXY.centerX, pointXY.centerY, lastX, lastY, paint) } } interface OnNineViewGroupListener { /** * 子view */ fun getChildMode(): NineChildParent<*> /** * 密码设置结束 * @param effective 是否有效 * @param password 密码 */ fun complete(effective: Boolean, password: String) } }
加载全部内容