效果:
九宫格 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
}
}
同样的,认证界面实现也是类似的,只要在回调函数中完成认证逻辑即可。