安卓指纹+密码支付(解锁)仿支付宝Demo

1.前言

Google 从 Android6.0(api23)就开始提供标准指纹识别支持,并对外提供指纹识别相关的接口。但是Android上的指纹识别似乎就是用来解锁手机屏幕,三方APP应用指纹的也是寥寥无几。一直想踩下安卓指纹识别的坑,直到这两天终于空出时间来尝试下android指纹识别的应用。

好吧,废话少说,show me the code,先上 Demo 截图:

2.使用指纹识别

点击指纹识别 button,弹出如图弹窗,弹窗使用 DialogFragment。具体实现请看下面

官方标准库

Google 提供的与指纹识别相关的核心类不多,主类是 FingerprintManager,主类依赖三个内部类,如下图所示:

FingerprintManager 主要提供三个方法如下:

FingerprintManager.AuthenticationCallback 类提供的回调接口如下,重点区分红色下划线标注的部分

启动指纹识别接口

看了上面的介绍,如果要写代码就变得简单了

1. AndroidManifest 权限声明

<uses-permission android:name="android.permission.USE_FINGERPRINT"/>

2. 获取 FingerManager 服务对象

public static FingerprintManager getFingerprintManager(Context context) { 
FingerprintManager fingerprintManager = null;
try {
fingerprintManager = (FingerprintManager)context.getSystemService(Context.FINGERPRINT_SERVICE);
} catch (Throwable e) {
Log.e("TAG","have not class FingerprintManager");
}
return fingerprintManager;
}

3. 启动指纹识别

mFingerprintManager.authenticate(cryptoObject, mCancellationSignal, 0, mAuthCallback, null);

官方v4兼容包

上面介绍最标准的官方实现指纹识别的方式,当然适配肯定没这么简单,因为有很多设备兼容性要考虑,
Google 后续在 v4 包中提供了一套完整的实现,实现类与上面的一一对应的,
就是改了个名字(FingerprintManager 改为了 FingerprintManagerCompat,
机智的发现 Compat 是兼容的意思,所以 Google 在 v4 包中做了一些兼容性处理),
做了很多兼容处理,官方推荐使用后者。v4 包中类结构如下:

v4包中的类使用与上面标准库中的一致,就是名字不一样而已,这里不再介绍使用方式。

3.使用密码解锁

指纹识别失败达到一定次数调用密码解锁,同指纹识别弹窗一样使用 DialogFragment。
用这个 DialogFragment 有个坑,稍后再讲。

密码解锁弹窗样式,fragment_pwd.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="40dp"
android:layout_marginRight="40dp"
android:layout_marginTop="100dp"
android:background="@drawable/shape_dialog"
android:orientation="vertical"
android:paddingBottom="@dimen/spacing_large">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
style="@style/style_black_normal_text"
android:layout_width="wrap_content"
android:layout_height="@dimen/text_item_height"
android:layout_centerInParent="true"
android:gravity="center"
android:text="请输入密码" />

<ImageView
android:id="@+id/iv_close"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="@drawable/selector_item_pressed"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/spacing_tiny"
android:src="@mipmap/icon_del" />

</RelativeLayout>

<View style="@style/style_separate_line" />

<com.chengww.fingerdemo.PwdView
android:id="@+id/pwdView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/spacing_large"
android:layout_marginRight="@dimen/spacing_large"
android:background="@color/white" />

<TextView
android:id="@+id/tv_miss_pwd"
style="@style/style_blue_normal_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/text_item_right_margin"
android:layout_marginEnd="@dimen/text_item_right_margin"
android:layout_marginRight="@dimen/text_item_right_margin"
android:text="忘记密码?"
android:background="@drawable/selector_item_pressed"
android:layout_gravity="end"
android:gravity="center" />

</LinearLayout>

<com.chengww.fingerdemo.InputMethodView
android:id="@+id/inputMethodView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />

</RelativeLayout>

密码显示圆点框

