Android(17)——绘制方式实现解锁页面

本节内容

1.Demo简介

2.详解实现步骤

3.创建类,计算半径和间距

4.绘制9个点

5.点亮一个点

6.点亮所有点

7.画线

8.设置选中状态

9.移动线条绘制和记录密码

一、Demo简介
1.解锁页面我们之前在Android开发(9)详细介绍了使用“图片显示或隐藏”的方式来实现,但是对于这种方法来说,布局比较麻烦,而且很不灵活->复用性(可移植性)不强。
2.还有另外两种方式来实现这个功能:自定义ViewGroup和自定义View。
自定义的优点:
  • 可以完成系统没有的效果或者功能
  • 化零为整
3.在这个demo里面,需要实现一些效果。圆点:正常|选中。线条:两点之间的线|移动时的线条。背景:图片|颜色。要实现这些效果,就需要使用ViewGroup。
4.自定义View:全都是靠绘制的方式实现。我们需要用自定义View实现以下功能:
  • 绘制背景
  • 绘制9个点
  • 选中一个点,在原来的位置绘制一个有颜色的点
  • 绘制线条
5.绘制的顺序为:底层背景、线条、圆点、选中的点。(因为后绘制的会覆盖之前绘制的)
二、详解实现步骤
1.实现的顺序
  • 绘制1个圆,x,y,radius,paint
  • 绘制9个圆
  • 记录密码:如何让绘制出来的一个图形,绑定一个值。(之前是图片,每个圆都是一个view,所以可以添加tag,但是自定义绘制添加不了)
2.用一个Dot类来管理一个圆的内容
3.绘制一条线:在move过程中不断刷新线的起点(startX,startY)和终点(EndX,EndY)。起点:上一个被选中点的中心位置。终点:当前移动的位置。
4.两点之间连接一条线。Path来记录。MoveTo(第一个被选中的点)LineTo(下一个点)LineTo(下一个点)
5.使用场景:绘制的View是一个正方形。如果用户设置的宽高不能形成一个正方形,那么就要自己确定绘制的范围。
三、创建类,计算半径和间距
1.创建工程,定义一个类管理绘制视图
class TouchUnlockView:View {
}
2.实现父类的构造方法
    //代码创建
    constructor(context: Context):super(context){}
    //xml创建
    constructor(context: Context,attrs: AttributeSet?):super(context,attrs){}
    //style
    constructor(context: Context,attrs: AttributeSet?,style:Int):super(context,attrs,style){}
3.确定绘制区域大小的过程如下图所示:
image.png
4.实现一下onMeasure方法和onSizeChanged方法,并在里面实现一下计算半径和间距的方法
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //初始化
        init()
    }
5.在初始化里面计算间距和半径,根据我们之前的规则来计算。这样算的话,半径和间距算出来的大小一样。当然还要提前定义一下半径和间距变量。
//圆的半径
    private var radius = 0f
    //间距
    private var padding = 0f
 private fun init(){
        //第一个点的中心点坐标
        var cx= 0f
        var cy= 0f
        //计算半径和间距
        //判断用户设置当前view的尺寸 确保在正方形区域绘制
        if(measuredWidth >= measuredHeight){
            //半径
            radius = measuredHeight/10f
            //间距
            padding= (measuredHeight-6*radius)/4
            cx = ( measuredWidth-measuredHeight)/2f + radius + padding
            cy = padding + radius
        }else{
            radius = measuredWidth/10f
            padding = (measuredHeight-6*radius)/4
            cx= padding + radius
            cy = (measuredHeight-measuredWidth)/2f + radius + padding
        }
}
四、绘制9个点
1.将每一个点独立出来,能够独立设置对应的颜色。用一个类来管理每一个圆点的具体样式。
class DotInfo(val cx:Float, val cy:Float,val radius:Float,val tag:Int) {
}
2.定义一个画笔
val paint = Paint().apply {
        color = Color.BLACK
        style = Paint.Style.FILL
    }
3.添加两个变量来记录选中后内部小圆点的半径,还有圆点是否被选中。并记录一下每一个小圆点所在的矩形区域。
    //选中的点的半径
    var innerCircleRadius = 0f
    //记录是否被选中
    var isSelected = false
  //这个点的矩形区域
    val rect = Rect()
4.添加一个修改颜色的函数,当圆点被选中时,画笔的颜色要发生改变。
fun setColor(color:Int){
       paint.color = color
   }
5.在TouchUnlockView类里面,添加一个保存所有9个点的信息对象
 private val dotInfos = mutableListOf<DotInfo>()
