这个教程呢,并不是up原创的,而是参考了网上的一篇素材 https://www.300168.com/yidong/show-1593.html
但是代码中存在一些bug,并做了一些改进和优化,(bug如下中间经过的键未能被选中)并没有轻视原楼主的意思,还是很棒的
修改方法是只要在ACTION_MOVE里加这样一段代码就可以了(源代码待会儿我会贴出来的,大家不要觉得麻烦
第一个javaBean
/**
* 锁屏中点的信息类
*/
public class PointInfo {
private int mId; //点的id
private int mNextId;//当前点指向的下一个点的id
private boolean mSelected;//是否选中
private int mDefaultX;//默认图片左上角的X坐标
private int mDefaultY;//默认图片左上角Y的坐标
private int mSelectedX; // 被选中时图片的左上角X坐标
private int mSelectedY; // 被选中时图片的左上角Y坐标
private int mRangeWidth;//范围
private int mSelectedWidth;//选中显示的范围
/**
* 构造方法
* selectedWidth 选中时图片选中的范围
* rangeWidth 触摸能响应事件的范围 如果rangeWidth > selectedWith 相当于加了个padding
*/
public PointInfo(int id, int defaultX, int defaultY, int selectedX,
int selectedY, int selectedWidth, int rangeWidth) {
mId = id;
mNextId = mId;
mDefaultX = defaultX;
mDefaultY = defaultY;
mSelectedX = selectedX;
mSelectedY = selectedY;
mSelectedWidth = selectedWidth;//选中范围
mRangeWidth = rangeWidth;
}
public void setNextId(int nextId) {
mNextId = nextId;
}
public void setSelected(boolean selected) {
mSelected = selected;
}
public int getId() {
return mId;
}
public int getNextId() {
return mNextId;
}
public boolean isSelected() {
return mSelected;
}
public int getDefaultX() {
return mDefaultX;
}
public int getDefaultY() {
return mDefaultY;
}
public int getSelectedX() {
return mSelectedX;
}
public int getSelectedY() {
return mSelectedY;
}
/**
* 是否有下一个id
* @return
*/
public boolean hasNextId() {
return mNextId != mId;
}
/**
* 得到中心X
* @return
*/
public int getCenterX(){
return mSelectedX + mSelectedWidth/2;
}
/**
* 得到中心Y
* @return
*/
public int getCenterY(){
return mSelectedY + mSelectedWidth/2;
}
/**
* 坐标(x,y)是否在当前点的范围内
* @return
*/
public boolean isInMyPlace(int x, int y) {
boolean inX = x >= getCenterX() - mRangeWidth/2 && x <= getCenterX() + mRangeWidth/2;
boolean inY = y >= getCenterY() - mRangeWidth/2 && y <= getCenterY() + mRangeWidth/2;
return (inX && inY);//只有两个都为真时才为真
}
}
第二个主类
package com.example.ninepointtask.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.example.ninepointtask.R;
import com.example.ninepointtask.bean.PointInfo;
import com.example.ninepointtask.util.DensityUtil;
import java.util.ArrayList;
import java.util.List;
/**
* Created by xiaohan on 2018/4/25.
*/
public class NinePointView extends View {
private static final String TAG = "NinePointView";
private Paint mLinePaint = new Paint();//线画笔
private Bitmap mDefaultBitmap;
private int mDefaultBitmapRadius;//宽为20dp
private Bitmap mSelectedBitmap;//选中的图片
private int mSelectedBitmapDiameter;//选中图片的直径
private int mSelectedBitmapRadius;//选中图片的半径
//private Bitmap mFailBitmap;//失败图
private int mPointRange;//点的半径
private PointInfo[] mPoints = new PointInfo[25];//点信息
private int mWidth, mHeight;//用于记录view的宽高
private int mMoveX, mMoveY;//获取用户的移动坐标
private int mStartX, mStartY;//开始坐标
private boolean mIsUp = false;//用户是否抬起
private List<Integer> mPasswordArr = new ArrayList<>();//存放用户输入的密码
private int mColumnNum;
public NinePointView(Context context) {
super(context);
init();
}
public NinePointView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public NinePointView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化
*/
private void init() {
mColumnNum = (int) Math.sqrt(mPoints.length);
mDefaultBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lock);//默认图形
mDefaultBitmapRadius = DensityUtil.dipToPx(40);//默认图形的大小半径
mSelectedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.indicator_lock_area);//选中的图片
mSelectedBitmapDiameter = DensityUtil.dipToPx(40);//选中的图形直径为40
mPointRange = DensityUtil.dipToPx(55);//直径给个50吧
mSelectedBitmapRadius = mSelectedBitmapDiameter / 2;//选中时图片的半径
Matrix matrix = new Matrix();
matrix.postScale(mDefaultBitmapRadius * 2 / (float) mDefaultBitmap.getWidth(), mDefaultBitmapRadius * 2 / (float) mDefaultBitmap.getHeight());//先压缩一下
mDefaultBitmap = Bitmap.createBitmap(mDefaultBitmap, 0, 0, mDefaultBitmap.getWidth(),
mDefaultBitmap.getHeight(), matrix, true);//压缩一下默认图片
Matrix matrix1 = new Matrix();
matrix1.postScale(mSelectedBitmapDiameter / (float) mSelectedBitmap.getWidth(), mSelectedBitmapDiameter / (float) mSelectedBitmap.getHeight());//先压缩一下
mSelectedBitmap = Bitmap.createBitmap(mSelectedBitmap, 0, 0, mSelectedBitmap.getWidth(), mSelectedBitmap.getHeight(), matrix1, true);
initPaint();
}
/**
* 初始化一下画笔
*/
private void initPaint() {
mLinePaint.setColor(Color.GRAY);//线性画笔的颜色
mLinePaint.setStrokeWidth(DensityUtil.dipToPx(5));//线的宽度默认为5dp
mLinePaint.setAntiAlias(true);
mLinePaint.setStrokeCap(Paint.Cap.ROUND);//圆帽
}
/**
* 初始化点数组
*/
private void initPoint() {
int len = mPoints.length;
boolean HGreaterThanW = (mHeight > mWidth);//高是否大于宽
int selectedSpacing = (Math.min(mWidth, mHeight) - mSelectedBitmapDiameter * mColumnNum) / (mColumnNum + 1);//分割宽度为宽度和高度中小的值减去6个圆的大小除以列数加1
mPointRange = Math.min(mPointRange, mSelectedBitmapDiameter + selectedSpacing);防止点范围越界
int selectedX = HGreaterThanW ? selectedSpacing : (mWidth - mHeight) / 2 + selectedSpacing;//图片的左上角X点坐标
int selectedY = HGreaterThanW ? mHeight - mWidth + selectedSpacing : selectedSpacing;//左上角Y坐标
int defaultX = selectedX + mSelectedBitmapRadius - mDefaultBitmapRadius;//默认X
int defaultY = selectedY + mSelectedBitmapRadius - mDefaultBitmapRadius;//选中的中心点
for (int i = 0; i < len; i++) {
if ((i % mColumnNum) == 0 && i != 0) {
selectedX = HGreaterThanW ? selectedSpacing : (mWidth - mHeight) / 2 + selectedSpacing; //重归开头
selectedY += mSelectedBitmapDiameter + selectedSpacing;//向下偏移
defaultX = selectedX + mSelectedBitmapRadius - mDefaultBitmapRadius;
defaultY += mSelectedBitmapDiameter + selectedSpacing;//向下偏移
}
mPoints[i] = new PointInfo(i, defaultX, defaultY, selectedX, selectedY, mSelectedBitmapDiameter, mPointRange);//初始化
selectedX += mSelectedBitmapDiameter + selectedSpacing;//向右偏
defaultX += mSelectedBitmapDiameter + selectedSpacing;//向右偏
}
}
/**
* 用户的触摸事件
* 这个DOWN和MOVE、UP是成对的,如果没从UP释放,就不会再获得DOWN;
* 而获得DOWN时,一定要确认消费该事件,否则MOVE和UP不会被这个VIEW的onTouchEvent接收
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mIsUp) {
finishDraw();
}
handleEvent(event);
return true;
}
/**
* 结束绘制 重置一下
*/
private void finishDraw() {
if (mIsUp) {
for (PointInfo pointInfo : mPoints) {
pointInfo.setSelected(false);//
pointInfo.setNextId(pointInfo.getId());
}
mPasswordArr.clear();//清除已存的密码
mIsUp = false;
invalidate();//重绘一下
}
}
/**
* 处理事件
*
* @param event
*/
private void handleEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int downX = (int) event.getX();
int downY = (int) event.getY();
for (PointInfo pointInfo : mPoints) {
if (pointInfo.isInMyPlace(downX, downY)) {
pointInfo.setSelected(true);//选中
mStartX = pointInfo.getCenterX();
mStartY = pointInfo.getCenterY();
mPasswordArr.add(pointInfo.getId());//将id加进去
break;//找到了就不用再找了
}
}
invalidate();//重绘
break;
case MotionEvent.ACTION_MOVE:
mMoveX = (int) event.getX();
mMoveY = (int) event.getY();
handlePoint();
invalidate();//重绘
break;
case MotionEvent.ACTION_UP:
mStartX = mStartY = mMoveX = mMoveY = 0;
mIsUp = true;
invalidate();//重绘
if (mOnEndListener != null && mPasswordArr != null && mPasswordArr.size() != 0)
mOnEndListener.onFinish(mPasswordArr);//将数组传递出去
break;
}
}
/**
* 处理一下这些点
*/
private void handlePoint() {
for (PointInfo pointInfo : mPoints) {
if (pointInfo.isInMyPlace(mMoveX, mMoveY) && !pointInfo.isSelected()) {//如果在范围之内 并且没选中
pointInfo.setSelected(true);//设为选中
mStartX = pointInfo.getCenterX();
mStartY = pointInfo.getCenterY();
if (mPasswordArr.size() != 0) {
int preId = mPasswordArr.get(mPasswordArr.size() - 1);//前一个点的id
int firstRax = preId / mColumnNum;//求出行数
int firstColumn = preId % mColumnNum;//求出列数
int secondRax = pointInfo.getId() / mColumnNum;//当前经过点的行数
int secondColumn = pointInfo.getId() % mColumnNum;//当前经过点的列数
Log.d(TAG, "handlePoint: firstRaw " + firstRax + " firstColumn " + firstColumn);
Log.d(TAG, "handlePoint: secondRaw " + secondRax + " secondColumn " + secondColumn);
if (firstRax == secondRax) {//如果在同一行上
for (int i = firstColumn + 1; i < secondColumn; i++) //如果第二个点在第一个点右边
preId = connect(preId, firstRax * mColumnNum + i);
for (int i = firstColumn - 1; i > secondColumn; i--) //如果第二个点在第一个点左边
preId = connect(preId, firstRax * mColumnNum + i);
} else if (firstColumn == secondColumn) {//如果在同一列上
for (int i = firstRax + 1; i < secondRax; i++) //如果第一个点在第二个点上面
preId = connect(preId, i * mColumnNum + firstColumn);
for (int i = firstRax - 1; i > secondRax; i--) //如果第一个点在第二个点下面
preId = connect(preId, i * mColumnNum + firstColumn);
} else if (Math.abs(firstColumn - secondColumn) == Math.abs(firstRax - secondRax)) {//如果在同一对角线上
int raxOffset = (firstRax > secondRax) ? -1 : 1;
int indexRax = firstRax + raxOffset;//临时行
for (int i = firstColumn + 1; i < secondColumn; i++) { //如果第二个点在第一个点右边
preId = connect(preId, indexRax * mColumnNum + i);//再连一下
indexRax = firstRax + raxOffset;//行数也跟着偏移
}
for (int i = firstColumn - 1; i > secondColumn; i--) { //如果第二个点在第一个点左边
preId = connect(preId, indexRax * mColumnNum + i);//再连一下
indexRax = firstRax + raxOffset;//行数也跟着偏移
}
}
mPoints[preId].setNextId(pointInfo.getId());
}
mPasswordArr.add(pointInfo.getId());//将点id添加进去
break;//返回
}
}
}
/**
* 将两个点相连返回下一个点
*/
private int connect(int preId, int nextId) {
Log.d(TAG, "connect: preId " + preId + " nextId " + nextId);
if (!mPoints[nextId].isSelected()) {//如果没被选中
mPoints[preId].setNextId(nextId);//下一个id
mPasswordArr.add(nextId);//把这个点加进来
mPoints[nextId].setSelected(true);//设为选中
return nextId;
}
return preId;//把第一个id返回去
}
/**
* 绘制
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
if (mMoveX != 0 && mMoveY != 0 && mStartX != 0 && mStartY != 0)// 绘制当前活动的线段
canvas.drawLine(mStartX, mStartY, mMoveX, mMoveY, mLinePaint);//绘制线
drawNinePoint(canvas);
}
/**
* 绘制那几个点
*
* @param canvas
*/
private void drawNinePoint(Canvas canvas) {
for (PointInfo pointInfo : mPoints) { //先把用户的画出的线绘制好
if (pointInfo.hasNextId()) { //如果有下一个
int nextId = pointInfo.getNextId();//下一个的id
canvas.drawLine(pointInfo.getCenterX(), pointInfo.getCenterY(),
mPoints[nextId].getCenterX(), mPoints[nextId].getCenterY(), mLinePaint);
}
}
for (PointInfo pointInfo : mPoints) {
if (pointInfo.isSelected()) {//如果被选中
canvas.drawBitmap(mSelectedBitmap, pointInfo.getSelectedX(),
pointInfo.getSelectedY(), mLinePaint);//绘制一下默认图片
} else {
canvas.drawBitmap(mDefaultBitmap, pointInfo.getDefaultX(),
pointInfo.getDefaultY(), mLinePaint);//绘制一下默认图片
}
}
}
/**
* 在这里得到view的宽高
*/
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
super.onSizeChanged(w, h, oldW, oldH);
Log.d(TAG, "onSizeChanged: " + " w " + w + " h " + h + " oldW " + oldW + " oldH " + oldH);
mWidth = w;
mHeight = h;//得到view宽高
initPoint();//当获得宽高好得到一下
}
//绘制结束监听
public interface OnEndListener {
void onFinish(List<Integer> passwordArr);//将密码触底出去
}
private OnEndListener mOnEndListener;
/**
* 设置结束的监听
*/
public void setOnEndListener(OnEndListener onEndListener) {
mOnEndListener = onEndListener;
}
}
第三个一个小的工具类
package com.example.ninepointtask.util;
import com.example.ninepointtask.base.MyApplication;
/**
* Created by xiaohan on 2018/4/25.
*/
public class DensityUtil {
/**
* 工具类 构造器私有
*/
private DensityUtil() {
}
public static int dipToPx(float dpValue) {
float scale = MyApplication.getContext().getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public static int pxToDp(float pxValue) {
float scale = MyApplication.getContext().getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
因为代码写的比较灵活 只要把NinePointView的
private PointInfo[] mPoints = new PointInfo[16];//点信息
9改为16就变成16键了 当然25键也是可以的 不过不建议太密