Touch事件分发 - 九宫格解锁

1.概述


自定义View效果越写越难,但是将这些效果一步一步分解后,其实挺简单的,早期自己项目中用到九宫格解锁,我都是从网上下的,因为心里一开始觉得自己写应该会很困难,后来发现自己闲下来写写原来这么简单。这期的自定义View的效果我们用Kotlin来写

九宫格解锁

2.实现


2.1. 绘制出相对于这个View的居中的九个圆,刚开始当然是默认的
2.2. 当触摸屏幕的时候判断是否点击在这九个圆上
2.3. 在屏幕上滑动的时候,绘制两个点之间的线条和箭头,以及选中状态的点
2.4. 提供几个方法供用户调用,监听回调密码是否太短,合法等等

2.1. 绘制出相对于这个View的居中的九个圆

class LockPatternView : View {

    // 二维数组初始化,int[3][3]
    private var mPoints: Array<Array<Point?>> = Array(3) { Array<Point?>(3, { null }) }
    // 是否初始化
    private var mIsInit = false
    private var mWidth: Int = 0
    private var mHeight: Int = 0
    // 外圆的半径
    private var mDotRadius: Int = 0
    // 画笔
    private var mLinePaint: Paint? = null
    private var mPressedPaint: Paint? = null
    private var mErrorPaint: Paint? = null
    private var mNormalPaint: Paint? = null
    private var mArrowPaint: Paint? = null
    // 颜色
    private val mOuterPressedColor = 0xff8cbad8.toInt()
    private val mInnerPressedColor = 0xff0596f6.toInt()
    private val mOuterNormalColor = 0xffd9d9d9.toInt()
    private val mInnerNormalColor = 0xff929292.toInt()
    private val mOuterErrorColor = 0xff901032.toInt()
    private val mInnerErrorColor = 0xffea0945.toInt()

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onDraw(canvas: Canvas) {
        if (!mIsInit) {
            initPoints()
        }

        drawToCanvas(canvas)
    }

    private fun drawToCanvas(canvas: Canvas) {
        for (i in mPoints.indices) {
            for (j in 0..mPoints[i].size - 1) {
                val point = mPoints[i][j]
                if (point != null) {
                    // 循环绘制默认圆
                    mNormalPaint!!.color = mOuterNormalColor
                    canvas.drawCircle(point.centerX.toFloat(), point.centerY.toFloat(), mDotRadius.toFloat(), mNormalPaint!!)
                    mNormalPaint!!.color = mInnerNormalColor
                    canvas.drawCircle(point.centerX.toFloat(), point.centerY.toFloat(), (mDotRadius / 6).toFloat(), mNormalPaint!!
                }
            }
        }

        drawLineToCanvas(canvas)
    }

     /**
     * 初始化点
     */
    private fun initPoints() {
        mWidth = width
        mHeight = height

        var offsetX = 0
        var offsetY = 0

        if (mWidth > mHeight) {
            offsetX = (mWidth - mHeight) / 2
            mWidth = mHeight
        } else {
            offsetY = (mHeight - mWidth) / 2
            mHeight = mWidth
        }

        mDotRadius = mWidth / 12

        val padding = mDotRadius / 2
        val sideSize = (mWidth - 2 * padding) / 3
        offsetX += padding
        offsetY += padding

        for (i in mPoints.indices) {
            for (j in mPoints.indices) {
                // 循环初始化九个点
                mPoints[i][j] = Point(offsetX + sideSize * (i * 2 + 1) / 2,
                        offsetY + sideSize * (j * 2 + 1) / 2, i * mPoints.size + j)
            }
        }

        initPaint()

        mIsInit = true
    }


    private fun initPaint() {
        // 线的画笔
        mLinePaint = Paint()
        mLinePaint!!.color = mInnerPressedColor
        mLinePaint!!.style = Paint.Style.STROKE
        mLinePaint!!.isAntiAlias = true
        mLinePaint!!.strokeWidth = (mDotRadius / 9).toFloat()
        // 按下的画笔
        mPressedPaint = Paint()
        mPressedPaint!!.style = Paint.Style.STROKE
        mPressedPaint!!.isAntiAlias = true
        mPressedPaint!!.strokeWidth = (mDotRadius / 6).toFloat()
        // 错误的画笔
        mErrorPaint = Paint()
        mErrorPaint!!.style = Paint.Style.STROKE
        mErrorPaint!!.isAntiAlias = true
        mErrorPaint!!.strokeWidth = (mDotRadius / 6).toFloat()
        // 默认的画笔
        mNormalPaint = Paint()
        mNormalPaint!!.style = Paint.Style.STROKE
        mNormalPaint!!.isAntiAlias = true
        mNormalPaint!!.strokeWidth = (mDotRadius / 9).toFloat()
        // 箭头的画笔
        mArrowPaint = Paint()
        mArrowPaint!!.color = mInnerPressedColor
        mArrowPaint!!.style = Paint.Style.FILL
        mArrowPaint!!.isAntiAlias = true
    }
Kotlin初始化二维数组

2.2. 当触摸屏幕的时候判断是否点击在这九个圆上

override fun onTouchEvent(event: MotionEvent): Boolean {
        mMovingX = event.x
        mMovingY = event.y

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                val firstPoint = point
                if (firstPoint != null) {
                    // 已经开始选点了
                    mSelectPoints.add(firstPoint)
                    // 点设置为已经选中
                    firstPoint.setStatusPressed()
                    // 开始绘制
                    mSelectBegin = true
                }
            }
        }

