圖案解鎖(自定義)

簡介

今天為大家介紹的是圖案解鎖功能,采用自定義View的方式,小伙伴們可以直接拿過去用,獻上效果圖:


效果圖

實現(xiàn)

1、創(chuàng)建一個SlideUnlockView繼承于View龄捡,用于管理密碼圖案卓嫂,響應(yīng)滑動事件并返回密碼
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import java.lang.StringBuilder

class SlideUnlockView: View{

    constructor(context: Context?):super(context){}
    constructor(context: Context?,attrs: AttributeSet?): super(context,attrs){}
    constructor(context: Context?,attrs: AttributeSet?,style: Int): super(context,attrs,style){}
    //圓的半徑
    private var radius = 0f
    //間距
    private var padding = 0f
    //保存所有九個點的對象
    private val dots = mutableListOf<DotView>()
    //保存被選中的點
    private val selectedDots = mutableListOf<DotView>()
    //上一次被點亮的點
    private var lastSelectDot:DotView? = null
    //記錄移動線條
    private var endPoint = Point(0,0)

    //記錄劃線的路徑
    private val linePath = Path()
    //線條畫筆
    private val linePaint = Paint().apply {
        color = Color.DKGRAY
        strokeWidth = 5f
        style = Paint.Style.STROKE
    }
    //圓內(nèi)部遮蓋的Paint
    private val innerCirclePaint = Paint().apply{
        color = Color.WHITE
    }
    //記錄密碼
    private val password = StringBuilder()

    public var oPasswordListenner:OnPasswordChangedListenner? = null


    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //初始化
        init()
    }

    //具體繪制內(nèi)容
    override fun onDraw(canvas: Canvas?) {
        //畫線
        canvas?.drawPath(linePath,linePaint)
        //繪制移動的線
        if (!endPoint.equals(0,0)){
            canvas?.drawLine(lastSelectDot!!.cx,lastSelectDot!!.cy,endPoint.x.toFloat(),endPoint.y.toFloat(),linePaint)
        }
        //繪制九個點
        drawNineDots(canvas)

    }

    //繪制九個點
    private fun drawNineDots(canvas: Canvas?){
        for (dot in dots){
            canvas?.drawCircle(dot.cx,dot.cy,dot.radius,dot.paint)
            canvas?.drawCircle(dot.cx,dot.cy,radius-2,innerCirclePaint)
            if (dot.isSelected){
                canvas?.drawCircle(dot.cx,dot.cy,dot.innerCircleRadius,dot.paint)
            }
        }
    }

    //初始化
    private fun init(){
        //第一個點的中心坐標
        var cx = 0f
        var cy = 0f
        //計算半徑和間距
        //判斷你用戶設(shè)置當前View的尺寸  確保在正方形區(qū)域繪制
        if (measuredWidth >= measuredHeight){
            //半徑
            radius = measuredHeight/5/2f
            //間距
            padding = (measuredHeight-3*radius*2) / 4
            //中心點
            cx = (measuredWidth - measuredWidth)/2f + padding
            cy = padding + radius
        }else{
            radius = measuredWidth/5/2f
            padding = (measuredWidth - 3*radius*2) / 4
            cx = padding + radius
            cy = (measuredHeight - measuredWidth)/2f + padding +radius
        }

        //設(shè)置九個點組成的Path
        for (row in 0..2){
            for (colum in 0..2){
                DotView(cx+colum*(2*radius+padding),
                    cy+row*(2*radius+padding),
                    radius,row*3+colum+1).also { dots.add(it) }
            }
        }


    }


    override fun onTouchEvent(event: MotionEvent?): Boolean {
        //獲取觸摸點坐標
        val x = event?.x
        val y = event?.y
        when(event?.action){
            MotionEvent.ACTION_DOWN->{
                //判斷點是否在某個點的矩形區(qū)域內(nèi)
                containsPoint(x,y).also {
                    if (it != null){
                        //在某個圓點
                        selectItem(it)
                        selectedDots.add(it)
                        linePath.moveTo(it.cx,it.cy)
                    }else{
                        //不在某個圓點
                    }
                }
            }
            MotionEvent.ACTION_MOVE->{
                //判斷點是否在某個點的矩形區(qū)域內(nèi)
                containsPoint(x,y).also {
                    if (it != null){
                        if (!it.isSelected){
                            //沒有被點亮
                            //是不是第一個點
                            if (lastSelectDot == null){
                                //第一個點
                                linePath.moveTo(it.cx,it.cy)
                            }else{
                                //從上一個點畫線
                                linePath.lineTo(it.cx,it.cy)
                            }
                            //點亮這個點
                            selectItem(it)
                            selectedDots.add(it)
                        }
                    }else{
                        //觸摸點在外部
                        if (lastSelectDot != null){
                            endPoint.set(x!!.toInt(),y!!.toInt())
                            invalidate()
                        }
                    }
                }
            }
            MotionEvent.ACTION_UP->{
                oPasswordListenner?.passwordChanged(password.toString())
                reset()
            }
        }
        return true
    }

    //重設(shè)
    private fun reset(){


        //將顏色改為正常顏色
        for (item in selectedDots){
            item.isSelected = false
        }
        invalidate()
        //清空
        selectedDots.clear()
        lastSelectDot = null
        //線條重設(shè)
        linePath.reset()
        //設(shè)置endPoint為空
        endPoint.set(0,0)

        //清空密碼
        Log.v("yyy",password.toString())
        password.delete(0,password.length)

    }

    //選中某個點
    private fun selectItem(item: DotView){
        //設(shè)為被選中
        item.isSelected = true
        //刷新
        invalidate()
        //保存點亮點
        selectedDots.add(item)
        //記錄點
        lastSelectDot = item
        //設(shè)置endPoint為空
        endPoint.set(0,0)
        //記錄當前密碼
        password.append(item.tag)
    }

    //查找某個矩形區(qū)域是否包含某個觸摸點
    private fun containsPoint(x: Float?,y: Float?):DotView?{
        for (item in dots){
            if (item.rect.contains(x!!.toInt(),y!!.toInt())){
                return item
            }
        }
        return null
    }

    //回調(diào)密碼
    fun passwordBlock(pwd:(String) -> Unit){

        pwd(password.toString())
    }

    //接口 回調(diào)密碼
    interface OnPasswordChangedListenner{
       fun passwordChanged(pwd:String)

    }