public class PwdView extends View {

private ArrayList<String> result;//输入结果保存
private int count;//密码位数
private int size;//默认每一格的大小
private Paint mBorderPaint;//边界画笔
private Paint mDotPaint;//掩盖点的画笔
private int mBorderColor;//边界颜色
private int mDotColor;//掩盖点的颜色
private RectF mRoundRect;//外面的圆角矩形
private int mRoundRadius;//圆角矩形的圆角程度

public PwdView(Context context) {
super(context);
init(null);
}

private InputCallBack inputCallBack;//输入完成的回调
private InputMethodView inputMethodView; //输入键盘


public interface InputCallBack {
void onInputFinish(String result);
}

public PwdView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}

public PwdView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}

/**
* 初始化相关参数
*/
void init(AttributeSet attrs) {
final float dp = getResources().getDisplayMetrics().density;
this.setFocusable(true);
this.setFocusableInTouchMode(true);
result = new ArrayList<>();
if (attrs != null) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.PwdView);
mBorderColor = ta.getColor(R.styleable.PwdView_border_color, Color.LTGRAY);
mDotColor = ta.getColor(R.styleable.PwdView_dot_color, Color.BLACK);
count = ta.getInt(R.styleable.PwdView_count, 6);
ta.recycle();
} else {
mBorderColor = Color.LTGRAY;
mDotColor = Color.GRAY;
count = 6;//默认6位密码
}
size = (int) (dp * 30);//默认30dp一格
//color
mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBorderPaint.setStrokeWidth(3);
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setColor(mBorderColor);

mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDotPaint.setStrokeWidth(3);
mDotPaint.setStyle(Paint.Style.FILL);
mDotPaint.setColor(mDotColor);
mRoundRect = new RectF();
mRoundRadius = (int) (5 * dp);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = measureWidth(widthMeasureSpec);
int h = measureHeight(heightMeasureSpec);
int wsize = MeasureSpec.getSize(widthMeasureSpec);
int hsize = MeasureSpec.getSize(heightMeasureSpec);
//宽度没指定,但高度指定
if (w == -1) {
if (h != -1) {
w = h * count;//宽度=高*数量
size = h;
} else {//两个都不知道,默认宽高
w = size * count;
h = size;
}
} else {//宽度已知
if (h == -1) {//高度不知道
h = w / count;
size = h;
}
}
setMeasuredDimension(Math.min(w, wsize), Math.min(h, hsize));
}

private int measureWidth(int widthMeasureSpec) {
//宽度
int wmode = MeasureSpec.getMode(widthMeasureSpec);
int wsize = MeasureSpec.getSize(widthMeasureSpec);
if (wmode == MeasureSpec.AT_MOST) {//wrap_content
return -1;
}
return wsize;
}

private int measureHeight(int heightMeasureSpec) {
//高度
int hmode = MeasureSpec.getMode(heightMeasureSpec);
int hsize = MeasureSpec.getSize(heightMeasureSpec);
if (hmode == MeasureSpec.AT_MOST) {//wrap_content
return -1;
}
return hsize;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {//点击控件弹出输入键盘
requestFocus();
inputMethodView.setVisibility(VISIBLE);
return true;
}
return true;
}

@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (gainFocus) {
inputMethodView.setVisibility(VISIBLE);
} else {
inputMethodView.setVisibility(GONE);
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int width = getWidth() - 2;
final int height = getHeight() - 2;
//先画个圆角矩形
mRoundRect.set(0, 0, width, height);
canvas.drawRoundRect(mRoundRect, 0, 0, mBorderPaint);
//画分割线
for (int i = 1; i < count; i++) {
final int x = i * size;
canvas.drawLine(x, 0, x, height, mBorderPaint);
}
//画掩盖点,
// 这是前面定义的变量 private ArrayList<Integer> result;//输入结果保存
int dotRadius = size / 8;//圆圈占格子的三分之一
for (int i = 0; i < result.size(); i++) {
final float x = (float) (size * (i + 0.5));
final float y = size / 2;
canvas.drawCircle(x, y, dotRadius, mDotPaint);
}
}

@Override
public boolean onCheckIsTextEditor() {
return true;
}

@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;//输入类型为数字
outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
return new MyInputConnection(this, false);
}

