九宮格-手勢解鎖-自定義view

效果圖

九宮格手勢解鎖.gif

核心思路

九個格子的中心點(diǎn)計算


image.png

圓心的位置計算以及連線的起始點(diǎn)坐標(biāo)計算


image.png

三角形的繪制
image.png

核心代碼

/**
 * 九宮格密碼解鎖自定義view
 */

interface LockPatternListener {
    fun lock(password: String)
}

class LockPatternView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {

    //[3][3] 二維數(shù)組
    private val mPoints: Array<Array<Point>> = Array(3) { Array(3) { Point(0, 0, 0) } }

    private var mWidth: Int = 0
    private var mHeight: Int = 0

    // 外圓的半徑
    private var mDotRadius: Int = 0

    // 畫筆
    private lateinit var mLinePaint: Paint
    private lateinit var mPressedPaint: Paint
    private lateinit var mErrorPaint: Paint
    private lateinit var mNormalPaint: Paint
    private lateinit var mArrowPaint: Paint

    // 顏色
    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()

    private var mMovingX = 0f
    private var mMovingY = 0f

    private var mSelectBegin = false
    private var mIsErrorStatus = false

    private var mSelectPoints = mutableListOf<Point>()

    /**
     * 獲取按下的點(diǎn)
     * @return 當(dāng)前按下的點(diǎn)
     */
    private val point: Point?
        get() {
            for (i in mPoints.indices) {
                for (j in mPoints.indices) {
                    val point = mPoints[i][j]
                    if (checkInRound(point.centerX.toFloat(), point.centerY.toFloat(), mMovingX, mMovingY,mDotRadius.toFloat())) {
                        return point
                    }
                }
            }
            return null
        }

    private var mListener: LockPatternListener? = null

    fun setLockPatternListener(listener: LockPatternListener) {
        this.mListener = listener
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        if (changed) {
            initWidthAndHeight()
        }
    }

