自定義九宮格手勢解鎖
阿新 • • 發佈:2019-01-31
專案中用到手勢解鎖,然而沒有在GitHub上找到想要的樣式= =,只好自己來定義了,下面來看程式碼~~
基本上很多應用的手勢解鎖全都是九宮格的,內部內就是九個小圈圈而已。那麼我們就先來自定義這個小圈圈吧~
圈圈的顏色選擇狀態有大致有三種狀態,所以我定義了一個列舉來區分
package com.juzisang.com.library;
/**
* Created by 橘子桑 on 2016/3/27.
*/
public enum LockState {
SELECT_STATE,//選中
ERRER_STATE, //錯誤
DEFAULT_COLOR //預設
}
圈圈分為邊框,內部填充色,還有內部圓。所以我定義了三個畫筆來區分。
package com.juzisang.com.library;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by 橘子桑 on 2016/3/27.
*/
public class MarkerView extends View {
//是否顯示內部的圈圈
private boolean mInsideNodeShow;
//寬度
protected int mContentWidth;
//寬度
protected int mContentRadius;
//選中狀態
protected LockState mCurrentState = LockState.DEFAULT_COLOR;
//畫邊框畫圓的的畫筆
private Paint mNodeFramePaint;
private Paint mNodeCirclePaint;
private Paint mNodeFullPaint;
//預設的顏色
private int mDefaultColor = Color.parseColor("#757575");
private int mDefailtFullColor = Color.parseColor("#64757575");
private int mNodeDefaultColor = Color.parseColor("#757575");
//選中的顏色
private int mSelectColor = Color.parseColor("#7ECEF4");
private int mFrameSelectFullColor = Color.parseColor("#647ECEF4");
private int mNodeSelectColor = Color.parseColor("#7ECEF4");
//錯誤時候的顏色
private int mErrerColor = Color.parseColor("#EC6941");
private int mErrerFullColor = Color.parseColor("#64EC6941");
private int mErrerNodeColor = Color.parseColor("#EC6941");
//邊框的寬度
private int mFrameLineWidth;
private int mNodeRadius;
//每個圈圈的內邊距
private int mNodePadding;
//觸控有效的範圍
private float mTouchRatio;
//當前標記的位置
private int mNum;
public MarkerView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context, attrs, 0);
}
public MarkerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs, defStyleAttr);
}
//以後外部佈局傳來的引數
public MarkerView(Context context, int mDefaultColor, int mDefailtFullColor, int mNodeDefaultColor,
int mSelectColor, int mFrameSelectFullColor, int mNodeSelectColor,
int mErrerColor, int mErrerFullColor, int mErrerNodeColor,
int mFrameLineWidth, int mNodeRadius, int mNodePadding, boolean insideNodeShow) {
super(context);
this.mInsideNodeShow = insideNodeShow;
this.mDefaultColor = mDefaultColor;
this.mDefailtFullColor = mDefailtFullColor;
this.mNodeDefaultColor = mNodeDefaultColor;
this.mSelectColor = mSelectColor;
this.mFrameSelectFullColor = mFrameSelectFullColor;
this.mNodeSelectColor = mNodeSelectColor;
this.mErrerColor = mErrerColor;
this.mErrerFullColor = mErrerFullColor;
this.mErrerNodeColor = mErrerNodeColor;
this.mFrameLineWidth = mFrameLineWidth;
this.mNodeRadius = mNodeRadius;
this.mNodePadding = mNodePadding;
//內邊距
setPadding(mNodePadding, mNodePadding, mNodePadding, mNodePadding);
//外部圓
mNodeFramePaint = new Paint();
mNodeFramePaint.setColor(mDefaultColor);
mNodeFramePaint.setAntiAlias(true);
mNodeFramePaint.setStrokeWidth(mFrameLineWidth);
mNodeFramePaint.setStyle(Paint.Style.STROKE);//只畫出邊框
//內部填充色
mNodeFullPaint = new Paint();
mNodeFullPaint.setColor(mDefailtFullColor);
mNodeFullPaint.setStyle(Paint.Style.FILL);
mNodeFullPaint.setAntiAlias(true);
//內部圓
mNodeCirclePaint = new Paint();
mNodeCirclePaint.setColor(mNodeDefaultColor);
mNodeCirclePaint.setStyle(Paint.Style.FILL);//填充
mNodeCirclePaint.setAntiAlias(true);
}
//取當前透明度的百分比
public int getFullAlpha(int color, float ratio) {
return Color.argb((int) (Color.alpha(color) * ratio), Color.red(color), Color.green(color), Color.blue(color));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mContentWidth = getWidth();
mContentRadius = mContentWidth / 2 - Math.abs(getPaddingLeft()) - mFrameLineWidth / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
switch (mCurrentState) {
case DEFAULT_COLOR: //預設
mNodeFramePaint.setColor(mDefaultColor);
mNodeFullPaint.setColor(mDefailtFullColor);
mNodeCirclePaint.setColor(mNodeDefaultColor);
//外圓
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mContentRadius, mNodeFramePaint);
//填充色
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mContentRadius - mFrameLineWidth / 2, mNodeFullPaint);
//中心圓
if (mInsideNodeShow)
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mNodeRadius, mNodeCirclePaint);
break;
case ERRER_STATE://錯誤
mNodeFramePaint.setColor(mErrerColor);
mNodeFullPaint.setColor(mErrerFullColor);
mNodeCirclePaint.setColor(mErrerNodeColor);
//外圓
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mContentRadius, mNodeFramePaint);
//填充色
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mContentRadius - mFrameLineWidth / 2, mNodeFullPaint);
//中心圓
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mNodeRadius, mNodeCirclePaint);
break;
case SELECT_STATE://選中
mNodeFramePaint.setColor(mSelectColor);
mNodeFullPaint.setColor(mFrameSelectFullColor);
mNodeCirclePaint.setColor(mNodeSelectColor);
//外圓
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mContentRadius, mNodeFramePaint);
//填充色
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mContentRadius - mFrameLineWidth / 2, mNodeFullPaint);
//中心圓
canvas.drawCircle(mContentWidth / 2, mContentWidth / 2, mNodeRadius, mNodeCirclePaint);
break;
}
}
//設定狀態,並且重繪
public void setState(LockState CurrentState) {
mCurrentState = CurrentState;
invalidate();
}
//是否選中
public boolean isHighLighted() {
if (mCurrentState == LockState.SELECT_STATE || mCurrentState == LockState.ERRER_STATE) {
return true;
}
return false;
}
//中心點X
public int getCenterX() {
return (getLeft() + getRight()) / 2;
}
//中心點Y
public int getCenterY() {
return (getTop() + getBottom()) / 2;
}
//設定圈圈在手勢鎖當中的位置
protected void setNum(int num) {
mNum = num;
}
protected int getNum() {
return mNum;
}
}
以上就是一個簡單的圓了
那麼,自定義View當然會有自定義屬性,所以有這麼多T0T,不要問我為什麼這麼多屬性,任性= =(其實我還想寫更多),自定義屬性的方法
<!-- 線的顏色 -->
<attr name="lineColor" format="color" />
<!-- 線的寬度 -->
<attr name="lineWidth" format="dimension" />
<!--預設顏色 -->
<attr name="defaultColor" format="color" />
<!--預設時的填充色-->
<attr name="defaultFullColor" format="color" />
<!--預設內部圓顏色-->
<attr name="defaultNodeColor" format="color" />
<!-- ======================================================= -->
<!-- 邊框選中時邊框的顏色 -->
<attr name="selectColor" format="color" />
<!-- 邊框選中時內部的填充色 -->
<attr name="selectFrameFullColor" format="color" />
<!--內部圓圈選中時的顏色-->
<attr name="selectNodeColor" format="color" />
<!-- ======================================================= -->
<!-- 錯誤的顏色 -->
<attr name="errorColor" format="color" />
<!--錯誤時內部的填充色-->
<attr name="errorFullColor" format="color" />
<!-- 錯誤時的顏色 -->
<attr name="errorNodeColor" format="color" />
<!-- ======================================================= -->
<!--邊框的的寬度-->
<attr name="frameLineWidth" format="dimension" />
<!-- 內部圓圈的寬度 -->
<attr name="nodeRadius" format="dimension" />
<!--內邊距-->
<attr name="nodePadding" format="dimension" />
<!--觸控有效的比例-->
<attr name="touchRatio" format="float" />
<!-- 是否顯示內部的圓圈 -->
<attr name="insideNodeShow" format="boolean"/>
LockView的程式碼
package com.juzisang.com.library;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
import java.util.ArrayList;
/**
* Created by 橘子桑 on 2016/3/27.
*/
public class LockView extends ViewGroup {
//畫連線線的畫筆
private Paint mLinePaint;
//可以觸控的區域百分比
private float mTouchRatio;
//線的顏色
protected int mLineColor;
//先的寬度
protected float mLineWidth;
//已經選中了的View
ArrayList<MarkerView> mNodeViews = new ArrayList<>();
//儲存密碼
protected StringBuilder pawBuilder = new StringBuilder();
//當前手指觸控的x座標
protected float x;
//當前手指觸控的y座標
protected float y;
//回撥
private onLockCallback mOnLockCallback;
protected int mDefaultColor;
protected int mSelectColor;
protected int mErrerColor;
//禁用手勢鎖
private boolean mLockScreen;
private boolean isTouch;
//是否把連線線繪製在子View的上面
private boolean mLineTop = false;
//手指離開立即重繪
private boolean mFingerLeaveRedraw = true;
public LockView(Context context) {
this(context, null);
}
public LockView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs, defStyleAttr);
}
protected void initView(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray r = context.obtainStyledAttributes(attrs, R.styleable.MarkerView);
boolean insideNodeShow = r.getBoolean(R.styleable.LockView_insideNodeShow, false);
//預設的顏色
mDefaultColor = r.getColor(R.styleable.LockView_defaultColor, context.getResources().getColor(android.R.color.holo_blue_dark));
int mDefailtFullColor = r.getColor(R.styleable.LockView_defaultFullColor, getFullAlpha(mDefaultColor, 0.3F));
int mNodeDefaultColor = (int) r.getColor(R.styleable.LockView_defaultNodeColor, mDefaultColor);
//選中的顏色
mSelectColor = (int) r.getColor(R.styleable.LockView_selectColor, context.getResources().getColor(android.R.color.holo_blue_light));
int mFrameSelectFullColor = r.getColor(R.styleable.LockView_selectFrameFullColor, getFullAlpha(mSelectColor, 0.3F));
int mNodeSelectColor = r.getColor(R.styleable.LockView_selectNodeColor, mSelectColor);
//錯誤時候的顏色
mErrerColor = r.getColor(R.styleable.LockView_errorColor, context.getResources().getColor(android.R.color.holo_red_light));
int mErrerFullColor = r.getColor(R.styleable.LockView_errorFullColor, getFullAlpha(mErrerColor, 0.3F));
int mErrerNodeColor = r.getColor(R.styleable.LockView_errorNodeColor, mErrerColor);
//圓框變的寬度
int mFrameLineWidth = (int) r.getDimension(R.styleable.LockView_frameLineWidth, DensityUtils.dip2px(context, 5));
//內圓的直徑
int mNodeRadius = (int) r.getDimension(R.styleable.LockView_nodeRadius, DensityUtils.dip2px(context, 5));
//內邊距
int mNodePadding = (int) r.getDimension(R.styleable.LockView_nodePadding, DensityUtils.dip2px(context, 10));
//觸控有效區域
mTouchRatio = r.getFloat(R.styleable.LockView_touchRatio, mTouchRatio);
mLineColor = r.getColor(R.styleable.LockView_lineColor, mDefaultColor);
mLineWidth = r.getDimension(R.styleable.LockView_lineWidth, DensityUtils.dip2px(context, 5));
r.recycle();
//設定線的顏色
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setColor(mLineColor);
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setStrokeWidth(mLineWidth);
mLinePaint.setStrokeCap(Paint.Cap.ROUND);
mLinePaint.setStrokeJoin(Paint.Join.ROUND);
for (int i = 0; i < 9; i++) {
MarkerView view = new MarkerView(context, mDefaultColor, mDefailtFullColor, mNodeDefaultColor, mSelectColor, mFrameSelectFullColor, mNodeSelectColor,
mErrerColor, mErrerFullColor, mErrerNodeColor, mFrameLineWidth, mNodeRadius, mNodePadding, insideNodeShow);
view.setNum(i + 1);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
view.setLayoutParams(params);
addView(view);
}
// 清除FLAG,否則 onDraw() 不會呼叫,原因是 ViewGroup 預設透明背景不需要呼叫 onDraw()
setWillNotDraw(false);
}
public int getFullAlpha(int color, float ratio) {
return Color.argb((int) (Color.alpha(color) * ratio), Color.red(color), Color.green(color), Color.blue(color));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int size = Math.min(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); // 測量寬度
setMeasuredDimension(size, size);
for (int i = 0; i < getChildCount(); i++) {
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
float areaWidth = (r - l - getPaddingLeft() * 2) / 3;
for (int n = 0; n < 9; n++) {
MarkerView node = (MarkerView) getChildAt(n);
// 獲取3*3宮格內座標
int row = n / 3;
int col = n % 3;
//加上內間距
int left = (int) (getPaddingLeft() + col * areaWidth);
int top = (int) (getPaddingTop() + row * areaWidth);
int right = (int) (left + areaWidth);
int bottom = (int) (top + areaWidth);
node.layout(left, top, right, bottom);
}
}
}
/**
* 設定連線線是否繪製在子View的上面
* true 繪製在子View的上面
* false 繪製在子View的下面
*
* @param isLineTop 設定連線線是否繪製在子View的上面
*/
public void setLineTop(boolean isLineTop) {
mLineTop = isLineTop;
invalidate();
}
/**
* 設定連線線是否繪製在子View的上面
* true 繪製在子View的上面
* false 繪製在子View的下面
*/
public boolean getLineTop() {
return mLineTop;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (getLockScreen()) {
invalidate();
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//恢復預設
resetDefault();
x = event.getX();
y = event.getY();
isTouch = true;
break;
case MotionEvent.ACTION_MOVE:
x = event.getX(); // 這裡要實時記錄手指的座標
y = event.getY();
MarkerView nodeView = getNodeAt(x, y);
//沒有選中
if (nodeView != null && !nodeView.isHighLighted()) {
nodeView.setState(LockState.SELECT_STATE);
mNodeViews.add(nodeView);
//進度
if (mOnLockCallback != null) {
pawBuilder.setLength(0);
for (MarkerView markerView : mNodeViews) {
pawBuilder.append(markerView.getNum());
}
mOnLockCallback.onProgress(pawBuilder.toString(), nodeView.getNum());
}
}
if (mNodeViews.size() > 0) {
invalidate();
}
break;
case MotionEvent.ACTION_UP:
LogUtils.i("手指擡起了");
isTouch = false;
pawBuilder.setLength(0);
if (mNodeViews.size() <= 0) return true;
pawBuilder.delete(0, pawBuilder.length());
if (mOnLockCallback != null) {
for (MarkerView markerView : mNodeViews) {
pawBuilder.append(markerView.getNum());
}
mOnLockCallback.onFinish(pawBuilder.toString());
}
if (mFingerLeaveRedraw) {
resetDefault();
} else {
invalidate();
}
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
//線畫在子view的下面
if (!mLineTop) onDrawLock(canvas);
}
//畫子View的地方
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
//放在這裡的原因是,線會被子View擋到
if (mLineTop) onDrawLock(canvas);
}
/**
* 畫圖的方法
*/
private void onDrawLock(Canvas canvas) {
//螢幕鎖住了,只畫起點到終點的
if (getLockScreen()) {
onDrawNodeViewLock(canvas);
return;
}
if (isTouch || mFingerLeaveRedraw) {
//從第一個和最後一個的連線線
onDrawNodeViewLock(canvas);
//最後一個點,到手指之間的線
if (mNodeViews.size() > 0) {
MarkerView lastNode = mNodeViews.get(mNodeViews.size() - 1);
canvas.drawLine(lastNode.getCenterX(), lastNode.getCenterY(), x, y, mLinePaint);
}
} else {
//如果手指離開螢幕,並且設定了手指離開立即重繪
onDrawNodeViewLock(canvas);
}
}
private void onDrawNodeViewLock(Canvas canvas) {
//從第一個和最後一個的連線線
for (int i = 1; i < mNodeViews.size(); i++) {
MarkerView frontNode = mNodeViews.get(i - 1);
MarkerView backNode = mNodeViews.get(i);
canvas.drawLine(frontNode.getCenterX(), frontNode.getCenterY(), backNode.getCenterX(), backNode.getCenterY(), mLinePaint);
}
}
/**
* 獲取給定座標點的Node,返回null表示當前手指在兩個Node之間
*/
private MarkerView getNodeAt(float x, float y) {
for (int n = 0; n < getChildCount(); n++) {
MarkerView node = (MarkerView) getChildAt(n);
//計算觸控區域以外的距離
float ratioPadding = (node.getWidth() - (node.getWidth() * mTouchRatio)) / 2;
if (!(x >= node.getLeft() + ratioPadding && x < node.getRight() - ratioPadding)) {
continue;
}
if (!(y >= node.getTop() + ratioPadding && y < node.getBottom() - ratioPadding)) {
continue;
}
return node;
}
return null;
}
/**
* 設定連線線的顏色
*
* @param color 顏色值
*/
public void setLineColor(int color) {
mLinePaint.setColor(color);
}
/**
* 手指離開立即重繪
*/
public void setfingerLeaveRedraw(boolean mFingerLeaveRedraw) {
this.mFingerLeaveRedraw = mFingerLeaveRedraw;
}
public boolean getfingerLeaveRedraw() {
return this.mFingerLeaveRedraw;
}
/**
* 重置狀態 為預設狀態
*/
public void resetDefault() {
setState(LockState.DEFAULT_COLOR);
mNodeViews.clear();
}
/**
* 重置狀態錯誤狀態
*/
public void resetErrer() {
setState(LockState.ERRER_STATE);
}
/**
* 重置為選中狀態
*/
public void resetSelect() {
setState(LockState.SELECT_STATE);
}
/**
* 鎖屏,不允許觸控
*/
public void LockScreen(boolean isScreen) {
mLockScreen = isScreen;
}
public boolean getLockScreen() {
return mLockScreen;
}
public void setState(LockState state) {
switch (state) {
case DEFAULT_COLOR:
case SELECT_STATE:
setLineColor(mSelectColor);
break;
case ERRER_STATE:
setLineColor(mErrerColor);
break;
}
int size = mNodeViews.size();
for (int i = 0; i < size; i++) {
mNodeViews.get(i).setState(state);
}
invalidate();
}
public void setLockCallback(onLockCallback lockCallback) {
mOnLockCallback = lockCallback;
}
//回撥
public interface onLockCallback {
void onProgress(String paw, int current);
void onFinish(String paw);
}
}
以上註釋都寫的很清楚了,下面講一下遇到的一些問題。
1.畫出來的線被上面的圈圈覆蓋了。
通過百度,知道ViewGroup的onDraw是畫布局中的內容的,畫子View的的方法在這個方法的後面執行,所以ViewGroup的內容會被子View覆蓋,那麼怎麼才能把連線線畫在子View的上面呢,很簡單
只要在畫子View的方法中執行就好了
//畫子View的地方
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
//放在這裡的原因是,線會被子View擋到
if (mLineTop) onDrawLock(canvas);
}
下面是View的draw()方法
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
//這裡就是畫子View的方法了
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
2.怎麼設定觸控的區域?
/**
* 獲取給定座標點的Node,返回null表示當前手指在兩個Node之間
*/
private MarkerView getNodeAt(float x, float y) {
for (int n = 0; n < getChildCount(); n++) {
MarkerView node = (MarkerView) getChildAt(n);
//計算觸控區域以外的距離
float ratioPadding = (node.getWidth() - (node.getWidth() * mTouchRatio)) / 2;
if (!(x >= node.getLeft() + ratioPadding && x < node.getRight() - ratioPadding)) {
continue;
}
if (!(y >= node.getTop() + ratioPadding && y < node.getBottom() - ratioPadding)) {
continue;
}
return node;
}
return null;
}
看上面程式碼,
根據圓圈的寬度減去可觸控區域的長度除2,得到可觸控區域距離邊框的距的距離。
光看程式碼看著有點圓,畫個圖看一下吧
畫個圖是不是清晰很多,只要用getLeft+邊距,和getRight-邊距,就能得到可觸控區域在x軸上的範圍了,Y軸同理,不懂的同學自己用筆畫一下吧~
差不多就上面兩個問題了