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 ; } size = (int ) (dp * 30 ); 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) { return -1 ; } return wsize; } private int measureHeight (int heightMeasureSpec) { int hmode = MeasureSpec.getMode(heightMeasureSpec); int hsize = MeasureSpec.getSize(heightMeasureSpec); if (hmode == MeasureSpec.AT_MOST) { 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); } 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) { 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); } } 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(); } } } }); } void ensureFinishInput () { if (result.size() == count && inputCallBack != null ) { StringBuffer sb = new StringBuffer(); for (String i : result) { sb.append(i); } inputCallBack.onInputFinish(sb.toString()); clearResult(); } } 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); } 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/