欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

轻松实现安卓(Android)九宫格解锁

程序员文章站 2024-03-07 19:27:57
效果图 思路 首先我们来分析一下实现九宫格解锁的思路:当用户的手指触摸到某一个点时,先判断该点是否在九宫格的某一格范围之内,若在范围内,则该格变成选中的状态;之后用...

效果图

轻松实现安卓(Android)九宫格解锁

思路

首先我们来分析一下实现九宫格解锁的思路:当用户的手指触摸到某一个点时,先判断该点是否在九宫格的某一格范围之内,若在范围内,则该格变成选中的状态;之后用户手指滑动的时候,以该格的圆心为中心,用户手指为终点,两点连线。最后当用户手指抬起时,判断划过的九宫格密码是否和原先的密码匹配。

大致的思路流程就是上面这样的了,下面我们可以来实践一下。

point 类

我们先来创建一个 point 类,用来表示九宫格锁的九个格子。除了坐标 x ,y 之外,还有三种模式:正常模式按下模式错误模式。根据模式不同该格子的颜色会有所不同,这会在下面中说明。

public class point {

 private float x;
 private float y;
 // 正常模式
 public static final int normal_mode = 1;
 // 按下模式
 public static final int pressed_mode = 2;
 // 错误模式
 public static final int error_mode = 3;
 private int state = normal_mode;
 // 表示该格的密码,比如“1”、“2”等
 private string mark;

 public string getmark() {
  return mark;
 }

 public void setmark(string mark) {
  this.mark = mark;
 }

 public point(float x, float y, string mark) {
  this.x = x;
  this.y = y;
  this.mark = mark;
 }

 public int getstate() {
  return state;
 }

 public void setstate(int state) {
  this.state = state;
 }

 public float getx() {
  return x;
 }

 public void setx(float x) {
  this.x = x;
 }

 public float gety() {
  return y;
 }

 public void sety(float y) {
  this.y = y;
 }

}

rotatedegrees类

有了上面的 point 类之后,我们还要创建一个 rotatedegrees 类,主要作用是计算两个 point 坐标之间的角度

public class rotatedegrees {

 /**
  * 根据传入的point计算出它们之间的角度
  * @param a
  * @param b
  * @return
  */
 public static float getdegrees(point a, point b) {
  float degrees = 0;
  float ax = a.getx();
  float ay = a.gety();
  float bx = b.getx();
  float by = b.gety();

  if (ax == bx) {
   if (ay < by) {
    degrees = 90;
   } else {
    degrees = 270;
   }
  } else if (by == ay) {
   if (ax < bx) {
    degrees = 0;
   } else {
    degrees = 180;
   }
  } else {
   if (ax > bx) {
    if (ay > by) { // 第三象限
     degrees = 180 + (float) (math.atan2(ay - by, ax - bx) * 180 / math.pi);
    } else { // 第二象限
     degrees = 180 - (float) (math.atan2(by - ay, ax - bx) * 180 / math.pi);
    }
   } else {
    if (ay > by) { // 第四象限
     degrees = 360 - (float) (math.atan2(ay - by, bx - ax) * 180 / math.pi);
    } else { // 第一象限
     degrees = (float) (math.atan2(by - ay, bx - ax) * 180 / math.pi);
    }
   }
  }
  return degrees;
 }

 /**
  * 根据point和(x,y)计算出它们之间的角度
  * @param a
  * @param bx
  * @param by
  * @return
  */
 public static float getdegrees(point a, float bx, float by) {
  point b = new point(bx, by, null);
  return getdegrees(a, b);
 }

}

screenlockview 类

然后我们要先准备好关于九宫格的几张图片,比如在九宫格的格子中,normal_mode 模式下是蓝色的,被手指按住时九宫格的格子是绿色的,也就是对应着上面 point 类的中 pressed_mode 模式,还有 error_mode 模式下是红色的。另外还有圆点之间的连线,也是根据模式不同颜色也会不同。在这里我就不把图片贴出来了。

有了图片资源之后,我们要做的就是先在构造器中加载图片:

public class screenlockview extends view {

