慕课网图形锁学习笔记
程序结构:
代码:
MainActivity.java
package com.lw.patternlock;
import com.lw.patternlock.LockPatternView.Point.OnPatternChangeListener;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity implements OnPatternChangeListener{
private LockPatternView lockPatternView = null;
private TextView title = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.title = (TextView) super.findViewById(R.id.title);
this.lockPatternView = (LockPatternView) super.findViewById(R.id.patternView);
this.lockPatternView.setPatternChangeListener(this);
}
@Override
public void onPatternChange(String passwordStr) {
// TODO Auto-generated method stub
if(TextUtils.isEmpty(passwordStr))
{
this.title.setText("图案绘制失败");
this.lockPatternView.reset();
}
else
{
this.title.setText("密码为:"+passwordStr);
}
}
@Override
public void onPatternStart(boolean isStart) {
// TODO Auto-generated method stub
if(isStart)
{
this.title.setText("绘制图形锁");
}
}
}
LockPatternView.java
package com.lw.patternlock;
import java.util.ArrayList;
import java.util.List;
import com.lw.patternlock.LockPatternView.Point.OnPatternChangeListener;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class LockPatternView extends View {
private Matrix matrix = new Matrix();//用于设置线条的缩放
private static final int POINTSIZE = 4;//设置构成一个锁的点的数量
private Point[][] points = new Point[3][3];//9个点的
private List<Point> pointList = new ArrayList<Point>();//用于放置连接的点
private int width = 0,height = 0;//屏幕的宽高
private float movingX,movingY;//触屏时移动的X,Y坐标,用于判断是否在点上
private float offsetsX = 0,offsetsY = 0;//偏移量
private int bitmapR ;//这是图片资源的半径,此处是由于画笔绘制时是在这个点处开始的,所以绘制的偏移度太多
private boolean isInit = false;//表明是否初始化点了
private boolean isSelect,isFinish ;//这是表明是否开始选中点,然后连线结束
private boolean movingNoPoint ;//判断是否不是重复点
private Bitmap PointsNormal,PointsError,PointsPressed,lineError,linePressed;
private Paint paint = null;//创建的画笔用于绘画
private OnPatternChangeListener onPatternListener;//监听密码的监听器
public LockPatternView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public LockPatternView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TODO Auto-generated constructor stub
}
/**
* 重写这个方法,是用与绘制点的
*/
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
if(!isInit)//没有初始化点时
{
initPoints();
}
//画点
this.Points2Canvas(canvas);
//画线
if(this.pointList.size()>0)
{
Point a = this.pointList.get(0);
//画图形锁的线
for(int i=0;i<this.pointList.size();i++)
{
Point b = this.pointList.get(i);
this.line2Canvas(canvas, a, b);
//a点变为b点是为了下一个连接的点可以成为b点,然后开始可以连线
a = b;
}
//画鼠标的线
if(this.movingNoPoint)
{
this.line2Canvas(canvas, a, new Point(this.movingX,this.movingY));
}
}
}
/**
* 重写touch事件来设置触屏时点和线的情况
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
this.movingNoPoint = false;
this.movingX = event.getX();
this.movingY = event.getY();
this.isFinish = false;
Point point = null;
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN://触屏时
if(this.onPatternListener!=null)
{
this.onPatternListener.onPatternStart(true);
}
this.reset();//清空List中保存的点
point = this.checkPoint();
if(point!=null)
{
this.isSelect = true;
}
break;
case MotionEvent.ACTION_MOVE://触屏移动时
if(this.isSelect)
{
point = this.checkPoint();
if(point==null)
{
this.movingNoPoint = true;
}
}
break;
case MotionEvent.ACTION_UP://松开时
this.isFinish = true;
this.isSelect = false;
break;
}
//点还在连线当中,判断是否连接的点重复了
if(this.isSelect&&!this.isFinish&&point!=null)
{
//是一个重复点
if(this.crossPointCheck(point))
{
this.movingNoPoint = true;
}
else //是一个新点
{
point.state = Point.STATE_PRESSED;
this.pointList.add(point);
}
}
//点已经连线完毕
if(isFinish)
{
//绘制图形锁不成立
if(this.pointList.size()==1)
{
this.reset();//清空保存的点
}
//绘制的点太少,不能成为密码锁
else if(this.pointList.size()<this.POINTSIZE&&this.pointList.size()>1)
{
this.pointError();
if(this.onPatternListener!=null)
{
this.onPatternListener.onPatternChange(null);
}
}
else //绘制成功
{
if(this.onPatternListener!=null)
{
String passwordStr = "";
for(int i=0;i<this.pointList.size();i++)
{
passwordStr = passwordStr+this.pointList.get(i).index;
}
if(!TextUtils.isEmpty(passwordStr))
{
this.onPatternListener.onPatternChange(passwordStr);;
}
}
}
}
//调用onDraw(),刷新View
postInvalidate();
return true;
}
/**
* 检查两个以上的点时是否重复连接
*/
private boolean crossPointCheck(Point point)
{
if(this.pointList.contains(point))
{
return true;
}
else
{
return false;
}
}
/**
* 清空选中的点
*/
public void reset()
{
for(Point point:this.pointList)
{
point.state = Point.STATE_NORMAL;
}
this.pointList.clear();
}
/**
* 连线的点太少,不能成为一个图形锁
*/
private void pointError()
{
for(Point point:this.pointList )
{
point.state = Point.STATE_ERROR;
}
}
/**
* 用于检查鼠标的坐标与绘制的点的坐标是否重合
*/
private Point checkPoint()
{
//因为要判断是否与鼠标的坐标重合,所以只有通过便利所有的点的坐标来判断
for(int i=0;i<this.points.length;i++)
{
for(int j=0;j<this.points[i].length;j++)
{
Point point = this.points[i][j];
boolean withIt = this.with(point.x, point.y,this.bitmapR,this.movingX,this.movingY);
if(withIt)
{
return point;
}
}
}
return null;
}
/**
* 这个方法是计算鼠标与点的距离是否在一定范围内,如果是则认为是重合
*/
private boolean with(float pointx,float pointy,float r,float movingX,float movingY)
{
//这里是如果鼠标的坐标距离点的坐标在一定范围内,则认为已经重合了
boolean withIt = Math.sqrt(Math.pow(pointx-movingX, 2)+Math.pow(pointy-movingY, 2))<r;
return withIt;
}
/**
* 画线的方法
*/
private void line2Canvas(Canvas canvas,Point a,Point b)
{
float degree = this.getDegrees(a, b);
//旋转画布,以a.x,a.y的坐标为参考点(原点)
canvas.rotate(degree, a.x, a.y);
float lineLength = (float) this.distance(a, b);
if(a.state==Point.STATE_PRESSED)
{
//设置线的缩放比率,第一参数是X轴的缩放比率,第二参数是Y轴的
this.matrix.setScale(lineLength/linePressed.getWidth(),1);
//对线条进行平移,因为不确定线条是在哪里开始画的
this.matrix.postTranslate(a.x-this.linePressed.getWidth()/2, a.y-this.linePressed.getHeight()/2);
//画线
canvas.drawBitmap(this.linePressed, matrix, paint);
}
else//画错误的线
{
//设置线的缩放比率,第一参数是X轴的缩放比率,第二参数是Y轴的
this.matrix.setScale(lineLength/lineError.getWidth(),1);
//对线条进行平移,因为不确定线条是在哪里开始画的
this.matrix.postTranslate(a.x-this.lineError.getWidth()/2, a.y-this.lineError.getHeight()/2);
//画线
canvas.drawBitmap(this.lineError, matrix, paint);
}
//此处是将画布重新转回来,其实我们看的是旋转线条,但实际上是画布在旋转
canvas.rotate(-degree, a.x, a.y);
}
/**
* 两点之间的距离
*/
public static double distance(Point a,Point b)
{
double result = Math.sqrt(Math.pow((a.x-b.x), 2)+Math.pow((a.y-b.y),2));
return result;
}
/**
* 计算线旋转的角度
*/
public float getDegrees(Point a,Point b)
{
float ax = a.x;
float ay = a.y;
float bx = b.x;
float by = b.y;
float degree = 0;//表示要返回的角度
if(bx==ax)//表示这两点的连线与Y轴平行
{
if(by>ay)//b点在a点的下面,那么两点的连线旋转角度就是90度,手机是顺时针旋转,从X轴到Y轴的旋转
{
degree = 90;
}
else if(ay>by)//b点在a点上面,那么从a点旋转到b点就要经过270度
{
degree = 270;
}
}
else if(by==ay)//两点连线与X轴平行
{
if(bx>ax)//b点在a点右边,从a到b点的连线角度为0度
{
degree = 0;
}
else if(ax>bx)
{
degree = 180;
}
}
else
{
degree = (float) Math.toDegrees(Math.atan2(b.y - a.y, b.x - a.x));
}
return degree;
}
/**
* 用于绘制图形在画布上的方法
* @param canvas
*/
private void Points2Canvas(Canvas canvas) {
this.paint = new Paint();
//开始绘制点在画布上
for(int i=0;i<this.points.length;i++)
{
for(int j=0;j<this.points[i].length;j++)
{
Point point = this.points[i][j];
if(point.state==Point.STATE_PRESSED)
{
canvas.drawBitmap(this.PointsPressed, point.x-this.bitmapR, point.y-this.bitmapR, paint);
}
else if(point.state==Point.STATE_NORMAL)
{
canvas.drawBitmap(this.PointsNormal,point.x-this.bitmapR, point.y-this.bitmapR, paint);
}
else
{
canvas.drawBitmap(this.PointsError, point.x-this.bitmapR, point.y-this.bitmapR, paint);
}
}
}
}
/**
* 用于初始化点的函数
*/
private void initPoints() {
//1.得到屏幕宽高
this.width = this.getWidth();
this.height = this.getHeight();
//2. 判断手机是横屏还是竖屏,设置偏移量
if(this.width>this.height)//横屏
{
//因为此时是横屏,所以在X轴上要有偏移量,以便于绘制图形在中心
//由于Y轴上是平分为四份,(三个点分四份),所以在Y轴上是不需要偏移的
offsetsX = (this.width-this.height)/2;
//因为这是图形锁的宽高,而图形锁是一个正方形,但是此时width太大,不能成为正方形
this.width = this.height ;
}
else//竖屏
{
offsetsY = (this.height-this.width)/2;
this.height = this.width;
}
//3.加载图片资源
this.PointsNormal = BitmapFactory.decodeResource(getResources(), R.drawable.point_normal);
this.PointsPressed = BitmapFactory.decodeResource(getResources(), R.drawable.point_pressed);
this.PointsError = BitmapFactory.decodeResource(getResources(), R.drawable.point_error);
this.linePressed = BitmapFactory.decodeResource(getResources(), R.drawable.line_pressed);
this.lineError = BitmapFactory.decodeResource(getResources(), R.drawable.line_error);
//4.创建点
//第一行
//此处这里加上width/4是因为如果是竖屏则宽被分成四等份,同样由于图形锁是一个正方形,所以在Y轴上也要加上width/4
this.points[0][0] = new Point(this.offsetsX+this.width/4,this.offsetsY+this.width/4);
this.points[0][1] = new Point(this.offsetsX+this.width/2,this.offsetsY+this.width/4);
this.points[0][2] = new Point(this.offsetsX+this.width*3/4,this.offsetsY+this.width/4);
//第二行
this.points[1][0] = new Point(this.offsetsX+this.width/4,this.offsetsY+this.width/2);
this.points[1][1] = new Point(this.offsetsX+this.width/2,this.offsetsY+this.width/2);
this.points[1][2] = new Point(this.offsetsX+this.width*3/4,this.offsetsY+this.width/2);
//第三行
this.points[2][0] = new Point(this.offsetsX+this.width/4,this.offsetsY+this.width*3/4);
this.points[2][1] = new Point(this.offsetsX+this.width/2,this.offsetsY+this.width*3/4);
this.points[2][2] = new Point(this.offsetsX+this.width*3/4,this.offsetsY+this.width*3/4);
//5. 图片资源半径
this.bitmapR = this.PointsNormal.getWidth()/2;
//6.设置密码,其实就是把point的index初始化,那么我们连接的点在内部表示就是一堆数字,也就是设置的密码
int index = 1;
for(Point[] points : this.points)
{
for(Point point :points)
{
point.index = index;
index++;
}
}
this.isInit = true;
}
/**
* 自定义的点,用于绘制图形点
*/
public static class Point
{
//正常状态的点形状,就是用户还没有点击图形锁的时候
public static int STATE_NORMAL = 0;
//点击图形锁点的时候,就是开始绘制图形时
public static int STATE_PRESSED = 1;
//绘制图形锁错误时
public static int STATE_ERROR = 2;
//点的坐标
public float x,y;
public int index = 0,state = 0;
//CRT
public Point()
{}
public Point(float x,float y)
{
this.x = x;
this.y = y;
}
/***
* 图案的监听器,这个其实就是要Activity继承这个接口,然后就可以返回数据到Activity中
*/
public static interface OnPatternChangeListener
{
/**
* 图案改变的方法
* @param passwordStr 是图案的密码,这个是图案改变的方法
*/
void onPatternChange(String passwordStr);
/**
* 图案开始绘制
* @param isStart 表示是否重新开始绘制图案了
*/
void onPatternStart(boolean isStart);
}
}
/**
* 设置图案监听器
*/
public void setPatternChangeListener(OnPatternChangeListener onPatternListener)
{
if(onPatternListener!=null)
{
this.onPatternListener = onPatternListener;
}
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="绘制图形锁"/>
<com.lw.patternlock.LockPatternView
android:id="@+id/patternView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
代码解释:
首先来看自定义组件的画图代码:(就是画那个图形锁界面)
/**
* 用于初始化点的函数
*/
private void initPoints() {
//1.得到屏幕宽高
this.width = this.getWidth();
this.height = this.getHeight();
//2. 判断手机是横屏还是竖屏,设置偏移量
if(this.width>this.height)//横屏
{
//因为此时是横屏,所以在X轴上要有偏移量,以便于绘制图形在中心
//由于Y轴上是平分为四份,(三个点分四份),所以在Y轴上是不需要偏移的
offsetsX = (this.width-this.height)/2;
//因为这是图形锁的宽高,而图形锁是一个正方形,但是此时width太大,不能成为正方形
this.width = this.height ;
}
else//竖屏
{
offsetsY = (this.height-this.width)/2;
this.height = this.width;
}
//3.加载图片资源
this.PointsNormal = BitmapFactory.decodeResource(getResources(), R.drawable.point_normal);
this.PointsPressed = BitmapFactory.decodeResource(getResources(), R.drawable.point_pressed);
this.PointsError = BitmapFactory.decodeResource(getResources(), R.drawable.point_error);
this.linePressed = BitmapFactory.decodeResource(getResources(), R.drawable.line_pressed);
this.lineError = BitmapFactory.decodeResource(getResources(), R.drawable.line_error);
//4.创建点
//第一行
//此处这里加上width/4是因为如果是竖屏则宽被分成四等份,同样由于图形锁是一个正方形,所以在Y轴上也要加上width/4
this.points[0][0] = new Point(this.offsetsX+this.width/4,this.offsetsY+this.width/4);
this.points[0][1] = new Point(this.offsetsX+this.width/2,this.offsetsY+this.width/4);
this.points[0][2] = new Point(this.offsetsX+this.width*3/4,this.offsetsY+this.width/4);
//第二行
this.points[1][0] = new Point(this.offsetsX+this.width/4,this.offsetsY+this.width/2);
this.points[1][1] = new Point(this.offsetsX+this.width/2,this.offsetsY+this.width/2);
this.points[1][2] = new Point(this.offsetsX+this.width*3/4,this.offsetsY+this.width/2);
//第三行
this.points[2][0] = new Point(this.offsetsX+this.width/4,this.offsetsY+this.width*3/4);
this.points[2][1] = new Point(this.offsetsX+this.width/2,this.offsetsY+this.width*3/4);
this.points[2][2] = new Point(this.offsetsX+this.width*3/4,this.offsetsY+this.width*3/4);
//5. 图片资源半径
this.bitmapR = this.PointsNormal.getWidth()/2;
//6.设置密码,其实就是把point的index初始化,那么我们连接的点在内部表示就是一堆数字,也就是设置的密码
int index = 1;
for(Point[] points : this.points)
{
for(Point point :points)
{
point.index = index;
index++;
}
}
this.isInit = true;
}
1. 上面这段代码其实就是首先画点的,这些点其实就是一个图片,然后将图片的点绘制出来。
//3.加载图片资源
this.PointsNormal = BitmapFactory.decodeResource(getResources(), R.drawable.point_normal);
this.PointsPressed = BitmapFactory.decodeResource(getResources(), R.drawable.point_pressed);
this.PointsError = BitmapFactory.decodeResource(getResources(), R.drawable.point_error);
这是加载图片资源,这里加载了三张图片资源,是因为首先第一张是正常没有操作图形锁时显示的,第二张是开始连接点时点改变,第三张是绘制的密码错误时的点的样子。
线有两张图片也是一样的。
2.
//第二行
this.points[1][0] = new Point(this.offsetsX+this.width/4,this.offsetsY+this.width/2);
this.points[1][1] = new Point(this.offsetsX+this.width/2,this.offsetsY+this.width/2);
this.points[1][2] = new Point(this.offsetsX+this.width*3/4,this.offsetsY+this.width/2);
这是得到九个点,这里是第二行的点的初始化,里面的参数是点的位置。至于这些位置的得到offsetsX = (this.width-this.height)/2;就是这些代码,其实这个是偏移量,就是点偏移X轴或者Y轴的位置,那个this.width才是主要的位置参数。
3.画点
/**
* 用于绘制图形在画布上的方法
* @param canvas
*/
private void Points2Canvas(Canvas canvas) {
this.paint = new Paint();
//开始绘制点在画布上
for(int i=0;i<this.points.length;i++)
{
for(int j=0;j<this.points[i].length;j++)
{
Point point = this.points[i][j];
if(point.state==Point.STATE_PRESSED)
{
canvas.drawBitmap(this.PointsPressed, point.x-this.bitmapR, point.y-this.bitmapR, paint);
}
else if(point.state==Point.STATE_NORMAL)
{
canvas.drawBitmap(this.PointsNormal,point.x-this.bitmapR, point.y-this.bitmapR, paint);
}
else
{
canvas.drawBitmap(this.PointsError, point.x-this.bitmapR, point.y-this.bitmapR, paint);
}
}
}
}
这就是画点的方法。
4. 那么就是这个onDraw()方法调用画的方法开始画这些东西。
/**
* 重写这个方法,是用与绘制点的
*/
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
if(!isInit)//没有初始化点时
{
initPoints();
}
//画点
this.Points2Canvas(canvas);
//画线
if(this.pointList.size()>0)
{
Point a = this.pointList.get(0);
//画图形锁的线
for(int i=0;i<this.pointList.size();i++)
{
Point b = this.pointList.get(i);
this.line2Canvas(canvas, a, b);
//a点变为b点是为了下一个连接的点可以成为b点,然后开始可以连线
a = b;
}
//画鼠标的线
if(this.movingNoPoint)
{
this.line2Canvas(canvas, a, new Point(this.movingX,this.movingY));
}
}
}
5. 事件设置,这里设置了一个onTouch事件,就是为了用户设置图形密码时的各种操作。里面的各种注释很详细,就不过多的阐述。
/**
* 重写touch事件来设置触屏时点和线的情况
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
this.movingNoPoint = false;
this.movingX = event.getX();
this.movingY = event.getY();
this.isFinish = false;
Point point = null;
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN://触屏时
if(this.onPatternListener!=null)
{
this.onPatternListener.onPatternStart(true);
}
this.reset();//清空List中保存的点
point = this.checkPoint();
if(point!=null)
{
this.isSelect = true;
}
break;
case MotionEvent.ACTION_MOVE://触屏移动时
if(this.isSelect)
{
point = this.checkPoint();
if(point==null)
{
this.movingNoPoint = true;
}
}
break;
case MotionEvent.ACTION_UP://松开时
this.isFinish = true;
this.isSelect = false;
break;
}
//点还在连线当中,判断是否连接的点重复了
if(this.isSelect&&!this.isFinish&&point!=null)
{
//是一个重复点
if(this.crossPointCheck(point))
{
this.movingNoPoint = true;
}
else //是一个新点
{
point.state = Point.STATE_PRESSED;
this.pointList.add(point);
}
}
//点已经连线完毕
if(isFinish)
{
//绘制图形锁不成立
if(this.pointList.size()==1)
{
this.reset();//清空保存的点
}
//绘制的点太少,不能成为密码锁
else if(this.pointList.size()<this.POINTSIZE&&this.pointList.size()>1)
{
this.pointError();
if(this.onPatternListener!=null)
{
this.onPatternListener.onPatternChange(null);
}
}
else //绘制成功
{
if(this.onPatternListener!=null)
{
String passwordStr = "";
for(int i=0;i<this.pointList.size();i++)
{
passwordStr = passwordStr+this.pointList.get(i).index;
}
if(!TextUtils.isEmpty(passwordStr))
{
this.onPatternListener.onPatternChange(passwordStr);;
}
}
}
}
//调用onDraw(),刷新View
postInvalidate();
return true;
}
6.
/***
* 图案的监听器,这个其实就是要Activity继承这个接口,然后就可以返回数据到Activity中
*/
public static interface OnPatternChangeListener
{
/**
* 图案改变的方法
* @param passwordStr 是图案的密码,这个是图案改变的方法
*/
void onPatternChange(String passwordStr);
/**
* 图案开始绘制
* @param isStart 表示是否重新开始绘制图案了
*/
void onPatternStart(boolean isStart);
}
}
public class MainActivity extends Activity implements OnPatternChangeListener
@Override
public void onPatternStart(boolean isStart) {
// TODO Auto-generated method stub
if(isStart)
{
this.title.setText("绘制图形锁");
}
}
可以看到这三个代码,第一个是LockPatternView类中的自定义的监听器接口,
第二个是表明这个MainActivity继承这个接口
第三个是MainActivity为这个接口复写的方法
其实这是一种很常见的为Activity传递数据的方式。
布局上的细节:
首先看入下代码,对比两段代码会发现,第二段代码中在画时减去了一个点图片的半径,这里入果是第一段代码画出来的效果就是如下图所示,这里画笔开始画时是将点的图片从点的位置开始画的,就是“点”左边的其实是“点”的中点位置,所以我们减去“点”图片的半径是为了往右的偏移量变小。
canvas.drawBitmap(this.PointsPressed, point.x, point.y, paint); |
canvas.drawBitmap(this.PointsPressed, point.x-this.bitmapR, point.y-this.bitmapR, paint); |
下图二是第二段代码的图片。
结果:
这是绘制图形密码成功的时候
if(TextUtils.isEmpty(passwordStr)) { this.title.setText("图案绘制失败"); this.lockPatternView.reset(); } |
这是我在MainActivity中设置的,如果图形密码绘制错误那么就会清空,所以就是这个样子。
PS:以上笔记是学习慕课网图形锁视频的
项目完整代码我已经上传了,可以自己在我的资源里搜索。