android-(pin码)手势认证界面

347 阅读2分钟

效果:

九宫格 View 相关代码:

九宫格View的绘制:

package xxxx
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;


import java.util.ArrayList;
import java.util.List;

import lombok.Setter;



/**
 * @author 30819
 */
@Setter
public class GesturePasswordView extends View {
    boolean firstDrawFlag = true;
    final private List<Point> points = new ArrayList<>();
    final private List<Point> selectedPoint = new ArrayList<>();

    final static Paint NORMAL_LINE_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);
    final static Paint ERROR_LINE_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);
    final static Point FINGER_POINT = new Point();

    private Paint linePaint = NORMAL_LINE_PAINT;
    private Vibrator vibrator;
    private Callback callback;

    static {
    //初始化画笔
        NORMAL_LINE_PAINT.setColor(INIT_POINT_COLOR);
        NORMAL_LINE_PAINT.setStrokeWidth(INIT_POINT_RADIUS);
        NORMAL_LINE_PAINT.setColor(SELECTED_POINT_COLOR);

        ERROR_LINE_PAINT.setColor(INIT_POINT_COLOR);
        ERROR_LINE_PAINT.setStrokeWidth(INIT_POINT_RADIUS);
        ERROR_LINE_PAINT.setColor(ERROR_POINT_COLOR);
    }

    public GesturePasswordView(Context context, AttributeSet attrs) {
        super(context, attrs);
        vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
    }

    private void firstDrawPoint() {

        int width = this.getWidth();
        int height = this.getHeight();
        float distance = (float) Math.min(width, height) / 4;

        for (int i = 0; i < 9; i++) {
            Point point = new Point(i);
            points.add(point);
            switch (i % 3) {
                case 0: {
                    point.setX(distance);
                }
                break;
                case 1: {
                    point.setX(distance * 2);
                }
                break;
                case 2: {
                    point.setX(distance * 3);
                }
                break;
                default:
            }
            switch (i / 3) {
                case 0: {
                    point.setY(distance);
                }
                break;
                case 1: {
                    point.setY(distance * 2);
                }
                break;
                case 2: {
                    point.setY(distance * 3);
                }
                break;
                default:
            }
        }


    }

    private int getMySize(int defaultSize, int measureSpec) {
        int mySize = defaultSize;

        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        switch (mode) {
            case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
                mySize = defaultSize;
                break;
            }
            case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
                //我们将大小取最大值,你也可以取其他值
                mySize = size;
                break;
            }
            case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
                mySize = size;
                break;
            }
        }
        return mySize;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMySize(getWidth(), widthMeasureSpec);
        int height = getMySize(getWidth(), heightMeasureSpec);

        int length = Math.min(width, height);

        setMeasuredDimension(length, length);
    }

    @Override
    public void onDraw(Canvas canvas) {
    //第一次绘制执行
        if (firstDrawFlag) {
            this.firstDrawPoint();
            firstDrawFlag = false;
        }
//更新画面
        for (Point p : points) {
            canvas.drawCircle(p.x, p.y, p.radius, p.paint);
        }
        Point temPoint;
        //画线
        for (int i = 1; i <= selectedPoint.size(); i++) {
            if (i < selectedPoint.size()) {
                temPoint = selectedPoint.get(i);
            } else {
                temPoint = FINGER_POINT;
            }
            drawLineBetween2Points(canvas, selectedPoint.get(i - 1), temPoint);
        }

    }

    private void drawLineBetween2Points(Canvas canvas, Point point, Point point1) {
        canvas.drawLine(point.x, point.y, point1.x, point1.y, linePaint);
    }


    private Point checkSelectPoint(float x, float y) {
        for (Point p : points) {
            if (MathUtil.checkInRound(p.x, p.y, INIT_POINT_RADIUS * 3, (int) x, (int) y)) {
                return p;
            }
        }
        return null;
    }

    void vibratorPhone() {
        if (vibrator.hasVibrator()) {
            if (Build.VERSION.SDK_INT >= 26) {
                vibrator.vibrate(VibrationEffect.createOneShot(100, 200));
            } else {
                vibrator.vibrate(100);
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        super.onTouchEvent(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Point point = checkSelectPoint(event.getX(), event.getY());
                if (!selectedPoint.contains(point) && point != null) {
                    point.selected();
                    selectedPoint.add(point);
                    vibratorPhone();
                }
                return true;
            }
            case MotionEvent.ACTION_MOVE: {
                FINGER_POINT.x = event.getX();
                FINGER_POINT.y = event.getY();
                Point point = checkSelectPoint(event.getX(), event.getY());
                if (!selectedPoint.contains(point) && point != null) {
                    point.selected();
                    selectedPoint.add(point);
                    vibratorPhone();
                }
                break;
            }

            case MotionEvent.ACTION_UP: {
                performClick();
            }
            break;
            default:
        }

        this.postInvalidate();
        return false;
    }

    public void clearGesture() {
        for (Point p : selectedPoint) {
            p.unselected();
        }
        selectedPoint.clear();
        linePaint = NORMAL_LINE_PAINT;
    }

    public void errorGesture() {
        for (Point p : selectedPoint) {
            p.error();
        }
        linePaint = ERROR_LINE_PAINT;
    }

    @Override
    public boolean performClick() {
        super.performClick();

        if (!selectedPoint.isEmpty()) {
            Point endPoint = selectedPoint.get(selectedPoint.size() - 1);
            FINGER_POINT.setX(endPoint.getX());
            FINGER_POINT.setY(endPoint.getY());
        }

        callback.onResult(new Callback(getSelectedPoint()));

        return true;
    }

    private String getSelectedPoint() {
        StringBuilder sb = new StringBuilder();
        for (Point p : selectedPoint) {
            sb.append(p.index);
        }
        return sb.toString();
    }

}