 private static final string tag = "screenlockview";
 // 错误格子的图片
 private bitmap errorbitmap;
 // 正常格子的图片
 private bitmap normalbitmap;
 // 手指按下时格子的图片
 private bitmap pressedbitmap;
 // 错误时连线的图片
 private bitmap lineerrorbitmap;
 // 手指按住时连线的图片
 private bitmap linepressedbitmap;
 // 偏移量,使九宫格在屏幕*
 private int offset;
 // 九宫格的九个格子是否已经初始化
 private boolean init;
 // 格子的半径
 private int radius;
 // 密码
 private string password = "123456";
 // 九个格子
 private point[][] points = new point[3][3];
 private int width;
 private int height;
 private matrix matrix = new matrix();
 private float movex = -1;
 private float movey = -1;
 // 是否手指在移动
 private boolean ismove;
 // 是否可以触摸,当用户抬起手指,划出九宫格的密码不正确时为不可触摸
 private boolean istouch = true;
 // 用来存储记录被按下的点
 private list<point> pressedpoint = new arraylist<>();
 // 屏幕解锁监听器
 private onscreenlocklistener listener;

 public screenlockview(context context) {
  this(context, null);
 }

 public screenlockview(context context, attributeset attrs) {
  this(context, attrs, 0);
 }

 public screenlockview(context context, attributeset attrs, int defstyleattr) {
  super(context, attrs, defstyleattr);
  errorbitmap = bitmapfactory.decoderesource(getresources(), r.drawable.bitmap_error);
  normalbitmap = bitmapfactory.decoderesource(getresources(), r.drawable.bitmap_normal);
  pressedbitmap = bitmapfactory.decoderesource(getresources(), r.drawable.bitmap_pressed);
  lineerrorbitmap = bitmapfactory.decoderesource(getresources(), r.drawable.line_error);
  linepressedbitmap = bitmapfactory.decoderesource(getresources(), r.drawable.line_pressed);
  radius = normalbitmap.getwidth() / 2;
 }
 ...
}

在构造器中我们主要就是把图片加载完成,并且得到了格子的半径,即图片宽度的一半。

之后我们来看看 onmeasure(int widthmeasurespec, int heightmeasurespec) 方法:

@override
protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
 int widthsize = measurespec.getsize(widthmeasurespec);
 int widthmode = measurespec.getmode(widthmeasurespec);
 int heightsize = measurespec.getsize(heightmeasurespec);
 int heightmode = measurespec.getmode(heightmeasurespec);
 if (widthsize > heightsize) {
  offset = (widthsize - heightsize) / 2;
 } else {
  offset = (heightsize - widthsize) / 2;
 }
 setmeasureddimension(widthsize, heightsize);
}

onmeasure(int widthmeasurespec, int heightmeasurespec) 方法中,主要得到对应的偏移量,以便在下面的 ondraw(canvas canvas) 把九宫格绘制在屏幕*。

下面就是 ondraw(canvas canvas) 方法:

@override
protected void ondraw(canvas canvas) {
 if (!init) {
  width = getwidth();
  height = getheight();
  initpoint();
  init = true;
 }
 // 画九宫格的格子
 drawpoint(canvas);

 if (movex != -1 && movey != -1) {
  // 画直线
  drawline(canvas);
 }
}

首先判断了是否为第一次调用 ondraw(canvas canvas) 方法,若为第一次则对 points 进行初始化:

// 初始化点
private void initpoint() {
 points[0][0] = new point(width / 4, offset + width / 4, "0");
 points[0][1] = new point(width / 2, offset + width / 4, "1");
 points[0][2] = new point(width * 3 / 4, offset + width / 4, "2");

 points[1][0] = new point(width / 4, offset + width / 2, "3");
 points[1][1] = new point(width / 2, offset + width / 2, "4");
 points[1][2] = new point(width * 3 / 4, offset + width / 2, "5");

 points[2][0] = new point(width / 4, offset + width * 3 / 4, "6");
 points[2][1] = new point(width / 2, offset + width * 3 / 4, "7");
 points[2][2] = new point(width * 3 / 4, offset + width * 3 / 4, "8");
}

