概述
这一次我们来画一个九宫格密码锁界面。这个界面的实现也很简单,归根结底都是绘制然后处理好事件分发就好了。这次还是用Kotlin来写,因为这个功能可能比前两期写的控件还简单一点,所以涉及到自定义 View的一些基础的细节就不再重复细讲。
下面先看看效果:
下面分步实现:
1、画 9个空心大圆和 9个空心小圆
2、onTouch事件监听,记录手指划过的路径
3、留对外接口,密码判断逻辑交给调用者
1、画 9个空心大圆和 9个空心小圆
这一步比较简单。在控件测量完成后,根据控件的宽高先计算确定大圆的半径。然后再分别计算出 9个圆的圆心(大圆和小圆是同心圆)保存在列表里面。
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
pointList.clear()
// 根据控件宽度计算出大圆半径
radius = (width / 10).toFloat()
// 以控件中心点的位置,再根据大圆半径计算出第一个大圆圆心
// 两个大圆边缘距离这里等于半径距离
firstPoint.x = (width / 2).toFloat() - 3 * radius
firstPoint.y = (height / 2).toFloat() - 3 * radius
// 根据上面第一个圆的圆心,计算出剩下的圆的圆心,并保存到列表
for (i in 0 until 9) {
val point = PointF()
point.x = firstPoint.x + (i % 3) * 3 * radius
point.y = firstPoint.y + (i / 3) * 3 * radius
pointList.add(point)
}
}
/***
* 画 9个圆
* ***/
private fun drawCircle(canvas: Canvas?) {
// 画小圆
canvas?.drawCircle(pointList[item].x, pointList[item].y, radius / 5, circlePaint)
// 画大圆
canvas?.drawCircle(pointList[item].x, pointList[item].y, radius, circlePaint)
}
}
2、onTouch事件监听,记录手指划过的路径
这一步就是对 onTouch事件的处理。这里的 DOWN事件、MOVE事件和 UP事件都要处理。当手指按下时,上一次输入要清零,圆的颜色要恢复默认。MOVE事件时,有两步要走。第一步要判断手指触点是否在这些大圆的范围内。第二步判断当前大圆是否已入栈。如果MOVE事件手指触点在某个大圆上,并且是第一次落在这个大圆上,那么就把这个圆的记录下来,作为密码的一环。与此同时,在MOVE事件进行时,也要将密码路径用线连起来,最后一段线的末端是手指当前所处位置。然后是UP事件。手指抬起时,将记录到的密码路径通过接口回调给调用者。
override fun onTouchEvent(event: MotionEvent): Boolean {
currentPointF.x = event.x
currentPointF.y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 上次清零
clearSelect()
// 开始判断手指是否落在某个圆上
setSelected(event)
}
MotionEvent.ACTION_MOVE -> {
// 开始判断手指是否落在某个圆上
setSelected(event)
}
MotionEvent.ACTION_UP -> {
currentPointF.x = -1f
currentPointF.y = -1f
// 通过接口回调密码路径
if (selectedList.size > 0) mOnInputCompletedListener?.invoke(selectedList)
}
}
// 要不断重绘
invalidate()
return true
}
/***
* 画圆之间的连线
* ***/
private fun drawPath(canvas: Canvas?) {
if (selectedList.size <= 1) return
var path = Path()
path.moveTo(pointList[selectedList[0]].x, pointList[selectedList[0]].y)
for (i in 1 until selectedList.size) {
path.lineTo(pointList[selectedList[i]].x, pointList[selectedList[i]].y)
}
if (!passwordCorrect){
linePaint.color = Color.RED
}else{
linePaint.color = Color.BLUE
}
canvas?.drawPath(path, linePaint)
linePaint.color = Color.BLUE
}
/***
* 画手指连线
* ***/
private fun drawLine(canvas: Canvas?){
if ((selectedList.size <= 0) || (currentPointF.x < 0)) return
var startX = pointList[selectedList.last()].x
var startY = pointList[selectedList.last()].y
canvas?.drawLine(startX,startY, currentPointF.x, currentPointF.y, linePaint)
}
3、留对外接口,密码判断逻辑交给调用者
最后一步要完善一下对外接口。根据面向对象的原则,我们要将判断密码是否正确的逻辑交给调用者实现。我们只需要在手指抬起时将用户的输入通过接口回调给调用者就好:
// 回调,Lambda表达式
private var mOnInputCompletedListener: ((MutableList<Int>) -> Unit)? = null
fun setOnInputCompletedListener(onInputCompletedListener: ((MutableList<Int>) -> Unit)?){
this.mOnInputCompletedListener = onInputCompletedListener
}
/***
* 密码是否正确,给调用者调用的方法
* ***/
fun setPasswordCorrect(correct: Boolean){
this.passwordCorrect = correct
postInvalidate()
}
下面是完整代码:
class PasswordView : View {
// 大圆半径
private var radius = 0f
// 第一个点的圆心
private var firstPoint: PointF = PointF()
// 所有点的圆心
private var pointList: MutableList<PointF> = ArrayList()
// 圆的画笔
private var circlePaint: Paint = Paint()
// 线的画笔
private var linePaint: Paint = Paint()
// 已选中的圆的集合
private var selectedList: MutableList<Int> = ArrayList(9)
// 当前手指触点位置
private var currentPointF = PointF()
// 回调,Lambda表达式
private var mOnInputCompletedListener: ((MutableList<Int>) -> Unit)? = null
private var passwordCorrect = true
constructor(context: Context) : this(context, null)
constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context,
attributeSet,
defStyle) {
init(context, attributeSet, defStyle)
}
private fun init(context: Context, attributeSet: AttributeSet?, defStyle: Int) {
circlePaint.color = Color.GRAY
circlePaint.style = Paint.Style.STROKE
circlePaint.isAntiAlias = true
circlePaint.isDither = true
circlePaint.strokeWidth = dpToPx(3f)
linePaint.color = Color.BLUE
linePaint.style = Paint.Style.STROKE
linePaint.isAntiAlias = true
linePaint.isDither = true
linePaint.strokeWidth = dpToPx(10f)
}
fun setOnInputCompletedListener(onInputCompletedListener: ((MutableList<Int>) -> Unit)?){
this.mOnInputCompletedListener = onInputCompletedListener
}
/***
* 密码是否正确,给调用者调用的方法
* ***/
fun setPasswordCorrect(correct: Boolean){
this.passwordCorrect = correct
postInvalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
var width = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
var height = MeasureSpec.getSize(heightMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST) width = dpToPx(300f).toInt()
if (heightMode == MeasureSpec.AT_MOST) height = dpToPx(300f).toInt()
setMeasuredDimension(width, height)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
pointList.clear()
radius = (width / 10).toFloat()
firstPoint.x = (width / 2).toFloat() - 3 * radius
firstPoint.y = (height / 2).toFloat() - 3 * radius
for (i in 0 until 9) {
val point = PointF()
point.x = firstPoint.x + (i % 3) * 3 * radius
point.y = firstPoint.y + (i / 3) * 3 * radius
pointList.add(point)
}
}
override fun onDraw(canvas: Canvas?) {
drawCircle(canvas)
drawPath(canvas)
drawLine(canvas)
}
/***
* 画 9个圆
* ***/
private fun drawCircle(canvas: Canvas?) {
for (item in 0 until pointList.size) {
if (isPointSelected(item)) {
circlePaint.color = Color.BLUE
circlePaint.style = Paint.Style.FILL
if (!passwordCorrect) circlePaint.color = Color.RED
} else {
circlePaint.color = Color.GRAY
circlePaint.style = Paint.Style.STROKE
}
canvas?.drawCircle(pointList[item].x, pointList[item].y, radius / 5, circlePaint)
circlePaint.style = Paint.Style.STROKE
canvas?.drawCircle(pointList[item].x, pointList[item].y, radius, circlePaint)
}
}
/***
* 画圆之间的连线
* ***/
private fun drawPath(canvas: Canvas?) {
if (selectedList.size <= 1) return
var path = Path()
path.moveTo(pointList[selectedList[0]].x, pointList[selectedList[0]].y)
for (i in 1 until selectedList.size) {
path.lineTo(pointList[selectedList[i]].x, pointList[selectedList[i]].y)
}
if (!passwordCorrect){
linePaint.color = Color.RED
}else{
linePaint.color = Color.BLUE
}
canvas?.drawPath(path, linePaint)
linePaint.color = Color.BLUE
}
/***
* 画手指连线
* ***/
private fun drawLine(canvas: Canvas?){
if ((selectedList.size <= 0) || (currentPointF.x < 0)) return
var startX = pointList[selectedList.last()].x
var startY = pointList[selectedList.last()].y
canvas?.drawLine(startX,startY, currentPointF.x, currentPointF.y, linePaint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
currentPointF.x = event.x
currentPointF.y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
clearSelect()
setSelected(event)
}
MotionEvent.ACTION_MOVE -> {
setSelected(event)
}
MotionEvent.ACTION_UP -> {
currentPointF.x = -1f
currentPointF.y = -1f
if (selectedList.size > 0) mOnInputCompletedListener?.invoke(selectedList)
}
}
invalidate()
return true
}
/***
* 当前点是否已被选中
* ***/
private fun isPointSelected(item: Int): Boolean {
if (selectedList.size <= 0) return false
for (i in 0 until selectedList.size) {
if (item == selectedList[i]) return true
}
return false
}
/***
* 清空
* ***/
private fun clearSelect() {
selectedList.clear()
passwordCorrect = true
}
/***
* 收集数字
* ***/
private fun setSelected(event: MotionEvent) {
for (i in 0 until pointList.size) {
Log.d("contains",
"contains = " + selectedList.contains(i) + "-size = " + selectedList.size)
if ((isOnThePoint(pointList[i], event, radius)) && (!selectedList.contains(i))) {
selectedList.add(i)
}
}
}
/***
* 触点是否落在某个大圆上
* ***/
private fun isOnThePoint(pointF: PointF, event: MotionEvent, theRadius: Float): Boolean {
return sqrt((event.x - pointF.x).toDouble()
.pow(2) + (event.y - pointF.y).pow(2)) < theRadius
}
private fun dpToPx(dip: Float): Float {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, resources.displayMetrics)
}
}
最后记录一下 Kotlin语法问题
接口回调:
// 定义回调,Lambda表达式
private var mOnInputCompletedListener: ((MutableList<Int>) -> Unit)? = null
// 赋值
fun setOnInputCompletedListener(onInputCompletedListener: ((MutableList<Int>) -> Unit)?){
this.mOnInputCompletedListener = onInputCompletedListener
}
// 接口调用
mOnInputCompletedListener?.invoke(selectedList)
// 使用
passwordView.setOnInputCompletedListener { password ->
Log.d(TAG, "password = $password")
}