Android手势密码view学习笔记(二)
我们还是接着我们上一篇博客中的内容往下讲哈,上一节 Android手势密码view笔记(一)我们已经实现了我们的IndicatorView指示器view了:
下面我们来实现下我们的手势密码view:
实现思路:
1、我们照样需要拿到用户需要显示的一些属性(行、列、选中的图片、未选中的图片、错误显示的图片、连接线的宽度跟颜色......)。
2、我们需要根据手势的变换然后需要判断当前手指位置是不是在某个点中,在的话就把该点设置为选中状态,然后每移动到两个点(也就是一个线段)就记录该两个点。
3、最后把记录的所有点(所有线段)画在canvas上,并记录每个点对应的num值(也就是我们设置的密码)。
4、当手指抬起的时候,执行回调方法,把封装的密码集合传给调用着。
好啦~ 既然右了思路,我们就来实现下:
首先是定义一个attrs.xml文件(为了方便,我就直接在上一篇博客中实现的indicatorview的attr里面继续往下定义了):
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="IndicatorView"> <!--默认状态的drawable--> <attr name="normalDrawable" format="reference" /> <!--被选中状态的drawable--> <attr name="selectedDrawable" format="reference" /> <!--列数--> <attr name="column" format="integer" /> <!--行数--> <attr name="row" format="integer" /> <!--错误状态的drawabe--> <attr name="erroDrawable" format="reference" /> <!--padding值,padding值越大点越小--> <attr name="padding" format="dimension" /> <!--默认连接线颜色--> <attr name="normalStrokeColor" format="color" /> <!--错误连接线颜色--> <attr name="erroStrokeColor" format="color" /> <!--连接线size--> <attr name="strokeWidth" format="dimension" /> </declare-styleable> </resources>
然后就是第一个叫GestureContentView的view去继承viewgroup,并重新三个构造方法:
public class GestureContentView extends ViewGroup { public void setGesturePwdCallBack(IGesturePwdCallBack gesturePwdCallBack) { this.gesturePwdCallBack = gesturePwdCallBack; } public GestureContentView(Context context) { this(context, null); } public GestureContentView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public GestureContentView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
然后我们需要在带三个参数的构造方法中获取我们传入的自定义属性:
public GestureContentView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setWillNotDraw(false); obtainStyledAttr(context, attrs, defStyleAttr); initViews(); }
你会发现我这里调用下setWillNotDraw(false);方法,顾名思义,设置为false后onDraw方法才会被调用,当然你也可以重写dispatchDraw方法(具体原因我就不扯了哈,自己网上查或者看view的源码)。
private void obtainStyledAttr(Context context, AttributeSet attrs, int defStyleAttr) { final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.IndicatorView, defStyleAttr, 0); mNormalDrawable = a.getDrawable(R.styleable.IndicatorView_normalDrawable); mSelectedDrawable = a.getDrawable(R.styleable.IndicatorView_selectedDrawable); mErroDrawable = a.getDrawable(R.styleable.IndicatorView_erroDrawable); checkDrawable(); if (a.hasValue(R.styleable.IndicatorView_row)) { mRow = a.getInt(R.styleable.IndicatorView_row, NUMBER_ROW); } if (a.hasValue(R.styleable.IndicatorView_column)) { mColumn = a.getInt(R.styleable.IndicatorView_row, NUMBER_COLUMN); } if (a.hasValue(R.styleable.IndicatorView_padding)) { DEFAULT_PADDING = a.getDimensionPixelSize(R.styleable.IndicatorView_padding, DEFAULT_PADDING); } strokeColor=a.getColor(R.styleable.IndicatorView_normalStrokeColor,DEFAULT_STROKE_COLOR); erroStrokeColor=a.getColor(R.styleable.IndicatorView_erroStrokeColor,ERRO_STROKE_COLOR); strokeWidth=a.getDimensionPixelSize(R.styleable.IndicatorView_strokeWidth,DEFAULT_STROKE_W); }
然后获取到了我们需要的东西后,我们需要知道每个点的大小跟自己的大小了(还是一样的套路,不懂的看上一篇博客哈):
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); float width = MeasureSpec.getSize(widthMeasureSpec); float height = MeasureSpec.getSize(heightMeasureSpec); float result = Math.min(width, height); height = getHeightValue(result, heightMode); width = getWidthValue(result, widthMode); setMeasuredDimension((int) width, (int) height); }
private float getHeightValue(float height, int heightMode) { if (heightMode == MeasureSpec.EXACTLY) { mCellHeight = (height - (mColumn + 1) * DEFAULT_PADDING) / mColumn; } else { mCellHeight = Math.min(mNormalDrawable.getIntrinsicHeight(), mSelectedDrawable.getIntrinsicHeight()); height = mCellHeight * mColumn + (mColumn + 1) * DEFAULT_PADDING; } return height; } private float getWidthValue(float width, int widthMode) { if (widthMode == MeasureSpec.EXACTLY) { mCellWidth = (width - (mRow + 1) * DEFAULT_PADDING) / mRow; } else { mCellWidth = Math.min(mNormalDrawable.getIntrinsicWidth(), mSelectedDrawable.getIntrinsicWidth()); width = mCellWidth * mRow + (mRow + 1) * DEFAULT_PADDING; } return width; }
好了,view的大小跟点的大小我们都知道了,然后我们需要根据我们传入的行数跟列数添加我们的点了:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (!isInitialed && getChildCount() == 0) { isInitialed = true; points = new ArrayList<>(); addChildViews(); } }
首先在onSizeChanged方法中去添加我们的点(也就是子view)因为onSizeChanged会被调很多次,然后避免重复添加子view,我们做了一个判断,第一次并且child=0的时候再去添加子view:
private void addChildViews() { for (int i = 0; i < mRow; i++) { for (int j = 0; j < mColumn; j++) { GesturePoint point = new GesturePoint(); ImageView image = new ImageView(getContext()); point.setImageView(image); int left = (int) ((j + 1) * DEFAULT_PADDING + j * mCellWidth); int top = (int) ((i + 1) * DEFAULT_PADDING + i * mCellHeight); int right = (int) (left + mCellWidth); int bottom = (int) (top + mCellHeight); point.setLeftX(left); point.setRightX(right); point.setTopY(top); point.setBottomY(bottom); point.setCenterX((int) (left + mCellWidth / 2)); point.setCenterY((int) (top + mCellHeight / 2)); point.setNormalDrawable(mNormalDrawable); point.setErroDrawable(mErroDrawable); point.setSelectedDrawable(mSelectedDrawable); point.setState(PointState.POINT_STATE_NORMAL); point.setNum(Integer.parseInt(String.valueOf(mRow * i + j))); point.setPointX(i); point.setPointY(j); this.addView(image, (int) mCellWidth, (int) mCellHeight); points.add(point); } } }
添加的个数=行数*列数,然后把创建的image添加进当前viewgroup中:
for (int i = 0; i < mRow; i++) { for (int j = 0; j < mColumn; j++) { GesturePoint point = new GesturePoint(); ImageView image = new ImageView(getContext()); ...... this.addView(image, (int) mCellWidth, (int) mCellHeight); } }
并且把所有的点信息存放在了一个叫points.add(point);的集合中,集合中存放的是GesturePoint对象:
public class GesturePoint { //点的左边距值 private int leftX; //点的top值 private int topY; //点的右边距值 private int rightX; private int bottomY; //点的中间值x轴 private int centerX; private int centerY; //点对应的行值 private int pointX; //点对应的列值 private int pointY; //点对应的imageview private ImageView imageView; //当前点的状态:选中、未选中、错误 private PointState state; //当前点对应的密码数值 private int num; //未选中点的drawale private Drawable normalDrawable; private Drawable erroDrawable; private Drawable selectedDrawable; }
既然我们已经添加了很多个点,然后我们要做的就是根据行列排列我们点的位置了(重写onLayout方法摆放子view(点)的位置):
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (points != null && points.size() > 0) { for (GesturePoint point : points) { point.layout(); } } }
public void layout() { if (this.imageView != null) { this.imageView.layout(leftX, topY, rightX, bottomY); } }
然后我们添加下我们的view并运行代码:
<FrameLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:layout_marginTop="10dp" > <com.leo.library.view.GestureContentView android:id="@+id/id_gesture_pwd" android:layout_gravity="center_horizontal" android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_height="match_parent" app:column="3" app:row="3" app:padding="50dp" app:normalDrawable="@drawable/gesture_node_normal" app:selectedDrawable="@drawable/gesture_node_pressed" app:erroDrawable="@drawable/gesture_node_wrong" app:normalStrokeColor="#000" app:erroStrokeColor="#ff0000" app:strokeWidth="4dp" /> </FrameLayout>
效果图:
写到这里,离我们的目标越来越近了,接下来要做的就是重写onTouchEvent方法,然后通过手指滑动位置做出相应的改变了:
1、我们需要根据手指的位置查看是否在某个点内,判断该点是不是被选中,没选中就改变其状态为选中状态。
2、手指每连接两个点的时候,我们需要判断两个点中间是否有点,比如:我们手指连接了(0,0) 跟(2,2)这两个点,那么我们需要判断中间是否有点(1、1)存在。然后需要把线段(0,0)~(1、1)和线段(1、1)~(2、2)保存在集合中,然后下一次再画线段的时候起点就为(2、2)点了。
3、没选中一个点我们就把该点对应的num值(密码)存入集合中。
4、当执行ACTION_UP事件(也就是手指抬起的时候),回调方法,把存储的集合数据传给调用者。
@Override public boolean onTouchEvent(MotionEvent event) { //是否允许用户绘制 if (!isDrawEnable) return super.onTouchEvent(event); //画笔颜色设置为绘制颜色 linePaint.setColor(strokeColor); int action = event.getAction(); //当手指按下的时候 if (MotionEvent.ACTION_DOWN == action) { //清除画板 changeState(PointState.POINT_STATE_NORMAL); preX = (int) event.getX(); preY = (int) event.getY(); //根据当前手指位置找出对应的点 currPoint = getPointByPosition(preX, preY); //如果当前手指在某个点中的时候,把该点标记为选中状态 if (currPoint != null) { currPoint.setState(PointState.POINT_STATE_SELECTED); //把当前选中的点添加进集合中 pwds.add(currPoint.getNum()); } //当手指移动的时候 } else if (MotionEvent.ACTION_MOVE == action) { //,清空画板,然后画出前面存储的线段 clearScreenAndDrawLine(); //获取当前移动的位置是否在某个点中 GesturePoint point = getPointByPosition((int) event.getX(), (int) event.getY()); //没有在点的范围内的话并且currpoint也为空的时候(在画板外移动手指)直接返回 if (point == null && currPoint == null) { return super.onTouchEvent(event); } else { //当按下时候的点为空,然后手指移动到了某一点的时候,把该点赋给currpoint if (currPoint == null) { currPoint = point; //修改该点的状态 currPoint.setState(PointState.POINT_STATE_SELECTED); //添加该点的值 pwds.add(currPoint.getNum()); } } //当移动的不在点范围内、一直在同一个点中移动、选中了某个点后再次选中的时候不让选中(也就是不让出现重复密码) if (point == null || currPoint.getNum() == point.getNum() || point.getState() == PointState.POINT_STATE_SELECTED) { lineCanvas.drawLine(currPoint.getCenterX(), currPoint.getCenterY(), event.getX(), event.getY(), linePaint); } else { //修改该点的状态为选中 point.setState(PointState.POINT_STATE_SELECTED); //连接currpoint跟当前point lineCanvas.drawLine(currPoint.getCenterX(), currPoint.getCenterY(), point.getCenterX(), point.getCenterY(), linePaint); //判断两个点中是否存在点 List<Pair<GesturePoint, GesturePoint>> betweenPoints = getBetweenPoints(currPoint, point); //如果存在点的话,把中间点对应的线段存入集合总 if (betweenPoints != null && betweenPoints.size() > 0) { pointPairs.addAll(betweenPoints); currPoint = point; pwds.add(point.getNum()); } else { pointPairs.add(new Pair(currPoint, point)); pwds.add(point.getNum()); currPoint = point; } } invalidate(); } else if (MotionEvent.ACTION_UP == action) { //手指抬起的时候回调 if (gesturePwdCallBack != null) { List<Integer> datas=new ArrayList<>(pwds.size()); datas.addAll(pwds); gesturePwdCallBack.callBack(datas); } } return true; }
重点解释下getBetweenPoints方法(声明一下哈:本人数学比较差,有好方法的童鞋虐过哈。记得评论告诉我一下,拜谢啦~~ 自己觉得自己的方法比较蠢,嘻嘻~!!):
private List<Pair<GesturePoint, GesturePoint>> getBetweenPoints(GesturePoint currPoint, GesturePoint point) { //定义一个集合装传入的点 List<GesturePoint> points1 = new ArrayList<>(); points1.add(currPoint); points1.add(point); //排序两个点 Collections.sort(points1, new Comparator<GesturePoint>() { @Override public int compare(GesturePoint o1, GesturePoint o2) { return o1.getNum() - o2.getNum(); } }); GesturePoint maxPoint = points1.get(1); GesturePoint minPoint = points1.get(0); points1.clear(); /** * 根据等差数列公式an=a1+(n-1)*d,我们知道an跟a1,n=(an的列或者行值-a1的列或者行值+1), * 算出d,如果d为整数那么为等差数列,如果an的列或者行值-a1的列或者行值>1的话,就证明存在 * 中间值。 * 1、算出的d是否为整数 * 2、an的行值-a1的行值>1或者an的列值-a1的列值>1 * * 两个条件成立的话就证明有中间点 */ if (((maxPoint.getNum() - minPoint.getNum()) % Math.max(maxPoint.getPointX(), maxPoint.getPointY()) == 0) && ((maxPoint.getPointX() - minPoint.getPointX()) > 1 || maxPoint.getPointY() - minPoint.getPointY() > 1 )) { //算出等差d int duration = (maxPoint.getNum() - minPoint.getNum()) / Math.max(maxPoint.getPointX(), maxPoint.getPointY()); //算出中间有多少个点 int count = maxPoint.getPointX() - minPoint.getPointX() - 1; count = Math.max(count, maxPoint.getPointY() - minPoint.getPointY() - 1); //利用等差数列公式算出中间点(an=a1+(n-1)*d) for (int i = 0; i < count; i++) { int num = minPoint.getNum() + (i + 1) * duration; for (GesturePoint p : this.points) { //在此判断算出的中间点是否存在并且没有被选中 if (p.getNum() == num && p.getState() != PointState.POINT_STATE_SELECTED) { //把选中的点添加进集合 pwds.add(p.getNum()); //修改该点的状态为选中状态 p.setState(PointState.POINT_STATE_SELECTED); points1.add(p); } } } } //利用算出的中间点来算出中间线段 List<Pair<GesturePoint, GesturePoint>> pairs = new ArrayList<>(); for (int i = 0; i < points1.size(); i++) { GesturePoint p = points1.get(i); if (i == 0) { pairs.add(new Pair(minPoint, p)); } else if (pairs.size() > 0) { pairs.add(new Pair(pairs.get(0).second, p)); } if (i == points1.size() - 1) { pairs.add(new Pair(p, maxPoint)); } } //返回中间线段 return pairs; }
好啦!!代码就解析到这里了哈,看不懂的童鞋自己去拖代码然后跑跑就知道了。
整个做下来除了某几个地方有点难度外,其它的地方也都是很简单的东西呢?以前看起来很高大上的东西是不是现在觉得soeasy了呢?? 哈哈~~~
最后给出项目github链接:
https://github.com/913453448/GestureContentView