###1.概述canvas
自定義View效果越寫越難,可是將這些效果一步一步分解後,其實挺簡單的,早期本身項目中用到九宮格解鎖,我都是從網上下的,由於內心一開始以爲本身寫應該會很困難,後來發現本身閒下來寫寫原來這麼簡單。這期的自定義View的效果咱們用Kotlin來寫數組
###2.實現bash
2.1. 繪製出相對於這個View的居中的九個圓,剛開始固然是默認的 2.2. 當觸摸屏幕的時候判斷是否點擊在這九個圓上 2.3. 在屏幕上滑動的時候,繪製兩個點之間的線條和箭頭,以及選中狀態的點 2.4. 提供幾個方法供用戶調用,監聽回調密碼是否過短,合法等等markdown
2.1. 繪製出相對於這個View的居中的九個圓ide
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 } 複製代碼
2.2. 當觸摸屏幕的時候判斷是否點擊在這九個圓上oop
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. 在屏幕上滑動的時候,繪製兩個點之間的線條和箭頭,以及選中狀態的點post
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. 提供幾個方法供用戶調用,監聽回調密碼是否過短,合法等等this
/** * 回調 */ 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 ,我會在介紹貝塞爾曲線消息拖拽的時候講一次數學課,固然咱們在項目中有可能會直接使用資源圖片。spa
全部分享大綱:Android進階之旅 - 自定義View篇code
視頻講解地址:http://pan.baidu.com/s/1pK8eQPt