initpoint() 方法中主要创建了九个格子,并设置了相应的位置和密码。初始化完成之后把 init 置为 false ,下次不会再调用。

回过头再看看 ondraw(canvas canvas) 中其他的逻辑,接下来调用了 drawpoint(canvas) 来绘制格子:

// 画九宫格的格子
private void drawpoint(canvas canvas) {
 for (int i = 0; i < points.length; i++) {
  for (int j = 0; j < points[i].length; j++) {
   int state = points[i][j].getstate();
   if (state == point.normal_mode) {
    canvas.drawbitmap(normalbitmap, points[i][j].getx() - radius, points[i][j].gety() - radius, null);
   } else if (state == point.pressed_mode) {
    canvas.drawbitmap(pressedbitmap, points[i][j].getx() - radius, points[i][j].gety() - radius, null);
   } else {
    canvas.drawbitmap(errorbitmap, points[i][j].getx() - radius, points[i][j].gety() - radius, null);
   }
  }
 }
}

在绘制格子还是很简单的,主要分为了三种:普通模式下的格子按下模式下的格子以及错误模式下的格子

ontouchevent

在绘制好了格子之后,我们先不看最后的 drawline(canvas) 方法,因为绘制直线是和用户手指的触摸事件息息相关的,所以我们先把目光转向 ontouchevent(motionevent event) 方法:

@override
public boolean ontouchevent(motionevent event) {
 if (istouch) {
  float x = event.getx();
  float y = event.gety();
  point point;
  switch (event.getaction()) {
   case motionevent.action_down:
   // 判断用户触摸的点是否在九宫格的任意一个格子之内
    point = ispoint(x, y);
    if (point != null) {
     point.setstate(point.pressed_mode); // 切换为按下模式
     pressedpoint.add(point); 
    }
    break;
   case motionevent.action_move:
    if (pressedpoint.size() > 0) {
     point = ispoint(x, y);
     if (point != null) {
      if (!crosspoint(point)) {
       point.setstate(point.pressed_mode);
       pressedpoint.add(point);
      }
     }
     movex = x;
     movey = y;
     ismove = true;
    }
    break;
   case motionevent.action_up:
    ismove = false;
    string temppwd = "";
    for (point p : pressedpoint) {
     temppwd += p.getmark();
    }
    if (listener != null) {
     listener.getstringpassword(temppwd);
    }

    if (temppwd.equals(password)) {
     if (listener != null) {
      listener.ispassword(true);
     }
    } else {
     for (point p : pressedpoint) {
      p.setstate(point.error_mode);
     }
     istouch = false;
     this.postdelayed(runnable, 1000);
     if (listener != null) {
      listener.ispassword(false);
     }
    }
    break;
  }
  invalidate();
 }
 return true;
}

public interface onscreenlocklistener {
 public void getstringpassword(string password);
 public void ispassword(boolean flag);
}

public void setonscreenlocklistener(onscreenlocklistener listener) {
 this.listener = listener;
}

motionevent.action_down 中,先在 ispoint(float x, float y) 方法内判断了用户触摸事件的坐标点是否在九宫格的任意一格之内。如果是,则需要把该九宫格的格子添加到 pressedpoint 中:

// 该触摸点是否为格子
private point ispoint(float x, float y) {
 point point;
 for (int i = 0; i < points.length; i++) {
  for (int j = 0; j < points[i].length; j++) {
   point = points[i][j];
   if (iscontain(point, x, y)) {
    return point;
   }
  }
 }
 return null;
}

// 该点(x,y)是否被包含
private boolean iscontain(point point, float x, float y) {
 // 该点的(x,y)与格子圆心的距离若小于半径就是被包含了
 return math.sqrt(math.pow(x - point.getx(), 2f) + math.pow(y - point.gety(), 2f)) <= radius;
}

接下来就是要看 motionevent.action_move 的逻辑了。一开始判断了用户触摸的点是否为九宫格的某个格子。但是比 motionevent.action_down 还多了一个步骤:若用户触摸了某个格子,还要判断该格子是否已经被包含在 pressedpoint 里面了。

