Android自定义多宫格解锁控件

得意犹堪夸世俗,诏黄新湿字如鸦。这篇文章主要讲述Android自定义多宫格解锁控件相关的知识,希望能为你提供帮助。
在此之前,一直在想九宫格的实现方法,经过一个上午的初步研究终于完成了一个简单的N*N的宫格解锁组件,代码略显粗糙,仅仅做到简单的实现,界面等后期在做优化,纯粹是学习的目的,在算法上有点缺陷,如果有错误或者更好的方法,欢迎提出,相互学习。先来看一下预览图
九宫格效果展示 N=3 手指抬起

Android自定义多宫格解锁控件

文章图片

N=4 手指没有抬起
Android自定义多宫格解锁控件

文章图片

其他的废话不多说了,直接开始吧.....
实现步骤
  • 设置声明属性attrs.xml文件
  • 创建SeniorPoint.java文件
  • 创建View并重写其中的几个重要方法
  • 设置触摸事件,并进行数据处理
  • 设置回调函数,在Activity里面调用
设置声明属性很简单的xml内容,在res文件夹里面新建文件attrs.xml,将下面的内容写入即可。
< ?xml version="1.0" encoding="utf-8"?> < resources> < declare-styleable name="Lock"> < !--圆的半径--> < attr name="circleRadius" format="dimension"> < /attr> < !--圆的颜色--> < attr name="circleColor" format="color"> < /attr> < !--圆的线宽--> < attr name="circlrWidth" format="dimension"> < /attr> < !--线的宽度--> < attr name="LineWidth" format="dimension"> < /attr> < !--线的颜色--> < attr name="LineColor" format="color"> < /attr> < !--松开手之后线的颜色--> < attr name="LineColorAfterLeave" format="color"> < /attr> < !--圆圈的个数--> < attr name="circlrNumber" format="integer"> < /attr> < /declare-styleable> < /resources>

创建SeniorPoint.java文件SeniorPoint.java是一个Bean,里面保存着以圆心点的参考信息,代码如下:
package cn.example.tao.newview; import android.graphics.Point; /** * Created by Tao on 2017/2/3. */public class SeniorPoint extends Point { private boolean isSelect=false; public SeniorPoint(int x, int y, boolean isSelect) { super(x, y); this.isSelect = isSelect; } public boolean isSelect() { return isSelect; }public void setSelect(boolean select) { isSelect = select; } }

创建View并重写其中的几个重要方法至于怎么自定义View这里不在过多的赘述,可以看一下我的文章,里面写了怎么自定义一个齿轮的View http://www.jianshu.com/p/104a9d7eeefd ,创建java文件Lock.java,继承View组件
获取在Activity布局中的设置属性值在Activity的布局中的设置如下,具体每个属性的意义,请结合attrs.xml文件分析:
< ?xml version="1.0" encoding="utf-8"?> < LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="@mipmap/sky" > < cn.example.tao.newview.widget.Lock android:id="@+id/lock" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" tools:circleColor="#EAEAEA" tools:circlrWidth="2dp" tools:circleRadius="30dp" tools:LineColor="#EAEAEA" tools:LineWidth="3dp" tools:LineColorAfterLeave="#77E6D8" tools:circlrNumber="3" /> < /LinearLayout>

首先获得我们在xml文件中设置的属性值,代码如下:
public Lock(Context context, AttributeSet attrs) throws Exception { super(context, attrs); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Lock); //绘制圆的半径,默认值30dp circleRadius = typedArray.getDimension(R.styleable.Lock_circleRadius, dp2px(30)); //绘制圆形的颜色,默认白色 circleColor = typedArray.getColor(R.styleable.Lock_circleColor, Color.WHITE); //绘制圆形的宽度,默认3dp circleWidth = typedArray.getDimension(R.styleable.Lock_circlrWidth, dp2px(3)); //折线的颜色 lineColor = typedArray.getColor(R.styleable.Lock_LineColor, Color.GRAY); //折线的宽度 lineWidth = typedArray.getDimension(R.styleable.Lock_LineWidth, dp2px(1)); //连线完成后的线的颜色 lineColorAfterLeaver = typedArray.getColor(R.styleable.Lock_LineColorAfterLeave, Color.argb(255, 92, 186, 167)); //每行圆的数目,有事N*N,所以也是每列的数目,当然也可以根据次设置行数和列数不同的样式 circlrNumber=typedArray.getInt(R.styleable.Lock_circlrNumber,3); typedArray.recycle(); //设置圆的数量为0或者负数的时候异常抛出 if (circlrNumber< 1) throw new Exception("圆的数量不能为0或负数"); //用字符串的形式保存点的位置,比如01代表第0行1列,当然可以在回调函数根据自己的需要设计 password=new StringBuffer(); mPaint = new Paint(); mPaint.setStrokeWidth(dp2px(5)); //初始化保存圆心位置的二维数组 location = new SeniorPoint[circlrNumber][circlrNumber]; }private float dp2px(int i) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, i, getResources().getDisplayMetrics()); }