    private fun initWidthAndHeight() {
        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) {
                // 循環(huán)初始化九個點(diǎn)
                mPoints[i][j] = Point(
                    offsetX + sideSize * (i * 2 + 1) / 2,
                    offsetY + sideSize * (j * 2 + 1) / 2,
                    i * mPoints.size + j
                )
            }
        }

        initPaint()
    }

    private fun initPaint() {
        // 線的畫筆
        mLinePaint = Paint().apply {
            color = mInnerPressedColor
            style = Paint.Style.STROKE
            isAntiAlias = true
            strokeWidth = (mDotRadius / 9).toFloat()
        }
        // 按下的畫筆
        mPressedPaint = Paint().apply {
            style = Paint.Style.STROKE
            isAntiAlias = true
            strokeWidth = (mDotRadius / 6).toFloat()
        }
        // 錯誤的畫筆
        mErrorPaint = Paint().apply {
            style = Paint.Style.STROKE
            isAntiAlias = true
            strokeWidth = (mDotRadius / 6).toFloat()
        }
        // 默認(rèn)的畫筆
        mNormalPaint = Paint().apply {
            style = Paint.Style.STROKE
            isAntiAlias = true
            strokeWidth = (mDotRadius / 9).toFloat()
        }
        // 箭頭的畫筆
        mArrowPaint = Paint().apply {
            color = mInnerPressedColor
            style = Paint.Style.FILL
            isAntiAlias = true
        }
    }

    override fun onDraw(canvas: Canvas) {
        for (i in mPoints.indices) {
            for (j in mPoints.indices) {
                val point = mPoints[i][j]
                // 循環(huán)繪制默認(rèn)圓
                when (point.state) {
                    Point.State.NORMAL -> {
                        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 / 3.toFloat(), mNormalPaint)
                    }

                    Point.State.SELECT -> {
                        mPressedPaint.color = mOuterPressedColor
                        canvas.drawCircle(point.centerX.toFloat(), point.centerY.toFloat(), mDotRadius.toFloat(), mPressedPaint)
                        mPressedPaint.color = mInnerPressedColor
                        canvas.drawCircle(point.centerX.toFloat(), point.centerY.toFloat(), mDotRadius / 3.toFloat(), mPressedPaint)
                    }

                    else -> {
                        mErrorPaint.color = mOuterErrorColor
                        canvas.drawCircle(point.centerX.toFloat(), point.centerY.toFloat(), mDotRadius.toFloat(), mErrorPaint)
                        mErrorPaint.color = mInnerErrorColor
                        canvas.drawCircle(point.centerX.toFloat(), point.centerY.toFloat(), mDotRadius / 3.toFloat(), mErrorPaint)
                    }
                }
            }
        }

        drawLineToCanvas(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 until mSelectPoints.size) {
                val point = mSelectPoints[i]
                // 不斷的畫線
                drawLine(lastPoint, point, canvas, mLinePaint)
                drawArrow(canvas, mArrowPaint, lastPoint, point, (mDotRadius / 4).toFloat(), 38)
                lastPoint = point
            }

            val isInnerPoint = checkInRound(lastPoint.centerX.toFloat(), lastPoint.centerY.toFloat(), mMovingX, mMovingY,mDotRadius.toFloat())
            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 distance = distance(start.centerX.toDouble(), start.centerY.toDouble(), end.centerX.toDouble(), end.centerY.toDouble())
        val cosAngle = (end.centerX - start.centerX) / distance
        val sinAngle = (end.centerY - start.centerY) / distance
        val rx = (mDotRadius / 6 + mPressedPaint.strokeWidth) * cosAngle
        val ry = (mDotRadius / 6 + mPressedPaint.strokeWidth) * sinAngle
        canvas.drawLine(
            (start.centerX + rx).toFloat(), (start.centerY + ry).toFloat(),
            (end.centerX - rx).toFloat(), (end.centerY - ry).toFloat(),
            paint
        )
    }

    /**
     * 畫箭頭
     */
    private fun drawArrow(canvas: Canvas, paint: Paint, start: Point, end: Point, arrowHeight: Float, angle: Int) {
        val d = 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)
    }

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

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                val firstPoint = point
                if (firstPoint != null) {
                    // 已經(jīng)開始選點(diǎn)了
                    mSelectPoints.add(firstPoint)
                    // 點(diǎn)設(shè)置為已經(jīng)選中
                    firstPoint.setStatusPressed()
                    // 開始繪制
                    mSelectBegin = true
                }
            }

            MotionEvent.ACTION_MOVE -> if (mSelectBegin) {
                val selectPoint = point
                if (selectPoint != null) {
                    selectPoint.setStatusPressed()
                    if (!mSelectPoints.contains(selectPoint)) {
                        // 把選中的點(diǎn)添加到集合
                        mSelectPoints.add(selectPoint)
                    }
                }
            }

            MotionEvent.ACTION_UP -> if (mSelectBegin) {
                if (mSelectPoints.size == 1) {
                    // 清空選擇
                    clearSelectPoints()
                } else if (mSelectPoints.size <= 4) {
                    // 太短顯示錯誤
                    showSelectError()
                } else {
                    // 成功回調(diào)
                    if (mListener != null) {
                        lockCallBack()
                    }
                }
                mSelectBegin = false
            }
        }

        invalidate()
        return true
    }

    /**
     * 回調(diào)
     */
    private fun lockCallBack() {
        var password = ""
        for (selectPoint in mSelectPoints) {
            password += selectPoint.index
        }
        mListener?.lock(password)
    }


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

    /**
     * 顯示錯誤
     */
    fun showSelectError() {
        for (selectPoint in mSelectPoints) {
            selectPoint.setStatusError()
            mIsErrorStatus = true
        }

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

    /**
     * 檢查是否在圓內(nèi)(包括圓上)
     */
    private fun checkInRound(centerX: Float, centerY: Float,  x: Float, y: Float,radius: Float): Boolean {
        val dx = x - centerX
        val dy = y - centerY
        return sqrt(dx.pow(2) + dy.pow(2)) <= radius
    }

    /**
     * 計算圓心之間的距離
     */
    private fun distance(startX: Double, startY: Double, endX: Double, endY: Double): Double {
        return sqrt((endX - startX).pow(2.0) + (endY - startY).pow(2.0))
    }
}

源碼地址

https://github.com/treech/MyView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末窿给,一起剝皮案震驚了整個濱河市助琐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忍饰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耐版,死亡現(xiàn)場離奇詭異祠够,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)粪牲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門古瓤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人腺阳,你說我怎么就攤上這事落君。” “怎么了亭引?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵绎速,是天一觀的道長。 經(jīng)常有香客問我焙蚓,道長纹冤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任购公,我火速辦了婚禮萌京,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘君丁。我一直安慰自己枫夺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布绘闷。 她就那樣靜靜地躺著橡庞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪印蔗。 梳的紋絲不亂的頭發(fā)上扒最,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機(jī)與錄音华嘹,去河邊找鬼吧趣。 笑死,一個胖子當(dāng)著我的面吹牛耙厚,可吹牛的內(nèi)容都是我干的强挫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼薛躬,長吁一口氣:“原來是場噩夢啊……” “哼俯渤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起型宝,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤八匠,失蹤者是張志新(化名)和其女友劉穎絮爷,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梨树,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坑夯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了抡四。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柜蜈。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖指巡,靈堂內(nèi)的尸體忽然破棺而出跨释,到底是詐尸還是另有隱情,我是刑警寧澤厌处,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站岁疼,受9級特大地震影響阔涉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捷绒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一瑰排、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧暖侨,春花似錦椭住、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至葫掉,卻和暖如春些举,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背俭厚。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工户魏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人挪挤。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓叼丑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扛门。 傳聞我的和親對象是個殘疾皇子鸠信,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容