颜色常量:

/**
 * @author 30819
 */
public class Constant {
    final public static int INIT_POINT_COLOR = 0xff8a8a8a;
    final public static int SELECTED_POINT_COLOR = 0xff7dc5eb;
    final public static int ERROR_POINT_COLOR = 0xffe32636;
}

point类:

package xxx

import android.graphics.Paint;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;


/**
 * @author 30819
 */
@Getter
@Setter
@NoArgsConstructor
public class Point {
    final static Paint INIT_POINT_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);
    final static Paint SELECT_POINT_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);
    final static Paint ERROR_POINT_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);

    final static int INIT_POINT_RADIUS = 10;
    final static int SELECTED_POINT_RADIUS = 2 * INIT_POINT_RADIUS;

    static {
        INIT_POINT_PAINT.setColor(INIT_POINT_COLOR);
        INIT_POINT_PAINT.setStyle(Paint.Style.STROKE);
        INIT_POINT_PAINT.setStrokeWidth(INIT_POINT_RADIUS >> 1);

        SELECT_POINT_PAINT.setColor(SELECTED_POINT_COLOR);

        ERROR_POINT_PAINT.setColor(ERROR_POINT_COLOR);
    }

    public static int STATE_NORMAL = 0;
    public static int STATE_CHECK = 1;
    public static int STATE_CHECK_ERROR = 2;

    public float x;
    public float y;
    public int state = STATE_NORMAL;
    public int index = 0;
    public int radius = INIT_POINT_RADIUS;
    public int color = INIT_POINT_COLOR;
    public Paint paint = INIT_POINT_PAINT;


    public Point(int value) {
        index = value;
    }


    public void selected() {
        radius = SELECTED_POINT_RADIUS;
        paint = SELECT_POINT_PAINT;
    }

    public void unselected() {
        radius = INIT_POINT_RADIUS;
        paint = INIT_POINT_PAINT;
    }

    public void error() {
        radius = SELECTED_POINT_RADIUS;
        paint = ERROR_POINT_PAINT;
    }
}

距离计算工具类:

public class MathUtil {
    public static boolean checkInRound(float sx, float sy, float r, float x,
                                       float y) {
        return Math.sqrt((sx - x) * (sx - x) + (sy - y) * (sy - y)) < r;
    }
}

使用代码:

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ImageView
            android:id="@+id/close_btn_icon"
            android:layout_width="24dp"
            android:layout_height="24dp"

            android:layout_alignParentEnd="true"
            android:layout_marginEnd="24dp"
            app:srcCompat="@drawable/ic_baseline_close_24"
            app:tint="#D9000000" />

    </RelativeLayout>

    <TextView
        android:id="@+id/tip_gesture_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:labelFor="@+id/gesture_password"
        android:tag="#FF0090FF"
        android:text="@string/init_string"
        android:textColor="#D9000000"
        android:textSize="16sp" />


    <xxx.GesturePasswordView
        android:id="@+id/gesture_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