        invalidate()
        return true
    }

    /**
     * 获取按下的点
     * @return 当前按下的点
     */
    private val point: Point?
        get() {
            for (i in mPoints.indices) {
                for (j in 0..mPoints[i].size - 1) {
                    val point = mPoints[i][j]
                    if (point != null) {
                        if (MathUtil.checkInRound(point.centerX.toFloat(), point.centerY.toFloat(), mDotRadius.toFloat(), mMovingX, mMovingY)) {
                            return point
                        }
                    }
                }
            }
            return null
        }

2.3. 在屏幕上滑动的时候,绘制两个点之间的线条和箭头,以及选中状态的点

override fun onTouchEvent(event: MotionEvent): Boolean {
        mMovingX = event.x
        mMovingY = event.y

        when (event.action) {
            MotionEvent.ACTION_MOVE -> if (mSelectBegin) {
                val selectPoint = point
                if (selectPoint != null) {
                    selectPoint.setStatusPressed()
                    if (!mSelectPoints.contains(selectPoint)) {
                        // 把选中的点添加到集合
                        mSelectPoints.add(selectPoint)
                    }
                }
            }
            MotionEvent.ACTION_UP -> if (mSelectBegin) {
                if (mSelectPoints.size == 1) {
                    // 清空选择
                    clearSelectPoints()
                } else if (mSelectPoints.size <= 4) {
                    // 太短显示错误
                    showSelectError()
                } else {
                    // 成功回调
                    if (mListener != null) {
                        lockCallBack()
                    }
                }
                mSelectBegin = false
            }
        }

        invalidate()
        return true
    }

    /**
     * 画线
     * @param canvas
     */
    private fun drawLineToCanvas(canvas: Canvas) {
        if (mSelectPoints.size >= 1) {
            if (mIsErrorStatus) {
                mLinePaint!!.color = mInnerErrorColor
                mArrowPaint!!.color = mInnerErrorColor
            } else {
                mLinePaint!!.color = mInnerPressedColor
                mArrowPaint!!.color = mInnerPressedColor
            }

            var lastPoint = mSelectPoints[0]
            for (i in 1..mSelectPoints.size - 1) {
                val point = mSelectPoints[i]
                // 不断的画线
                drawLine(lastPoint, point, canvas, mLinePaint!!)
                drawArrow(canvas, mArrowPaint!!, lastPoint, point, (mDotRadius / 4).toFloat(), 38)
                lastPoint = point
            }

            val isInnerPoint = MathUtil.checkInRound(lastPoint.centerX.toFloat(), lastPoint.centerY.toFloat(), mDotRadius.toFloat(), mMovingX, mMovingY)
            if (mSelectBegin && !isInnerPoint) {
                drawLine(lastPoint, Point(mMovingX.toInt(), mMovingY.toInt(), -1), canvas, mLinePaint!!)
            }
        }
    }

    /**
     * 画线
     */
    private fun drawLine(start: Point, end: Point, canvas: Canvas, paint: Paint) {
        val d = MathUtil.distance(start.centerX.toDouble(), start.centerY.toDouble(), end.centerX.toDouble(), end.centerY.toDouble())
        val rx = (((end.centerX - start.centerX) * mDotRadius).toDouble() / 5.0 / d).toFloat()
        val ry = (((end.centerY - start.centerY) * mDotRadius).toDouble() / 5.0 / d).toFloat()
        canvas.drawLine(start.centerX + rx, start.centerY + ry, end.centerX - rx, end.centerY - ry, paint)
    }

    /**
     * 画箭头
     */
    private fun drawArrow(canvas: Canvas, paint: Paint, start: Point, end: Point, arrowHeight: Float, angle: Int) {
        val d = MathUtil.distance(start.centerX.toDouble(), start.centerY.toDouble(), end.centerX.toDouble(), end.centerY.toDouble())
        val sin_B = ((end.centerX - start.centerX) / d).toFloat()
        val cos_B = ((end.centerY - start.centerY) / d).toFloat()
        val tan_A = Math.tan(Math.toRadians(angle.toDouble())).toFloat()
        val h = (d - arrowHeight.toDouble() - mDotRadius * 1.1).toFloat()
        val l = arrowHeight * tan_A
        val a = l * sin_B
        val b = l * cos_B
        val x0 = h * sin_B
        val y0 = h * cos_B
        val x1 = start.centerX + (h + arrowHeight) * sin_B
        val y1 = start.centerY + (h + arrowHeight) * cos_B
        val x2 = start.centerX + x0 - b
        val y2 = start.centerY.toFloat() + y0 + a
        val x3 = start.centerX.toFloat() + x0 + b
        val y3 = start.centerY + y0 - a
        val path = Path()
        path.moveTo(x1, y1)
        path.lineTo(x2, y2)
        path.lineTo(x3, y3)
        path.close()
        canvas.drawPath(path, paint)
    }