// 是否该格子已经被包含在pressedpoint里面了
private boolean crosspoint(point point) {
 if (pressedpoint.contains(point)) {
  return true;
 }
 return false;
}

最后来看看 motionevent.action_up ,把 pressedpoint 里保存的格子遍历后得到用户划出的密码,再和预先设置的密码比较,若相同则回调 onscreenlocklistener 监听器;不相同则把 pressedpoint 中的所有格子的模式设置为错误模式,并在 runnable 中调用 reset() 清空 pressedpoint ,重绘视图,再回调监听器。

private runnable runnable = new runnable() {
 @override
 public void run() {
  istouch = true;
  reset();
  invalidate();
 }
};

// 重置格子
private void reset(){
 for (int i = 0; i < points.length; i++) {
  for (int j = 0; j < points[i].length; j++) {
   points[i][j].setstate(point.normal_mode);
  }
 }
 pressedpoint.clear();
}

现在我们回过头来看看之前在 ondraw(canvas canvas) 里面的 drawline(canvas canvas) 方法:

// 画直线
private void drawline(canvas canvas) {

 // 将pressedpoint中的所有格子依次遍历,互相连线
 for (int i = 0; i < pressedpoint.size() - 1; i++) {
  // 得到当前格子
  point point = pressedpoint.get(i);
  // 得到下一个格子
  point nextpoint = pressedpoint.get(i + 1);
  // 旋转画布
  canvas.rotate(rotatedegrees.getdegrees(point, nextpoint), point.getx(), point.gety());

  matrix.reset();
  // 根据距离设置拉伸的长度
  matrix.setscale(getdistance(point, nextpoint) / linepressedbitmap.getwidth(), 1f);
  // 进行平移
  matrix.posttranslate(point.getx(), point.gety() - linepressedbitmap.getwidth() / 2);


  if (point.getstate() == point.pressed_mode) {
   canvas.drawbitmap(linepressedbitmap, matrix, null);
  } else {
   canvas.drawbitmap(lineerrorbitmap, matrix, null);
  }
  // 把画布旋转回来
  canvas.rotate(-rotatedegrees.getdegrees(point, nextpoint), point.getx(), point.gety());
 }

 // 如果是手指在移动的情况
 if (ismove) {
  point lastpoint = pressedpoint.get(pressedpoint.size() - 1);
  canvas.rotate(rotatedegrees.getdegrees(lastpoint, movex, movey), lastpoint.getx(), lastpoint.gety());

  matrix.reset();
  log.i(tag, "the distance : " + getdistance(lastpoint, movex, movey) / linepressedbitmap.getwidth());
  matrix.setscale(getdistance(lastpoint, movex, movey) / linepressedbitmap.getwidth(), 1f);
  matrix.posttranslate(lastpoint.getx(), lastpoint.gety() - linepressedbitmap.getwidth() / 2);
  canvas.drawbitmap(linepressedbitmap, matrix, null);

  canvas.rotate(-rotatedegrees.getdegrees(lastpoint, movex, movey), lastpoint.getx(), lastpoint.gety());
 }
}

// 根据point和坐标点计算出之间的距离
private float getdistance(point point, float movex, float movey) {
 point b = new point(movex,movey,null);
 return getdistance(point,b);
}

// 根据两个point计算出之间的距离
private float getdistance(point point, point nextpoint) {
 return (float) math.sqrt(math.pow(nextpoint.getx() - point.getx(), 2f) + math.pow(nextpoint.gety() - point.gety(), 2f));
}

drawline(canvas canvas) 整体的逻辑并不复杂,首先将 pressedpoint 中的所有格子依次遍历,将它们连线。之后若是用户的手指还有滑动的话,把最后一个格子和用户手指触摸的点连线。

总结

screenlockview 中的代码差不多就是这些了,实现效果还算不错吧,当然你也可以自己设置喜欢的九宫格图片,只要替换一下就可以了。如果对本篇文章有疑问可以留言。希望本文的内容对大家开发android能有所帮助。