本节内容
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" />