說明
手勢密碼這個有2種方法實現(xiàn),一個是直接繼承view然后在里面畫出來,并做事件處理。另一種就是用viewgroup來包了。奈何本人沒上過數(shù)學,所以我采取后者。
先看效果圖

錯誤

成功
分析實現(xiàn):
上面的指示部分后面在說,先看主體部分
-
里面的每一個格子是一個單獨的自定義view,隨便取個名字叫做lockView吧。
這個lockview有四種狀態(tài):- 普通NORMAL
- 按下DOWN
- 錯誤ERROR
- 成功SUCCESS
在繪制的時候根據(jù)不同狀態(tài)繪制不同效果。
-
外面則是一個viewgroup 取名叫GestureLockView,包裹著9個lockview.亂摸事件處理都在這個viewgroup中。
大概就是這樣。下面是具體代碼實現(xiàn).注釋相當詳細。
/**
每一個小格子view
*/
public class LockView extends View {
//初始狀態(tài)
public static final int NORMAL = 914;
//鼠標按下
public static final int DOWN = 669;
//密碼錯誤
public static final int ERROR = 873;
//密碼成功
public static final int SUCCESS = 440;
private static final String TAG = LockView.class.getSimpleName();
private
@Status
int status = NORMAL;
private Paint mPaint;
public LockView(Context context) {
super(context);
init();
}
public LockView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeWidth(2);
}
/**
不想用枚舉,這玩意好
*/
@IntDef({NORMAL, DOWN, ERROR,SUCCESS})
@Retention(RetentionPolicy.SOURCE)
public @interface Status {
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (status == ERROR) {
//密碼錯誤時,狀態(tài)顏色為紅色
mPaint.setColor(Color.RED);
}else if (status == DOWN){
//正在繪制中,狀態(tài)顏色為綠色
mPaint.setColor(Color.GREEN);
}else if (status == SUCCESS){
//成功
mPaint.setColor(Color.parseColor("#0094ff"));
} else {
mPaint.setColor(Color.WHITE);
}
mPaint.setStyle(Paint.Style.STROKE);
int center = getMeasuredHeight() / 2;
int radius = getWidth()/2;
//外圓
canvas.drawCircle(center, center, radius, mPaint);
if (status == ERROR || status == SUCCESS) {
//2條弧
if (status == ERROR){
mPaint.setColor(Color.RED);
}else{
mPaint.setColor(Color.parseColor("#0094ff"));
}
int r = radius / 2;
RectF rectF = new RectF(center - r, center - r, center + r, center + r);
canvas.drawArc(rectF, 70, 160, false, mPaint);
canvas.drawArc(rectF, -100, 140, false, mPaint);
//實心圓
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(center, center, radius / 8, mPaint);
}
}
/** 設置當前view狀態(tài) */
public void setStatus(@Status int status) {
this.status = status;
invalidate();
}
}
下面是ViewGroup
/** 手勢密碼容器 */
public class GestureLockView extends ViewGroup {
private static final String TAG = GestureLockView.class.getSimpleName();
private int colNum = 3;
/**
* 設置的9宮格密碼。當前view的id
*/
private final ArrayList<Integer> passwd = new ArrayList<>();
/**
* 當前選擇的密碼。當前view的id
*/
private final ArrayList<Integer> choose = new ArrayList<>();
/**
* 設置的密碼長度
*/
private int passwdLen;
/**
* 以2點為key,它們之間的中間點為value.用于記錄是否需要自動連接中間點
*/
private ArrayMap<String, Integer> betweenMap;
private LockListener mListener;
public GestureLockView(Context context, AttributeSet attrs) {
super(context, attrs);
betweenMap = new ArrayMap<>();
//映射2點與中間點
betweenMap.put("1,3", 2);
betweenMap.put("1,7", 4);
betweenMap.put("1,9", 5);
betweenMap.put("2,8", 5);
betweenMap.put("3,1", 2);
betweenMap.put("3,7", 5);
betweenMap.put("3,9", 6);
betweenMap.put("4,6", 5);
betweenMap.put("7,1", 4);
betweenMap.put("7,3", 5);
betweenMap.put("7,9", 8);
betweenMap.put("8,2", 5);
betweenMap.put("9,1", 5);
betweenMap.put("9,3", 6);
betweenMap.put("9,7", 8);
}
/** 根據(jù)子view來測量 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int resultWidth = 0;
int resultHeight = 0;
//添加9格view
int count = 9;
//默認margin
int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getContext().getResources().getDisplayMetrics());
//默認每個小格子寬度
int itemWidth = 200;
if (widthMode == MeasureSpec.EXACTLY) {
itemWidth = widthSize / colNum - margin;
}
//在添加子view前先清空
removeAllViews();
for (int i = 0; i < count; i++) {
int row = i / colNum; //當前view所在行
int col = i % colNum; //當前view所在列
//創(chuàng)建每個圖案
LockView lockView = new LockView(getContext());
MarginLayoutParams lp = new MarginLayoutParams(itemWidth, itemWidth);
lp.leftMargin = margin;
lp.topMargin = margin;
//添加一個標記
lockView.setId(i + 1);
addView(lockView, lp);
measureChild(lockView, widthMeasureSpec, heightMeasureSpec);
if (widthMeasureSpec != MeasureSpec.EXACTLY) {
//寬度取第一行。每個view寬度相+margin
if (row == 0) {
resultWidth += lockView.getMeasuredWidth() + margin; //左margin
}
}
if (heightMeasureSpec != MeasureSpec.EXACTLY) {
//高度取第一列的view高+margin
if (col == 0) {
resultHeight += lockView.getMeasuredHeight() + margin; //margin
}
}
}
resultWidth = resultWidth == 0 ? widthSize : resultWidth;
resultHeight = resultHeight == 0 ? heightSize : resultHeight;
setMeasuredDimension(resultWidth, resultHeight);
}
/** 按9宮格擺放 */
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
int col = i % colNum;
int row = i / colNum;
final View view = getChildAt(i);
final int childWidth = view.getMeasuredWidth();
final int childHeight = view.getMeasuredHeight();
MarginLayoutParams mp = (MarginLayoutParams) view.getLayoutParams();
int l, t, r, b;
/*
計算位置,套一下數(shù)值擺擺就知道是什么意思了
*/
l = col * childWidth + mp.leftMargin * (col + 1);
t = row * childHeight + mp.topMargin * (row + 1);
r = childWidth + l;
b = childHeight + t;
view.layout(l, t, r, b);
}
}
/**
* 繪制路徑用的
*/
private Path linePath = new Path();
/**
* 當前開始的view
*/
private LockView currentStartView;
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int count = getChildCount();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//每次按下時都表示從新開始,清除所有view的狀態(tài)
for (int i = 0; i < count; i++) {
((LockView) getChildAt(i)).setStatus(LockView.NORMAL);
}
choose.clear();
linePath.reset();
//根據(jù)按下的xy,獲取對應位置的view,有可能按的不在veiw的位置上,所以可能會為null
currentStartView = getChildByPos(x, y);
break;
case MotionEvent.ACTION_MOVE:
//手指開始亂摸,設置路徑線顏色為紅色
linePaint.setColor(Color.RED);
final LockView currentPosView = getChildByPos(x, y);
if (currentPosView != null) {
//有可能在按下的時候不在正確的view區(qū)域,所以會為null
if (currentStartView == null) {
currentStartView = currentPosView;
}
//2個view之間是否有需要連接的中間view
final int pos = getBetween(currentStartView, currentPosView);
if (pos != -1) {
//有需要進行連接的中間節(jié)點
LockView betweenView = (LockView) getChildAt(pos - 1);
//設置其選擇狀態(tài)
betweenView.setStatus(LockView.DOWN);
if (!choose.contains(betweenView.getId())) {
//添加到集合
choose.add(betweenView.getId());
}
}
int size = choose.size();
final int childViewId = currentPosView.getId();
//不保存連續(xù)相同的密碼
if ((0 == size) ||
(choose.get(size - 1) != childViewId)) {
choose.add(childViewId);
currentPosView.setStatus(LockView.DOWN);
/*
這2點的位置在圓心點旁邊附近。也可以設置成圓心點。
連接點:當手落在當前view上時,連接線要連接的點
*/
start.x = currentPosView.getLeft() / 2 + currentPosView.getRight() / 2;
start.y = currentPosView.getTop() / 2 + currentPosView.getBottom() / 2;
if (size == 0) {//表示是第一次。設置開頭點
linePath.moveTo(start.x, start.y);
} else {//連接到連接點
linePath.lineTo(start.x, start.y);
}
}
/*
這里一定要做。不然情況就是 只要經過了對角就會把對角一條線一起選擇
*/
currentStartView = currentPosView;
}
end.x = x;
end.y = y;
break;
case MotionEvent.ACTION_UP:
final int size = choose.size();
//手勢結束后,讓兩點合一,不顯示多余的尾巴
end.x = start.x;
end.y = start.y;
//選擇的密碼數(shù)量與設置的密碼長度不符合
if (size != passwdLen) {
for (int i = 0; i < size; i++) {
((LockView) getChildAt(choose.get(i) - 1)).setStatus(LockView.ERROR);
}
if (mListener != null && size > 0) {
int[] a = new int[choose.size()];
for (int i = 0; i < size; i++) {
a[i] = choose.get(i);
}
mListener.onError(a);
}
} else {
boolean isSuccess = true;
for (int i = 0; i < size; i++) {
final Integer item = choose.get(i);
//繪制的路徑中只要有一個不對,中判斷為錯誤
if (passwd.get(i).intValue() != item.intValue()) {
isSuccess = false;
break;
}
}
linePaint.setColor(isSuccess ? Color.parseColor("#0094ff") : Color.RED);
for (int i = 0; i < size; i++) {
final Integer item = choose.get(i);
((LockView) getChildAt(item - 1)).setStatus(isSuccess ? LockView.SUCCESS : LockView.ERROR);
}
if (isSuccess) {
if (mListener != null) {
mListener.onSuccess();
}
} else {
if (mListener != null) {
int[] a = new int[choose.size()];
for (int i = 0; i < size; i++) {
a[i] = choose.get(i);
}
mListener.onError(a);
}
}
}
if (mListener != null ) {
int[] a = new int[choose.size()];
for (int i = 0; i < size; i++) {
a[i] = choose.get(i);
}
mListener.onComplete(a);
}
break;
}
invalidate();
return true;
}
/**
* 根據(jù)坐標值,獲得view
*
* @param x
* @param y
* @return
*/
private LockView getChildByPos(int x, int y) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final LockView child = (LockView) getChildAt(i);
//x在當前view的寬度范圍內,y在當前view的高度范圍內
if ((x >= child.getLeft()) && (x <= child.getRight())
&& (y >= child.getTop()) && (y <= child.getBottom())) {
final int radiusX = child.getMeasuredWidth() / 2;
final int radiusY = child.getMeasuredHeight() / 2;
int centerX = child.getRight() - radiusX;
int centerY = child.getBottom() - radiusY;
/*
求點是否在圓上。公式:
到圓心的距離 是否大于半徑。半徑是R 如O(x,y)點圓心,任意一點P(x1,y1) (x-x1)*(x-x1)+(y-y1)*(y-y1)>R*R 那么在圓外 反之在圓內
手指經過當前view時,如果是在圓內,則判定有效,如果是在當前view的四邊角但是沒有在圓的范圍內,則無效
*/
if (!((centerX - x) * (centerX - x) + (centerY - y) * (centerY - y) > radiusX * radiusX)) {
return child;
}
}
}
return null;
}
/**
* 計算兩個view之間是否有需要連接的中間view.<br/>
* eg:如果點的3,拖到7,成對角,那么中間需要自動連接的就是5
*
* @param start
* @param end
* @return 返回中間view在父view的位置。沒有就返回-1
*/
private int getBetween(LockView start, LockView end) {
int s = getPosByView(start);
int e = getPosByView(end);
if (s == -1 || e == -1) {
return -1;
}
Integer betweenPos = getBetweenPosByKey("" + s + "," + e);
return betweenPos == null ? -1 : betweenPos;
}
/**
* 根據(jù)key,查找中間值。<br/>
* eg:key="3,7",value=5
*
* @param key
* @return value
*/
private Integer getBetweenPosByKey(String key) {
return betweenMap.get(key);
}
/**
* 根據(jù)view計算其在父view中所在位置
*
* @param view
* @return 沒有返回-1
*/
private int getPosByView(LockView view) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
if (getChildAt(i) == view) {
return (i + 1);
}
}
return -1;
}
private Paint linePaint = new Paint();
private Point start = new Point();
private Point end = new Point();
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(4);
//繪制連接線
canvas.drawPath(linePath, linePaint);
if (choose.size() > 0) {
if (end.x != 0 && end.y != 0) {
//這個主要是當手勢落在某個view上時,能自動把點定位連接到view的連接點
canvas.drawLine(start.x, start.y, end.x, end.y, linePaint);
}
}
}
/**
* 設置密碼
*
* @param pwd
*/
public void setPasswd(int[] pwd) {
this.passwdLen = pwd.length;
this.passwd.clear();
for (int i = 0; i < this.passwdLen; i++) {
this.passwd.add(pwd[i]);
}
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
public void setListener(LockListener listener) {
mListener = listener;
}
public interface LockListener {
void onComplete(int[] a);
void onSuccess();
void onError(int[] integers);
}
}
頂部的指示器
/**
* 主要就是 一個9位的數(shù)組。如果不設置密碼的時候,全部都是0
* 如果某位設置了密碼。則把數(shù)組對應的位置值設置1
* eg: 沒設置密碼時
* passwd的內容 0 0 0 0 0 0 0 0 0
* 設置密碼32147
* passwd的內容 1 1 1 1 0 0 1 0 0
* 最后在繪制的時候根據(jù)passwd的每個值0或1來繪制實心或空心圓。
* 因為只是做頂部的展示用,沒有其它,所以就這么來
*/
public class MiniLockView extends View {
private Paint mPaint;
private final byte[] passwd = new byte[9];
private int mColNum;
public MiniLockView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mColNum = 3;
Arrays.fill(passwd, (byte) 0);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int pwdColor = Color.parseColor("#0094ff");
int radius =20;
int count = passwd.length;
for (int i = 0; i < count; i++) {
int row = i / mColNum;
int col = i % mColNum;
final byte b = passwd[i];
//如果當前位為0,則表示譔位不是密碼位
//如果為1,表示是密碼位
if (b == 0) {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.LTGRAY);
} else {
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(pwdColor);
}
int x= (radius * 2+radius) * (col+1 );
int y = (radius * 2+radius) * (row+1 );
canvas.drawCircle(x,y , radius, mPaint);
}
}
/**
* 設置密碼
* @param pwd
*/
public void setPasswd(int[] pwd) {
Arrays.fill(passwd, (byte) 0);
final int length = pwd.length;
for (int i = 0; i < length; i++) {
final int index = pwd[i] - 1;
this.passwd[index] = 1;
}
invalidate();
}
}
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:background="#000000"
android:gravity="center"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.xp.mycustomviewapplication.MiniLockView
android:id="@+id/mini_lock_view"
android:layout_width="100dp"
android:layout_height="100dp"/>
<com.example.xp.mycustomviewapplication.GestureLockView
android:id="@+id/lock_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
MainActivity的oncreate
final GestureLockView gestureLockView = (GestureLockView) findViewById(R.id.lock_view);
final MiniLockView miniLockView = (MiniLockView) findViewById(R.id.mini_lock_view);
int[] a =new int[]{2, 6, 9, 8, 7, 4, 2};
gestureLockView.setPasswd(a);
gestureLockView.setListener(new GestureLockView.LockListener() {
@Override
public void onComplete(int[] a) {
if (a.length !=0)
Toast.makeText(MainActivity.this, "onComplete:"+ Arrays.toString(a), Toast.LENGTH_SHORT).show();
miniLockView.setPasswd(a);
}
@Override
public void onSuccess() {
}
@Override
public void onError(int[] integers) {
}
});
寫在最后
本人也是一個自定義view的low,學習中,代碼寫得不是很嚴謹,還有些小東西沒做處理。主要是學習一下思路。其它的在基于項目中的需求在修改下就好了。