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閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,659評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,151評論 4 61
  • 轉載:http://www.lxweimin.com/p/32fcadd12108 每個UIView有一個伙伴稱為l...
    F麥子閱讀 6,259評論 0 13
  • 忙碌的周末不能換來一夜的安睡,幸好,小羊為我借了一本木心的書。早餐時間,我們討論了一會兒木心,我希望他先了解,有興...
    夏洛的后花園閱讀 208評論 0 1
  • 恩,對。在路上。 挺好的,有風景看。 其實我不喜歡聽見嘰嘰喳喳的。可是難免的,這就是社會,你愿意的,不愿意的,統統...
    小默念tertt閱讀 218評論 0 0