2、單獨創(chuàng)建一個DotView聘殖,用于管理顯示的九個圓點晨雳,包含圓的坐標、半徑奸腺、畫筆顏色等信息餐禁,在ondraw中繪制時使用自己管理的屬性
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect

/**
 * 管理沒贏過圓點的具體樣式
 * 中心 x,y
 * 半徑 radius
 * 畫筆 paint
 * */
class DotView(val cx:Float, val cy:Float, val radius:Float,val tag: Int) {
    val paint = Paint().apply {
        color = Color.BLACK
        strokeWidth = 2f
        style = Paint.Style.FILL
    }

    //點的矩形范圍
    val rect = Rect()
    //選中點內(nèi)圓半徑
    var innerCircleRadius = 0f
    //記錄是否被選中
    var isSelected = false
        set(value) {
            field = value
            if (value){
                paint.color = Color.rgb(0,199,255)
            }else{
                paint.color = Color.BLACK
            }
        }


    //初始化代碼塊
    init {
        rect.left = (cx - radius).toInt()
        rect.top = (cy - radius).toInt()
        rect.right = (cx + radius).toInt()
        rect.bottom = (cy + radius).toInt()

        innerCircleRadius = radius / 3.5f
    }
    fun setColor(color: Int){
        paint.color = color
    }

}
3突照、創(chuàng)建好視圖帮非,就可在xml中使用并布局,再此創(chuàng)建一個SlideUnlockActivity讹蘑,并在xml中配置如下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SlideLoginActivity">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.15" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.22" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.29" />

    <View
        android:id="@+id/view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#333"
        app:layout_constraintBottom_toTopOf="@+id/guideline2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/mHeader"
        android:layout_width="96dp"
        android:layout_height="96dp"
        app:civ_border_width="3dp"
        app:civ_border_color="#fff"
        android:src="@drawable/tt"
        app:layout_constraintBottom_toTopOf="@+id/guideline2"
        app:layout_constraintEnd_toEndOf="@+id/view"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/view" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="浪到飛起的王者"
        android:textAlignment="center"
        app:layout_constraintBottom_toTopOf="@+id/guideline3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline2" />

    <TextView
        android:id="@+id/mAlert"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="請設(shè)置密碼"
        android:textAlignment="center"
        app:layout_constraintBottom_toTopOf="@+id/guideline4"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline3" />


    <yzl.swu.drawlogin.SlideUnlockView
        android:id="@+id/mSlideUnlock"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginStart="20dp"
        android:layout_marginEnd="20dp"
        app:layout_constraintDimensionRatio="w,1.05:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline4"></yzl.swu.drawlogin.SlideUnlockView>

</androidx.constraintlayout.widget.ConstraintLayout>
5末盔、在Activity代碼中增加動畫,接收SlideUnlockView回調(diào)來的密碼座慰,使用SharedPreference保存在應(yīng)用中

import android.animation.ObjectAnimator
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_slide_login.*

