Android 自定义锁屏图案 View

前言

Android 自定义 View 技能是成为高级工程师所必备的,笔者觉得自定义 View 没有什么捷径可走,唯有经常练习才能解决产品需求。笔者也好久没有写自定义 View 了,赶紧写个控件找点感觉回来。

本文实现的是一个 锁屏图案的自定义控件。效果图如下:

Github 地址:https://github.com/xing16/AndroidSample

Android 自定义锁屏图案 View_第1张图片

LockView 介绍

自定义属性:
属性 描述
app:rowCount=”3” 每行每列圆的个数
app:normalColor=”0xee776666” 圆的默认颜色
app:moveColor=”0xee0000ff” 圆的选中颜色
app:errorColor=”0xeeff0000” 圆的错误颜色
引用方式:

(1) 在布局文件中引入

    <com.xing.androidsample.view.LockView
        android:id="@+id/lock_view"
        app:rowCount="4"
        app:normalColor=""
        app:moveColor=""
        app:errorColor=""
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="40dp" />

(2) 在代码中设置正确的图案,用于校验是否匹配成功,并在回调中获取结果

List intList = new ArrayList<>();
        intList.add(3);
        intList.add(7);
        intList.add(4);
        intList.add(2);
        lockView.setStandard(intList);
        lockView.setOnDrawCompleteListener(new LockView.OnDrawCompleteListener() {
            @Override
            public void onComplete(boolean isSuccess) {
                Toast.makeText(CustomViewActivity.this, isSuccess ? "success" : "fail", Toast.LENGTH_SHORT).show();
            }
        });

实现思路

  1. 以默认状态绘制 rowCount * rowCount 个圆,外圆颜色需要在内圆颜色上加上一定的透明度。
  2. 在 onTouchEvent() 方法中,判断当前触摸点与各个圆的圆心距离是否小于圆的半径,决定各个圆此时处于哪个状态(normal,move,error),调用 invalidate() 重新绘制,更新颜色。
  3. ​将手指滑动触摸过的圆的坐标添加到一个 ArrayList 中,使用 Path 连接该集合中选中的圆,即可绘制出划过的路径线。

实现步骤

自定义属性

在 res/values 目录下新建 attrs.xml 文件:


<resources>
    <declare-styleable name="LockView">
        <attr name="normalColor" format="color|reference" />    
        <attr name="moveColor" format="color|reference" />       
        <attr name="errorColor" format="color|reference" />      
        <attr name="rowCount" format="integer" />                 
    declare-styleable>
resources>
获取自定义属性
  public LockView(Context context) {
          this(context, null);
      }

      public LockView(Context context, @Nullable AttributeSet attrs) {
          super(context, attrs);
          readAttrs(context, attrs);
          init();
      }

    /**
    * 获取自定义属性
    */
      private void readAttrs(Context context, AttributeSet attrs) {
          TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView);
          normalColor = typedArray.getColor(R.styleable.LockView_normalColor, DEFAULT_NORMAL_COLOR);
          moveColor = typedArray.getColor(R.styleable.LockView_moveColor, DEFAULT_MOVE_COLOR);
          errorColor = typedArray.getColor(R.styleable.LockView_errorColor, DEFAULT_ERROR_COLOR);
          rowCount = typedArray.getInteger(R.styleable.LockView_rowCount, DEFAULT_ROW_COUNT);
          typedArray.recycle();
      }

    /**
    * 初始化
    */
      private void init() {
          stateSparseArray = new SparseIntArray(rowCount * rowCount);
          points = new PointF[rowCount * rowCount];

          innerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
          innerCirclePaint.setStyle(Paint.Style.FILL);

          outerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
          outerCirclePaint.setStyle(Paint.Style.FILL);

          linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
          linePaint.setStyle(Paint.Style.STROKE);
          linePaint.setStrokeCap(Paint.Cap.ROUND);
          linePaint.setStrokeJoin(Paint.Join.ROUND);
          linePaint.setStrokeWidth(30);
          linePaint.setColor(moveColor);
      }
计算圆的半径