Activity文件:

package xxx

import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;



import java.util.concurrent.ScheduledExecutorService;

/**
 * @author 30819
 */
public abstract class GestureCommon extends AppCompatActivity implements Common {

    protected final ScheduledExecutorService scheduler = AppExecutors.getInstance().schedule();

    protected AppCompatActivity gesturePwdActivity;

    protected Intent intent;
    protected TextView textView;
    protected GesturePasswordView gesturePasswordView;
    protected static int MIN_LENGTH_PASSWORD = 4;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        intent = getIntent();
        setContentView(R.layout.ct_gesture_password);
        gesturePasswordView = findViewById(R.id.gesture_password);

        DisplayMetrics metrics = this.getResources().getDisplayMetrics();
        int height = metrics.heightPixels;

        textView = findViewById(R.id.tip_gesture_info);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        lp.setMargins(0, (int) (height * 0.26), 0, 0);
        textView.setLayoutParams(lp);

        //set close btn
        final ImageView closeImageView = findViewById(R.id.close_btn_icon);
        closeImageView.setOnClickListener((event)->finish());
        
    }

    @Override
    protected void onStart() {
        super.onStart();
        shakeTip();
    }

    protected void shakeTip(){
        //动画左右抖动
        //加载动画资源文件
        Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake_x);
        //表示重复多次;可以再shake_x设置
        shake.setRepeatCount(1);
        //给组件播放动画效果
        textView.startAnimation(shake);
    }
}

注册界面:

package xxx

import android.os.Bundle;
import androidx.annotation.Nullable;
import android.util.Log;


import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import lombok.Setter;


/**
 * @author 30819
 */
@Setter
public class GestureRegisterActivity extends GestureCommon {
    private static final String TAG = GestureRegisterActivity.class.getName();
    protected String prePassword = null;
    protected Runnable task = null;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        gesturePwdActivity = this;

        textView.setText("请绘制两次手势图案");
        textView.setTextColor(INIT_POINT_COLOR);


        task = () -> AppExecutors.getInstance().mainThread().execute(() -> {
            textView.setTextColor(INIT_POINT_COLOR);
            gesturePasswordView.clearGesture();
            gesturePasswordView.postInvalidate();
            Log.d(TAG, "定时任务,清除画面");
        });


        gesturePasswordView.setCallback(new Callback() {

            @Override
            public void onResult(Result result) {
                if (result.getMsg().length() < MIN_LENGTH_PASSWORD) {
                    textView.setText("图案小于四位");
                    textView.setTextColor(ERROR_POINT_COLOR);
                    gesturePasswordView.errorGesture();
                    gesturePasswordView.postInvalidate();
                    prePassword = null;
                    shakeTip();
                    Log.v(TAG, "图案小于四位");
                    scheduler.schedule(task, 500, TimeUnit.MILLISECONDS);
                    return;
                }

                if (prePassword == null) {
                    scheduler.schedule(task, 500, TimeUnit.MILLISECONDS);
                    prePassword = result.getMsg();
                    Log.d(TAG, "第一次密码" + prePassword);
                    textView.setText("请再次绘制手势图案");
                    shakeTip();
                    return;
                }
                Log.d("第二次密码", result.getMsg());
                if (prePassword.equals(result.getMsg())) {
                    Runnable runnable = () -> registerGesture();
                    AppExecutors.getInstance().networkIO().execute(runnable);

                } else {
                    gesturePasswordView.errorGesture();
                    gesturePasswordView.postInvalidate();
                    textView.setText("请重新绘制手势图案");
                    textView.setTextColor(ERROR_POINT_COLOR);
                    shakeTip();
                    prePassword = null;
                    Log.d(TAG, "两次密码不一致,清除画面");
                    scheduler.schedule(task, 1000, TimeUnit.MILLISECONDS);
                }

            }
        });
    }

    private void registerGesture() {
		//todo
    }

}

同样的,认证界面实现也是类似的,只要在回调函数中完成认证逻辑即可。