2.4. 提供几个方法供用户调用,监听回调密码是否太短,合法等等

    /**
     * 回调
     */
    private fun lockCallBack() {
        var password = ""
        for (selectPoint in mSelectPoints) {
            password += selectPoint.index
        }
        mListener!!.lock(password)
    }

    /**
     * 显示错误
     */
    fun showSelectError() {
        for (selectPoint in mSelectPoints) {
            selectPoint.setStatusError()
            mIsErrorStatus = true
        }

        postDelayed({
            clearSelectPoints()
            mIsErrorStatus = false
            invalidate()
        }, 1000)
    }

    /**
     * 清空所有的点
     */
    private fun clearSelectPoints() {
        for (selectPoint in mSelectPoints) {
            selectPoint.setStatusNormal()
        }
        mSelectPoints.clear()
    }

    /**
     * 清空所有的点
     */
    fun clearSelect() {
        for (selectPoint in mSelectPoints) {
            selectPoint.setStatusNormal()
        }
        mSelectPoints.clear()
        invalidate()
    }

    private var mListener: LockPatternListener? = null
    fun setLockPatternListener(listener: LockPatternListener) {
        this.mListener = listener
    }

    interface LockPatternListener {
        fun lock(password: String)
    }

如果觉得还是有点困难,可以看看前几篇自定义View的文章,至于箭头的绘制涉及到 sin、cos、tan ,我会在介绍贝塞尔曲线消息拖拽的时候讲一次数学课,当然我们在项目中有可能会直接使用资源图片。

所有分享大纲:Android进阶之旅 - 自定义View篇

视频讲解地址:http://pan.baidu.com/s/1pK8eQPt

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,574评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,623评论 4 59
  • 转载:http://www.jianshu.com/p/32fcadd12108 每个UIView有一个伙伴称为l...
    F麦子阅读 5,960评论 0 13
  • 忙碌的周末不能换来一夜的安睡,幸好,小羊为我借了一本木心的书。早餐时间,我们讨论了一会儿木心,我希望他先了解,有兴...
    夏洛的后花园阅读 193评论 0 1
  • 恩,对。在路上。 挺好的,有风景看。 其实我不喜欢听见叽叽喳喳的。可是难免的,这就是社会,你愿意的,不愿意的,统统...
    小默念tertt阅读 202评论 0 0