设定外圆半径和相邻两圆之间间距相同,内圆半径是外圆半径的一半,所以半径计算方式为:


radius = Math.min(w, h) / (2 * rowCount + rowCount - 1) * 1.0f;

设置各圆坐标

各圆坐标使用一维数组保存,计算方式为:

// 各个圆设置坐标点
for (int i = 0; i < rowCount * rowCount; i++) {
     points[i] = new PointF(0, 0);
     points[i].set((i % rowCount * 3 + 1) * radius, (i / rowCount * 3 + 1) * radius);
}
测量 View 宽高

根据测量模式设置控件的宽高,当布局文件中设置的是 wrap_content ,默认将控件宽高设置为 600dp

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getSize(widthMeasureSpec);
        int height = getSize(heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    private int getSize(int measureSpec) {
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        if (mode == MeasureSpec.EXACTLY) {
            return size;
        } else if (mode == MeasureSpec.AT_MOST) {
            return Math.min(size, dp2Px(600));
        }
        return dp2Px(600);
    }
onTouchEvent() 触摸事件

在手指滑动过程中,根据当前触摸点坐标是否落在圆的范围内,更新该圆的状态,在重新绘制时,绘制成新的颜色。手指抬起时,将存放状态的 list,选中圆的 list ,linePath 重置,并将结果回调出来。


    private PointF touchPoint;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                reset();
            case MotionEvent.ACTION_MOVE:
                if (touchPoint == null) {
                    touchPoint = new PointF(event.getX(), event.getY());
                } else {
                    touchPoint.set(event.getX(), event.getY());
                }
                for (int i = 0; i < rowCount * rowCount; i++) {
                    // 是否触摸在圆的范围内
                    if (getDistance(touchPoint, points[i]) < radius) {
                        stateSparseArray.put(i, STATE_MOVE);
                        if (!selectedList.contains(points[i])) {
                            selectedList.add(points[i]);
                        }
                        break;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (check()) {   // 正确图案
                    if (listener != null) {
                        listener.onComplete(true);
                    }
                    for (int i = 0; i < stateSparseArray.size(); i++) {
                        int index = stateSparseArray.keyAt(i);
                        stateSparseArray.put(index, STATE_MOVE);
                    }
                } else {     // 错误图案
                    for (int i = 0; i < stateSparseArray.size(); i++) {
                        int index = stateSparseArray.keyAt(i);
                        stateSparseArray.put(index, STATE_ERROR);
                    }
                    linePaint.setColor(0xeeff0000);
                    if (listener != null) {
                        listener.onComplete(false);
                    }
                }
                touchPoint = null;
                if (timer == null) {
                    timer = new Timer();
                }
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        linePath.reset();
                        linePaint.setColor(0xee0000ff);
                        selectedList.clear();
                        stateSparseArray.clear();
                        postInvalidate();
                    }
                }, 1000);
                break;
        }
        invalidate();
        return true;
    }
绘制各圆和各圆之间连接线段

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawCircle(canvas);
        drawLinePath(canvas);
    }

    private void drawCircle(Canvas canvas) {
        // 依次从索引 0 到索引 8,根据不同状态绘制圆点
        for (int index = 0; index < rowCount * rowCount; index++) {
            int state = stateSparseArray.get(index);
            switch (state) {
                case STATE_NORMAL:
                    innerCirclePaint.setColor(normalColor);
                    outerCirclePaint.setColor(normalColor & 0x66ffffff);
                    break;
                case STATE_MOVE:
                    innerCirclePaint.setColor(moveColor);
                    outerCirclePaint.setColor(moveColor & 0x66ffffff);
                    break;
                case STATE_ERROR:
                    innerCirclePaint.setColor(errorColor);
                    outerCirclePaint.setColor(errorColor & 0x66ffffff);
                    break;
            }
            canvas.drawCircle(points[index].x, points[index].y, radius, outerCirclePaint);
            canvas.drawCircle(points[index].x, points[index].y, radius / 2f, innerCirclePaint);
        }
    }

完整 View 代码

/**
 * Created by star.tao on 2018/5/30.
 * email: [email protected]
 * github: https://github.com/xing16
 */