public void setInputCallBack(InputCallBack inputCallBack) {
this.inputCallBack = inputCallBack;
}

public void clearResult() {
result.clear();
invalidate();
}


private class MyInputConnection extends BaseInputConnection {
public MyInputConnection(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
}

@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
//这里是接受输入法的文本的,我们只处理数字,所以什么操作都不做
return super.commitText(text, newCursorPosition);
}

@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
//软键盘的删除键 DEL 无法直接监听,自己发送del事件
if (beforeLength == 1 && afterLength == 0) {
return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
&& super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
}
return super.deleteSurroundingText(beforeLength, afterLength);
}
}


/**
* 设置输入键盘view
*
* @param inputMethodView
*/
public void setInputMethodView(InputMethodView inputMethodView) {
this.inputMethodView = inputMethodView;
this.inputMethodView.setInputReceiver(new InputMethodView.InputReceiver() {
@Override
public void receive(String num) {
if (num.equals("-1")) {
if (!result.isEmpty()) {
result.remove(result.size() - 1);
invalidate();
}
} else {
if (result.size() < count) {
result.add(num);
invalidate();
ensureFinishInput();
}
}


}
});
}

/**
* 判断是否输入完成,输入完成后调用callback
*/
void ensureFinishInput() {
if (result.size() == count && inputCallBack != null) {//输入完成
StringBuffer sb = new StringBuffer();
for (String i : result) {
sb.append(i);
}
inputCallBack.onInputFinish(sb.toString());
clearResult();
}
}

/**
* 获取输入文字
*
* @return
*/
public String getInputText() {
if (result.size() == count) {
StringBuffer sb = new StringBuffer();
for (String i : result) {
sb.append(i);
}
return sb.toString();
}
return null;
}
}

下方输入键盘

public class InputMethodView extends LinearLayout implements View.OnClickListener {

private InputReceiver inputReceiver;

public InputMethodView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.view_password_input, this);

initView();
}

private void initView() {
findViewById(R.id.btn_1).setOnClickListener(this);
findViewById(R.id.btn_2).setOnClickListener(this);
findViewById(R.id.btn_3).setOnClickListener(this);
findViewById(R.id.btn_4).setOnClickListener(this);
findViewById(R.id.btn_5).setOnClickListener(this);
findViewById(R.id.btn_6).setOnClickListener(this);
findViewById(R.id.btn_7).setOnClickListener(this);
findViewById(R.id.btn_8).setOnClickListener(this);
findViewById(R.id.btn_9).setOnClickListener(this);
findViewById(R.id.btn_0).setOnClickListener(this);
findViewById(R.id.btn_del).setOnClickListener(this);

findViewById(R.id.layout_hide).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
setVisibility(GONE);
}
});
}

@Override
public void onClick(View v) {
String num = (String) v.getTag();
this.inputReceiver.receive(num);
}


/**
* 设置接收器
* @param receiver
*/
public void setInputReceiver(InputReceiver receiver){
this.inputReceiver = receiver;
}

/**
* 输入接收器
*/
public interface InputReceiver{

void receive(String num);
}
}

MainActivity 实现输入回调就可以得到回调结果了

public class MainActivity extends AppCompatActivity implements PwdView.InputCallBack{
@Override
public void onInputFinish(String result) {
if (result.equals("123456")) {
fragment.dismiss();
Toast.makeText(this, "验证成功", Toast.LENGTH_SHORT).show();
}else {
showPwdError();
}
}
}

今天暂时写这么多吧,整个项目还有点 BUG,标题说仿支付宝也仿的不像,
改天把后半部分整理出来修改下再发个完整版的。

源代码下载:
http://git.oschina.net/chengww5217/fingerdemo

指纹解锁部分参考引用了以下文章,原作者指纹识别部分写的非常棒,强烈建议前往拜读:
http://www.cnblogs.com/popfisher/p/6063835.html
https://willowtreeapps.com/ideas/android-fingerprint-apis-an-overview-for-android-app-developers/

文章作者: chengww
文章链接: https://chengww.com/archives/Persional_Android_fingerprint_useage.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 chengww's blog