项目中用到手势解锁,然而没有在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轴同理,不懂的同学自己用笔画一下吧~
差不多就上面两个问题了