public class LockView extends View {

    private static final int DEFAULT_NORMAL_COLOR = 0xee776666;
    private static final int DEFAULT_MOVE_COLOR = 0xee0000ff;
    private static final int DEFAULT_ERROR_COLOR = 0xeeff0000;
    private static final int DEFAULT_ROW_COUNT = 3;

    private static final int STATE_NORMAL = 0;
    private static final int STATE_MOVE = 1;
    private static final int STATE_ERROR = 2;


    private int normalColor; // 无滑动默认颜色
    private int moveColor;   // 滑动选中颜色
    private int errorColor;  // 错误颜色

    private float radius;    // 外圆半径

    private int rowCount;

    private PointF[] points;   // 一维数组记录所有圆点的坐标点

    private Paint innerCirclePaint; // 内圆画笔

    private Paint outerCirclePaint; // 外圆画笔

    private SparseIntArray stateSparseArray;

    private List selectedList = new ArrayList<>();

    private List standardPointsIndexList = new ArrayList<>();

    private Path linePath = new Path();    // 手指移动的路径

    private Paint linePaint;

    private Timer timer;

    public LockView(Context context) {
        this(context, null);
    }

    public LockView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        readAttrs(context, attrs);
        init();
    }


    private void readAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView);
        normalColor = typedArray.getColor(R.styleable.LockView_normalColor, DEFAULT_NORMAL_COLOR);
        moveColor = typedArray.getColor(R.styleable.LockView_moveColor, DEFAULT_MOVE_COLOR);
        errorColor = typedArray.getColor(R.styleable.LockView_errorColor, DEFAULT_ERROR_COLOR);
        rowCount = typedArray.getInteger(R.styleable.LockView_rowCount, DEFAULT_ROW_COUNT);
        typedArray.recycle();
    }

    private void init() {
        stateSparseArray = new SparseIntArray(rowCount * rowCount);
        points = new PointF[rowCount * rowCount];

        innerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        innerCirclePaint.setStyle(Paint.Style.FILL);

        outerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        outerCirclePaint.setStyle(Paint.Style.FILL);


        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setStrokeCap(Paint.Cap.ROUND);
        linePaint.setStrokeJoin(Paint.Join.ROUND);
        linePaint.setStrokeWidth(30);
        linePaint.setColor(moveColor);

    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 外圆半径 = 相邻外圆之间间距 = 2倍内圆半径
        radius = Math.min(w, h) / (2 * rowCount + rowCount - 1) * 1.0f;
        // 各个圆设置坐标点
        for (int i = 0; i < rowCount * rowCount; i++) {
            points[i] = new PointF(0, 0);
            points[i].set((i % rowCount * 3 + 1) * radius, (i / rowCount * 3 + 1) * radius);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getSize(widthMeasureSpec);
        int height = getSize(heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    private int getSize(int measureSpec) {
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        if (mode == MeasureSpec.EXACTLY) {
            return size;
        } else if (mode == MeasureSpec.AT_MOST) {
            return Math.min(size, dp2Px(600));
        }
        return dp2Px(600);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawCircle(canvas);
        drawLinePath(canvas);
    }

    private void drawCircle(Canvas canvas) {
        // 依次从索引 0 到索引 8,根据不同状态绘制圆点
        for (int index = 0; index < rowCount * rowCount; index++) {
            int state = stateSparseArray.get(index);
            switch (state) {
                case STATE_NORMAL:
                    innerCirclePaint.setColor(normalColor);
                    outerCirclePaint.setColor(normalColor & 0x66ffffff);
                    break;
                case STATE_MOVE:
                    innerCirclePaint.setColor(moveColor);
                    outerCirclePaint.setColor(moveColor & 0x66ffffff);
                    break;
                case STATE_ERROR:
                    innerCirclePaint.setColor(errorColor);
                    outerCirclePaint.setColor(errorColor & 0x66ffffff);
                    break;
            }
            canvas.drawCircle(points[index].x, points[index].y, radius, outerCirclePaint);
            canvas.drawCircle(points[index].x, points[index].y, radius / 2f, innerCirclePaint);
        }
    }

    /**
     * 绘制选中点之间相连的路径
     *
     * @param canvas
     */
    private void drawLinePath(Canvas canvas) {
        // 重置linePath
        linePath.reset();
        // 选中点个数大于 0 时,才绘制连接线段
        if (selectedList.size() > 0) {
            // 起点移动到按下点位置
            linePath.moveTo(selectedList.get(0).x, selectedList.get(0).y);
            for (int i = 1; i < selectedList.size(); i++) {
                linePath.lineTo(selectedList.get(i).x, selectedList.get(i).y);
            }
            // 手指抬起时,touchPoint设置为null,使得已经绘制游离的路径,消失掉,
            if (touchPoint != null) {
                linePath.lineTo(touchPoint.x, touchPoint.y);
            }
            canvas.drawPath(linePath, linePaint);
        }
    }

    private PointF touchPoint;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                reset();
            case MotionEvent.ACTION_MOVE:
                if (touchPoint == null) {
                    touchPoint = new PointF(event.getX(), event.getY());
                } else {
                    touchPoint.set(event.getX(), event.getY());
                }
                for (int i = 0; i < rowCount * rowCount; i++) {
                    // 是否触摸在圆的范围内
                    if (getDistance(touchPoint, points[i]) < radius) {
                        stateSparseArray.put(i, STATE_MOVE);
                        if (!selectedList.contains(points[i])) {
                            selectedList.add(points[i]);
                        }
                        break;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (check()) {   // 正确图案
                    if (listener != null) {
                        listener.onComplete(true);
                    }
                    for (int i = 0; i < stateSparseArray.size(); i++) {
                        int index = stateSparseArray.keyAt(i);
                        stateSparseArray.put(index, STATE_MOVE);
                    }
                } else {     // 错误图案
                    for (int i = 0; i < stateSparseArray.size(); i++) {
                        int index = stateSparseArray.keyAt(i);
                        stateSparseArray.put(index, STATE_ERROR);
                    }
                    linePaint.setColor(0xeeff0000);
                    if (listener != null) {
                        listener.onComplete(false);
                    }
                }
                touchPoint = null;
                if (timer == null) {
                    timer = new Timer();
                }
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        linePath.reset();
                        linePaint.setColor(0xee0000ff);
                        selectedList.clear();
                        stateSparseArray.clear();
                        postInvalidate();
                    }
                }, 1000);
                break;
        }
        invalidate();
        return true;
    }

    /**
     * 清除绘制图案的条件,当触发 invalidate() 时将清空图案
     */
    private void reset() {
        touchPoint = null;
        linePath.reset();
        linePaint.setColor(0xee0000ff);
        selectedList.clear();
        stateSparseArray.clear();
    }


    public void onStop() {
        timer.cancel();
    }

    private boolean check() {
        if (selectedList.size() != standardPointsIndexList.size()) {
            return false;
        }
        for (int i = 0; i < standardPointsIndexList.size(); i++) {
            Integer index = standardPointsIndexList.get(i);
            if (points[index] != selectedList.get(i)) {
                return false;
            }
        }
        return true;
    }

    public void setStandard(List pointsList) {
        if (pointsList == null) {
            throw new IllegalArgumentException("standard points index can't null");
        }
        if (pointsList.size() > rowCount * rowCount) {
            throw new IllegalArgumentException("standard points index list can't large to rowcount * columncount");
        }
        standardPointsIndexList = pointsList;
    }

    private OnDrawCompleteListener listener;

    public void setOnDrawCompleteListener(OnDrawCompleteListener listener) {
        this.listener = listener;
    }


    public interface OnDrawCompleteListener {
        void onComplete(boolean isSuccess);
    }


    private float getDistance(PointF centerPoint, PointF downPoint) {
        return (float) Math.sqrt(Math.pow(centerPoint.x - downPoint.x, 2) + Math.pow(centerPoint.y - downPoint.y, 2));

    }

    private int dp2Px(int dpValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
    }

}

你可能感兴趣的:(Android,自定义,View)