重写测量方法在此直接写代码,不解释了
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getValueByComplete(widthMeasureSpec); int height = getValueByComplete(heightMeasureSpec); setMeasuredDimension(width, width); }public int getValueByComplete(int value) { int size = MeasureSpec.getSize(value); int mode = MeasureSpec.getMode(value); int resultValue = https://www.songbingjia.com/android/0; if (mode == MeasureSpec.EXACTLY) { resultValue = size; } else { resultValue = (int) mPaint.descent(); if (mode == MeasureSpec.AT_MOST) resultValue = size; } return resultValue; }

重写绘制方法这里体现是对界面的绘制,主要是绘制圆和线,具体解释参考注释,在看一下这个懒到家的模型图,以N=3为参数画的,主要是注意一些点的设置
Android自定义多宫格解锁控件

文章图片

@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //设置绘制圆形的时候,圆心的移动步长 int setp_x = getWidth() / circlrNumber; int setp_y = getHeight() / circlrNumber; //设置第一个圆的位置,后面的圆形的绘制都是相对于第一各院的圆心的位置进行移动,移动的单位也就是setp_x和setp_y int mPaint_x = getWidth() / (2*circlrNumber), mPaint_y = getHeight() / (2*circlrNumber); //设置绘制圆形的画笔信息 mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(circleWidth); mPaint.setColor(circleColor); //循环,开始绘制圆形 for (int i = 0; i < circlrNumber; i++) for (int j = 0; j < circlrNumber; j++) { //此处开始绘保存圆心位置信息,设置为没有选中 if (location[i][j] == null) location[i][j] = new SeniorPoint(mPaint_x + j * setp_x, mPaint_y + i * setp_y, false); //开始绘制圆形,圆心坐标(mPaint_x + i * setp_x, mPaint_y + j * setp_y) canvas.drawCircle(mPaint_x + i * setp_x, mPaint_y + j * setp_y, circleRadius, mPaint); } //使用arrayList保存被选中的点的信息 if (arrayList != null & & arrayList.size() > 0) { //如果存在被选中的点,则开始进行连线操作 //重新设置画笔的参数信息 mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(lineWidth); //如果现在的点的位置为(0,0)那么说明,手已经抬起来了,将这显得颜色更改为设置颜色,否则的话使用另外的颜色 if (nowPoint != null & & nowPoint.x == 0 & & nowPoint.y == 0) mPaint.setColor(lineColorAfterLeaver); else mPaint.setColor(lineColor); //进行折线的绘制工作 for (int i = 0; i < arrayList.size(); i++) { canvas.drawPoint(arrayList.get(i).x, arrayList.get(i).y, mPaint); if (i != 0) { canvas.drawLine(arrayList.get(i - 1).x, arrayList.get(i - 1).y, arrayList.get(i).x, arrayList.get(i).y, mPaint); } } //如果手没有抬起,继续跟随手的位置来移动 if ((nowPoint != null & & nowPoint.x != 0 & & nowPoint.y != 0)) { canvas.drawLine(arrayList.get(arrayList.size() - 1).x, arrayList.get(arrayList.size() - 1).y, nowPoint.x, nowPoint.y, mPaint); }}}

设置触摸事件,并进行数据处理
@Override public boolean onTouchEvent(MotionEvent event) { //return super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //每次重新按下之前,要清除arratList中的列表保存的信息 arrayList.clear(); case MotionEvent.ACTION_MOVE: //判断当前的手指的位置有没有在院内,如果在院内返回这个圆的的圆心并设置改圆为选中状态,否则返回null SeniorPoint select = checkLocation(event.getX(), event.getY()); //配置点前手指的位置 if (nowPoint == null) nowPoint = new SeniorPoint((int) event.getX(), (int) event.getY(), false); else nowPoint.set((int) event.getX(), (int) event.getY()); if (select != null) select.setSelect(true); //重绘 invalidate(); break; case MotionEvent.ACTION_UP: //手指从屏幕离开后,将当前点的坐标设置为(0,0) nowPoint.set(0, 0); //离开后,读取已经选中的位置信息,返回给回调函数 //这里仅仅返回来的坐标点的位置,需要处理下才行 password.delete(0,password.length()); for (int i = 0; i < arrayList.size(); i++) { password.append("第"+i+"个点的坐标 X:" + arrayList.get(i).x + "Y:" + arrayList.get(i).y + " "); } if (onFinsh != null) onFinsh.leaver(password.toString()); //重绘 invalidate(); break; } return super.onTouchEvent(event); }public SeniorPoint checkLocation(float x, float y) { //此处循环检测九个点的位置,此处代码使用算法优化,没有必要循环判断位置 //后面有时间会专门写一个文章来分析下,追求更快的方法 double radio = dp2px(30); for (int i = 0; i < circlrNumber; i++) for (int j = 0; j < circlrNumber; j++) { double l = getLong(x, y, location[i][j]); if (l < = radio) { //如果已选中的列表的长度为0 或者长度不是0,但是也不能和arryList表中上一个值完全一样,这样才能添加 if (arrayList.size() ==0 || (arrayList.size()> =1 & & (location[i][j].x != arrayList.get(arrayList.size() - 1).x || location[i][j].y != arrayList.get(arrayList.size() - 1).y))) arrayList.add(location[i][j]); return location[i][j]; } } return null; } public double getLong(float x, float y, SeniorPoint point) { //返回点前手指的点到圆形的位置 double s = Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2); return Math.sqrt(s); }

设置回调函数,在Activity里面调用手抬起的时候,应该将选择的结果返回给Activity,在Activity中检查是否解锁成功,然后进行相应的处理.
首先定义接口,并在Lock.java文件中定义
private OnFinsh onFinsh; //set方法 public void setOnFinsh(OnFinsh onFinsh) { this.onFinsh = onFinsh; }//当手指抬起的时候,调用其leaver()方法,将结果回调到Activity中 //在上面的OnTouch事件中调用 if (onFinsh != null) onFinsh.leaver(password.toString());

public interface OnFinsh { void leaver(String password); }

在Activity中这样使用
package cn.example.tao.newview; import android.support.v4.app.FragmentActivity; import android.os.Bundle; import android.util.Log; import android.view.Window; import cn.example.tao.newview.widget.Lock; public class MainActivity extends FragmentActivity { //声明变量 private Lock lock; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); //绑定变量,并设置回调函数 lock= (Lock) findViewById(R.id.lock); lock.setOnFinsh(new Lock.OnFinsh() { @Override public void leaver(String password) { Log.e("PassWord",password); } }); } }

打印的结果如下:
N=4的选中10个点
Android自定义多宫格解锁控件

文章图片

后记当然也可以设置一下属性:
  • 是否可见,设置为布尔型数据,如果true则绘制直线,否则不绘制直线。
  • 增加选中一个点之后即调用的回调函数
  • 设置类型为填充圆或者点或者图片
    ......
目前还存在的问题:
  • 当手指处于某一点的时候,判断这个位置是不是在某一圆内,这里为了简单,使用了循环判断的方法,但是显然这种效率是很慢的,所以我想了下面的过程,不知是否合适:
    1、将view界面想象分割成N*N的界面
    2、首先大致判断手指的位置是不是在某个方格内,如果在,那么找到这个方格内的那个圆
    3、通过一些逻辑计算得到这个圆的圆心位置
【Android自定义多宫格解锁控件】本博客内容一致同步到本人的博客站点:http://www.zhoutaotao.xyz 欢迎访问留言交流







    推荐阅读