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

1.前言

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

好吧,废话说不下去了,直接上Demo截图:

Screenrecorder-2017-05-14-02-50-58-795_20170514164200.gif

2.使用指纹识别

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

 3.使用密码解锁
Screenshot_2017-05-14-14-53-29-795_com.chengww.fingerdemo.png

指纹识别的使用

官方标准库
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有个坑,稍后再讲。

Screenshot_2017-05-14-14-53-47-312_com.chengww.fingerdemo.png

密码解锁弹窗样式,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/

推荐阅读更多精彩内容