介绍下自己编写的九宫格手势密码。先见图
思路:首先是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欢迎留言指出,下面粘出九宫格容器的全部代码。GitHub项目地址
/**
* 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)
}
}