class SlideLoginActivity : AppCompatActivity() {
    //記錄繪制的密碼
    private var password:String? = null
    //記錄初始密碼
    private var orgPassword:String? = null
    //確認密碼  設(shè)置密碼時
    private var firstSurePassword:String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_slide_login)

        //獲取初始密碼
        orgPassword = SharedPreferenceUtil.getInstance(this).getPassword()
        if (orgPassword == null){
            mAlert.text = "請設(shè)置密碼"
        }else{
            mAlert.text = "請繪制密碼"
        }

        mSlideUnlock.oPasswordListenner = (object :SlideUnlockView.OnPasswordChangedListenner{
            override fun passwordChanged(pwd: String) {
                Log.v("yyy","pwd is:$pwd")
                password = pwd
                passwordOperation()
            }
        })

    }

    //判斷密碼操作
    private fun passwordOperation() {
        //頭像旋轉(zhuǎn)
        mHeader.animate()
            .rotationBy(360f)
            .setDuration(1000)
            .start()
        //保存密碼
        if (orgPassword == null) {
            //設(shè)置密碼
            if (firstSurePassword == null) {
                firstSurePassword = password.toString()
                mAlert.text = "請確認密碼"
            } else {
                //判斷兩次密碼是否一致
                if (firstSurePassword.equals(password.toString())) {
                    mAlert.text = "密碼設(shè)置成功"
                    SharedPreferenceUtil.getInstance(this).savePassword(firstSurePassword!!)
                } else {
                    mAlert.setTextColor(Color.RED)
                    mAlert.text = "兩次密碼不一致陨舱,請重新設(shè)置"
                    firstSurePassword = null
                    loginAnim()
                }
            }
        } else {
            //確認密碼
            if (orgPassword.equals(password.toString())) {
                mAlert.text = "密碼正確"
            } else {
                mAlert.setTextColor(Color.RED)
                mAlert.text = "密碼錯誤,請重新繪制"
                loginAnim()
            }
        }

        //清空
        password = null
        Handler().postDelayed({
            mAlert.setTextColor(Color.BLACK)
        }, 1000)
    }

    //密碼確認動畫
    private fun loginAnim(){

        ObjectAnimator.ofFloat(mAlert,"translationX",5f,-5f,0f).apply {
            duration=200
            start()
        }

    }
}
6版仔、上例中用到SharedPreference游盲,封裝成一個工具類误墓,不容易出錯
import android.content.Context

class SharedPreferenceUtil private constructor(){
    private val FILE_NAME = "password"
    private val KEY = "passwordKey"

    companion object{
        private var instance: SharedPreferenceUtil? = null
        private var mContext: Context? = null

        fun getInstance(context: Context): SharedPreferenceUtil{
            mContext = context
            if (instance == null){
                synchronized(this){
                    instance = SharedPreferenceUtil()
                }
            }
            return instance!!
        }
    }

    fun savePassword(pwd: String){
        //獲取preference對象
        val sharedPreferences = mContext?.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
        //獲取edit對象 -> 寫數(shù)據(jù)
        val edit = sharedPreferences?.edit()
        //寫入數(shù)據(jù)
        edit?.putString(KEY,pwd)
        //提交
        edit?.apply()
    }

    fun getPassword(): String?{
        //獲取preference對象
        val sharedPreferences = mContext?.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
        return sharedPreferences?.getString(KEY,null)
    }
}

結(jié)語

Github地址:https://github.com/InuyashaY/DrawLogin.git
每天進步一點點!R娑小谜慌!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市链峭,隨后出現(xiàn)的幾起案子畦娄,更是在濱河造成了極大的恐慌,老刑警劉巖弊仪,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熙卡,死亡現(xiàn)場離奇詭異,居然都是意外死亡励饵,警方通過查閱死者的電腦和手機驳癌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來役听,“玉大人颓鲜,你說我怎么就攤上這事〉溆瑁” “怎么了甜滨?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瘤袖。 經(jīng)常有香客問我衣摩,道長,這世上最難降的妖魔是什么捂敌? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任艾扮,我火速辦了婚禮,結(jié)果婚禮上占婉,老公的妹妹穿的比我還像新娘泡嘴。我一直安慰自己,他們只是感情好逆济,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布酌予。 她就那樣靜靜地躺著,像睡著了一般奖慌。 火紅的嫁衣襯著肌膚如雪霎终。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天升薯,我揣著相機與錄音莱褒,去河邊找鬼。 笑死涎劈,一個胖子當著我的面吹牛广凸,可吹牛的內(nèi)容都是我干的阅茶。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼谅海,長吁一口氣:“原來是場噩夢啊……” “哼脸哀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起扭吁,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤撞蜂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后侥袜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝌诡,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年枫吧,在試婚紗的時候發(fā)現(xiàn)自己被綠了浦旱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡九杂,死狀恐怖颁湖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情例隆,我是刑警寧澤甥捺,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站镀层,受9級特大地震影響镰禾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鹿响,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一羡微、第九天 我趴在偏房一處隱蔽的房頂上張望谷饿。 院中可真熱鬧惶我,春花似錦、人聲如沸博投。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毅哗。三九已至听怕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間虑绵,已是汗流浹背尿瞭。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留翅睛,地道東北人声搁。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓黑竞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疏旨。 傳聞我的和親對象是個殘疾皇子很魂,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348