5.在TouchUnlockView类的init方法里面,设置9个点组成的Path
for(row in 0..2){
            for (colum in 0..2){
            DotInfo(cx+ colum*(2*radius + padding),
                cy + row*(2*radius + padding),
                radius,
                row*3+colum+1
                ).also {
                dotInfos.add(it)
            }
            }
        }
6.绘制9个点
fun drawNineDot(canvas: Canvas?){
     for(info in dotInfos){
         canvas?.drawCircle(info.cx,info.cy,info.radius,info.paint)
     }
    }
五、点亮一个点(获取触摸点的坐标,判断触摸点是否在某个矩形区域内)
1.在DotInfo类里面添加一个对象来记录每个小圆对应的绘制区域。然后在init里面初始化一下
 //这个点的矩形区域
    val rect = Rect()
    //初始化代码块
    init {
        //确定矩形区域
        rect.left = (cx - radius).toInt()
        rect.right = (cx + radius).toInt()
        rect.top = (cy - radius).toInt()
        rect.bottom = (cy + radius).toInt()

        innerCircleRadius = radius/3.5f
    }
2.然后重写TouchUnlockView类里面的onTouchEvent方法,并实现对应的事件
 override fun onTouchEvent(event: MotionEvent?): Boolean {
       //获取触摸点的坐标
        val x= event?.x
        val y = event?.y
       when(event?.action){
             MotionEvent.ACTION_DOWN ->{
                  }
            MotionEvent.ACTION_MOVE ->{
          }
     MotionEvent.ACTION_UP ->{
            }
}
3.当手指按下或移动的时候,我们都需要判断这个点是否在矩形区域内,所以我们写一个方法,来查找某个矩形区域是否包含触摸点。
private fun containsPoint(x:Float,y:Float):DotInfo?{
        for(item in dotInfos){
            if(item.rect.contains(x.toInt(),y.toInt())){
                     return item
            }
        }
        return null
    }
4.如果某个点在它所在的矩形区域内,那么就点亮这个点。所以我们写一个点亮点的函数。
 private fun selectItem(item:DotInfo){
        //改变这个点绘制的颜色
        item.setColor(Color.RED)
      //立刻刷新 重新绘制
        invalidate()
}
5.当我们放手的时候,又要记录一下那些被点亮的点。
   //保存所有被点亮的点的信息
    private val selectedItems= mutableListOf<DotInfo>()
6.手拿起来之后,又要重设一下
 private fun reset(){
        //将颜色改回正常颜色
        for(item in selectedItems){
            item.setColor(Color.BLACK)
        }
        invalidate()
        //清空
        selectedItems.clear()
}
7.在ACTION_DOWN里面完成点亮操作
MotionEvent.ACTION_DOWN ->{
                  //判断这个点是否在某个矩形区域内
                 containsPoint(x!!,y!!).also {
                     if(it!= null){
                         //点亮这个点
                         selectItem(it)
                     }
                 }
             }
六、点亮所有点
1.如果它没有被点亮,才需要点亮它。如果没有被点亮。还需要判断它是不是第一个点。所以我们需要一个变量来保存上一个被点亮点的圆点信息。
    //保存上一个被点亮的圆点信息
    private var lastSelectedItem:DotInfo?= null
2.那么在前面点亮的方法中,要把上一个被选中的点设置为当前点。
       //记录这个点
        lastSelectedItem = item
3.实现以一下ACTION_MOVE里面的操作
MotionEvent.ACTION_MOVE ->{
                //判断这个点是否在某个矩形区域内
                containsPoint(x!!,y!!).also {
                    if(it!= null){
                        //当前触摸点已经在某个圆点内部
                        if(!it.isSelected){
                            //没有点亮
                            //是不是第一个点
                            if(lastSelectedItem==null){
                                  
                            }else{
                               
                            }
                            //点亮这个点
                            selectItem(it)
                        }else{
                          
                        }
                    }
                }
            }
七、划线
1.连接两点之间的线,记录path路径。第一个点,调用moveTo,其他点直接lineTo。
2.添加一个变量记录线条的路径
    //记录线条的路径
    private val linePath = Path()
2.在ACTION_DOWN点亮一个点之后,它就作为linePath的起点
                   //linePath的起点为这个点的中心点
                    linePath.moveTo(it.cx,it.cy)
3.在ACTION_MOVE里面,判断是不是第一个点,如果是的话,它就是起点。否则它就从上一点到当前点画线。
                          if(lastSelectedItem==null){
                                //第一个点
                                //linePath的起点为这个点的中心点
                                linePath.moveTo(it.cx,it.cy)
                            }else{
                                 //从上一个点到当前点画线
                                linePath.lineTo(it.cx,it.cy)
                            }
4.在onDraw方法里面绘制线
  //绘制线
        canvas?.drawPath(linePath,linePaint)
5.给这个线另外设置一个画笔
//线的画笔
    private val linePaint= Paint().apply {
        color = Color.BLACK
        style = Paint.Style.STROKE
        strokeWidth = 2f
    }
6.在reset()方法里面,重设一下线条,让线条清空一下
       //线条重设
        linePath.reset()
八、设置选中状态
1.在完成前面一系列操作之后,我们发现,在绘制图案时,线会覆盖在圆上面,显得不美观。所以我们就绘制一个内圈圆,覆盖线的痕迹。
2.添加一个白色的内部画笔
private val innerCirclePaint = Paint().apply {
        color = Color.WHITE
        style = Paint.Style.FILL
    }
3.然后再绘制9个点的时候,也绘制这几个点。减去2其实是减去画笔的宽度。
canvas?.drawCircle(info.cx,info.cy,info.radius - 2,innerCirclePaint)
4.当选中点的时候,要把里面那个小圆画出来,所以要判断一下某个点是否被选中。被选中就再画一个小圆。
if(info.isSelected){
             canvas?.drawCircle(info.cx,info.cy,info.innerCircleRadius - 2,info.paint)
         }
5.当点亮某个点的时候,设置它被选中
item.isSelected = true
6.reset()方法里面也要修改一个选中的状态,不再是修改颜色,而是改变选中状态
for(item in selectedItems){
            item.isSelected = false
        }
九、移动线条绘制和记录密码
1.在onDraw里面绘制移动的线,先判断有没有上一个点,如果有的话,那么它就是起始点,终点是触摸点的坐标,会随着触摸点的改变而改变。
2.记录移动时触摸点的坐标 移动线条的终点
private val endPoint = Point()
3.绘制线
 if(!endPoint.equals(0,0)){
            canvas?.drawLine(lastSelectedItem!!.cx,lastSelectedItem!!.cy,
                endPoint.x.toFloat(), endPoint.y.toFloat(),
                linePaint
            )
        }
4.在onTouchEvent里面,如果触摸点在外部,那么就重新设置一下endPoint。因为只有触摸点在外部才能画线,而且必须要有起点才能画移动的线。
else{
                            //触摸点在外部
                            if(lastSelectedItem != null){
                             endPoint.set(x.toInt(),y.toInt())
                                invalidate()
                            }
                        }
5.停止绘制的时候要重设一下,就是清空所有信息。
 MotionEvent.ACTION_UP ->{
              reset()
            }
6.当点亮某个点之后,立刻设置endPoint为空。这样在没到达下一个点之前,这根线就不会被绘制出来。
      //设置endPoint为空
        endPoint.set(0,0)
7.设置tag为row*3+colum+1,因为是从0行0列开始的。
8.定义一个变量记录密码
private var password = StringBuilder()
9.一旦被点亮,就要记录一下这个点对应的tag值,在selectItem方法里面添加以下代码
password.append(item.tag)
10.然后在reset方法里面清空一下密码。
password.clear()

所有的流程差不多就讲完了,我把完整的代码放在下面。

1.DotInfo类
class DotInfo(val cx:Float, val cy:Float,val radius:Float,val tag:Int) {
    //这个点的画笔
    val paint = Paint().apply {
        color = Color.BLACK
        style = Paint.Style.FILL
    }

    //选中的点的半径
    var innerCircleRadius = 0f

    //记录是否被选中
    var isSelected = false

    //这个点的矩形区域
    val rect = Rect()
    //初始化代码块
    init {
        //确定矩形区域
        rect.left = (cx - radius).toInt()
        rect.right = (cx + radius).toInt()
        rect.top = (cy - radius).toInt()
        rect.bottom = (cy + radius).toInt()

        innerCircleRadius = radius/3.5f
    }

   fun setColor(color:Int){
       paint.color = color
   }

}
2.TouchUnlockView类
class TouchUnlockView:View {
    //圆的半径
    private var radius = 0f
    //间距
    private var padding = 0f

    //保存所有9个点的信息对象
    private val dotInfos = mutableListOf<DotInfo>()
    //保存所有被点亮的点的信息
    private val selectedItems= mutableListOf<DotInfo>()
    //保存上一个被点亮的圆点信息
    private var lastSelectedItem:DotInfo?= null

    //记录移动时触摸点的坐标 移动线条的终点
    private val endPoint = Point()

    //记录线条的路径
    private val linePath = Path()
    //线的画笔
    private val linePaint= Paint().apply {
        color = Color.BLACK
        style = Paint.Style.STROKE
        strokeWidth = 2f
    }

    //圆圈内部的白色遮盖的Paint
    private val innerCirclePaint = Paint().apply {
        color = Color.WHITE
        style = Paint.Style.FILL
    }

    //记录密码
    private var password = StringBuilder()


    //代码创建
    constructor(context: Context):super(context){}
    //xml创建
    constructor(context: Context,attrs: AttributeSet?):super(context,attrs){}
    //style
    constructor(context: Context,attrs: AttributeSet?,style:Int):super(context,attrs,style){}

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    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(lastSelectedItem!!.cx,lastSelectedItem!!.cy,
                endPoint.x.toFloat(), endPoint.y.toFloat(),
                linePaint
            )
        }

        //绘制9个点
      drawNineDot(canvas)
    }

    //绘制9个点
    fun drawNineDot(canvas: Canvas?){
     for(info in dotInfos){
         canvas?.drawCircle(info.cx,info.cy,info.radius,info.paint)
         canvas?.drawCircle(info.cx,info.cy,info.radius - 2,innerCirclePaint)
         if(info.isSelected){
             canvas?.drawCircle(info.cx,info.cy,info.innerCircleRadius,info.paint)
         }
     }
    }

    //初始化
   private fun init(){
        //第一个点的中心点坐标
        var cx= 0f
        var cy= 0f
        //计算半径和间距
        //判断用户设置当前view的尺寸 确保在正方形区域绘制
        if(measuredWidth >= measuredHeight){
            //半径
            radius = measuredHeight/10f
            //间距
            padding= (measuredHeight-6*radius)/4
            cx = ( measuredWidth-measuredHeight)/2f + radius + padding
            cy = padding + radius
        }else{
            radius = measuredWidth/10f
            padding = (measuredHeight-6*radius)/4
            cx= padding + radius
            cy = (measuredHeight-measuredWidth)/2f + radius + padding
        }

        //设置9个点组成的Path
        for(row in 0..2){
            for (colum in 0..2){
            DotInfo(cx+ colum*(2*radius + padding),
                cy + row*(2*radius + padding),
                radius,
                row*3+colum+1
                ).also {
                dotInfos.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)
                         //linePath的起点为这个点的中心点
                         linePath.moveTo(it.cx,it.cy)
                     }
                 }
             }
            MotionEvent.ACTION_MOVE ->{
                //判断这个点是否在某个矩形区域内
                containsPoint(x!!,y!!).also {
                    if(it!= null){
                        //当前触摸点已经在某个圆点内部
                        if(!it.isSelected){
                            //没有点亮
                            //是不是第一个点
                            if(lastSelectedItem==null){
                                //第一个点
                                //linePath的起点为这个点的中心点
                                linePath.moveTo(it.cx,it.cy)
                            }else{
                                 //从上一个点到当前点画线
                                linePath.lineTo(it.cx,it.cy)
                            }
                            //点亮这个点
                            selectItem(it)
                        }else{
                            //触摸点在外部
                            if(lastSelectedItem != null){
                             endPoint.set(x.toInt(),y.toInt())
                                invalidate()
                            }
                        }
                    }
                }
            }
            MotionEvent.ACTION_UP ->{
              reset()
            }
        }
        return true
    }

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

    //重设
       private fun reset(){
        //将颜色改回正常颜色
        for(item in selectedItems){
            item.isSelected = false
        }
        //线条重设
        linePath.reset()
        invalidate()
        //清空
        selectedItems.clear()
        //清空密码
       password.clear()
    }
    //点亮某个点
    private fun selectItem(item:DotInfo){
        //改变这个点绘制的颜色
        item.setColor(Color.RED)
        item.isSelected = true
        //立刻刷新 重新绘制
        invalidate()
        //保存这个点亮的点
        selectedItems.add(item)
        //记录这个点
        lastSelectedItem = item
        //设置endPoint为空
        endPoint.set(0,0)
        //记录当前的密码
        password.append(item.tag)
    }
}
3.layout中的xml代码
<com.example.drawunlock.TouchUnlockView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="30dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="1.1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容