图案解锁(自定义)

简介

今天为大家介绍的是图案解锁功能,采用自定义View的方式,小伙伴们可以直接拿过去用,献上效果图:


效果图

实现

1、创建一个SlideUnlockView继承于View,用于管理密码图案,响应滑动事件并返回密码
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
    }
    //圆内部遮盖的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()
    }

    //具体绘制内容
    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
        //计算半径和间距
        //判断你用户设置当前View的尺寸  确保在正方形区域绘制
        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
        }

        //设置九个点组成的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->{
                //判断点是否在某个点的矩形区域内
                containsPoint(x,y).also {
                    if (it != null){
                        //在某个圆点
                        selectItem(it)
                        selectedDots.add(it)
                        linePath.moveTo(it.cx,it.cy)
                    }else{
                        //不在某个圆点
                    }
                }
            }
            MotionEvent.ACTION_MOVE->{
                //判断点是否在某个点的矩形区域内
                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
    }

    //重设
    private fun reset(){


        //将颜色改为正常颜色
        for (item in selectedDots){
            item.isSelected = false
        }
        invalidate()
        //清空
        selectedDots.clear()
        lastSelectDot = null
        //线条重设
        linePath.reset()
        //设置endPoint为空
        endPoint.set(0,0)

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

    }

    //选中某个点
    private fun selectItem(item: DotView){
        //设为被选中
        item.isSelected = true
        //刷新
        invalidate()
        //保存点亮点
        selectedDots.add(item)
        //记录点
        lastSelectDot = item
        //设置endPoint为空
        endPoint.set(0,0)
        //记录当前密码
        password.append(item.tag)
    }

    //查找某个矩形区域是否包含某个触摸点
    private fun containsPoint(x: Float?,y: Float?):DotView?{
        for (item in dots){
            if (item.rect.contains(x!!.toInt(),y!!.toInt())){
                return item
            }
        }
        return null
    }

    //回调密码
    fun passwordBlock(pwd:(String) -> Unit){

        pwd(password.toString())
    }

    //接口 回调密码
    interface OnPasswordChangedListenner{
       fun passwordChanged(pwd:String)

    }
2、单独创建一个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()
    //选中点内圆半径
    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、创建好视图,就可在xml中使用并布局,再此创建一个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="请设置密码"
        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回调来的密码,使用SharedPreference保存在应用中

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
    //确认密码  设置密码时
    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 = "请设置密码"
        }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() {
        //头像旋转
        mHeader.animate()
            .rotationBy(360f)
            .setDuration(1000)
            .start()
        //保存密码
        if (orgPassword == null) {
            //设置密码
            if (firstSurePassword == null) {
                firstSurePassword = password.toString()
                mAlert.text = "请确认密码"
            } else {
                //判断两次密码是否一致
                if (firstSurePassword.equals(password.toString())) {
                    mAlert.text = "密码设置成功"
                    SharedPreferenceUtil.getInstance(this).savePassword(firstSurePassword!!)
                } else {
                    mAlert.setTextColor(Color.RED)
                    mAlert.text = "两次密码不一致,请重新设置"
                    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对象 -> 写数据
        val edit = sharedPreferences?.edit()
        //写入数据
        edit?.putString(KEY,pwd)
        //提交
        edit?.apply()
    }

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

结语

Github地址:https://github.com/InuyashaY/DrawLogin.git
每天进步一点点!!!

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