Android自定义点选验证码

上一篇文章里,通过简单的思路分析和代码演示,最终实现了自定义拼图验证码
的效果。接下来,我们要实现的是如下图所示的汉字点选验证码:

ezgif-3-a89a55a08fab.gif

分析

效果图可以分成上下两个部分,其中第 2 可以通过常规组合布局的方式实现,我们只需重点关注第 1 即可,实现了第 1 ,然后通过组合布局的方式,把上下两个部分放一起,验证码就算是大功告成了。


QQ20190705-093535.png

第 1 部分思路分析:

  1. 准备一张图片,通过canvas.drawBitmap()方法画出背景图
  2. 随机生成4个坐标点,通过canvas.drawText()方法依次把预设的汉字写到画板上。这里的随机,你要考虑以下情况
    • 边界。如果生成的左边刚好在(x,fontSize)怎么办,这时候文字可能跑到画布边界外面去了。
      绘制越界.png
  • 重合。如果第一次生成的坐标是(100,100), 第二次生成的坐标也是(100,100),或者(101,101),那两个汉字岂不是绘制在同一位置了。
    绘制正常.png

    绘制重合.png

    所以,除了要结合画布大小和文字大小,计算出一块安全区域,在安全区域内随机生成坐标点,还要保存已绘制文字区域范围 region,对下一次要随机生成的坐标点,先判断是不是在已绘制文字区域内,避免文字在同一块区域重复绘制。
  1. 在用户交互上,我们希望点击点如果在文字区域内,则显示用户点击的顺序,如果不在,则不处理点击事件。这里可以点击点为中心画圆背景,然后结合圆背景大小、序号文字大小,计算序号文字需要显示的位置,尽量显示在圆正中。
  2. 完成界面的绘制后,剩下的就是逻辑判断和回调接口处理,根据记录的文字和点击顺序判断就可以了。对外暴露设置文字、点击刷新和判断结果回调。

代码实现

TapVerificationView.java

package com.example.qingfengwei.myapplication;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class TapVerificationView extends View {

    /*画布宽高*/
    private int width;
    private int height;

    private Bitmap oldBitmap;
    /*根据准备的图片重新调整尺寸后的背景图*/
    private Bitmap bgBitmap;
    private Paint bgPaint;
    private RectF bgRectF;

    /*验证码文字画笔*/
    private Paint textPaint;
    private Paint selectPaint;
    private Paint selectTextPaint;
    private List<Region> regions = new ArrayList<Region>();

    private Random random;

    private String fonts = "";
    private int checkCode = 0;


    private List<Point> tapPoints = new ArrayList<Point>();
    private List<Integer> tapIndex = new ArrayList<Integer>();

    private List<Point> textPoints = new ArrayList<Point>();
    private List<Integer> degrees = new ArrayList<Integer>();

    private boolean isInit = true;

    public TapVerificationView(Context context) {
        this(context, null);
    }

    public TapVerificationView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TapVerificationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        oldBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.syzt);

        bgPaint = new Paint();
        bgPaint.setAntiAlias(true);
        bgPaint.setFilterBitmap(true);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setFakeBoldText(true);
        textPaint.setColor(Color.parseColor("#AA000000"));
        textPaint.setShadowLayer(3, 2, 2, Color.RED);
        textPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN));

        selectPaint = new Paint();
        selectPaint.setAntiAlias(true);
        selectPaint.setStyle(Paint.Style.FILL);
        selectPaint.setColor(Color.WHITE);


        selectTextPaint = new Paint();

        random = new Random();

        int temp = fonts.length() - 1;
        while (temp > -1) {
            checkCode += temp * Math.pow(10, temp);
            temp--;
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int minimumWidth = getSuggestedMinimumWidth();
        int minimumHeight = getSuggestedMinimumHeight();
        width = measureSize(minimumWidth, widthMeasureSpec);
        height = width;
        bgBitmap = clipBitmap(oldBitmap, width, height);
        bgRectF = new RectF(0, 0, width, height);
        textPaint.setTextSize(width / 6);
        setMeasuredDimension(width, height);
    }

    public Bitmap clipBitmap(Bitmap bm, int newWidth, int newHeight) {
        int width = bm.getWidth();
        int height = bm.getHeight();
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
    }

    private int measureSize(int defaultSize, int measureSpec) {
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        int result = defaultSize;
        switch (mode) {
            case MeasureSpec.UNSPECIFIED:
                result = defaultSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = size;
                break;
        }
        return result;
    }

    public static int dp2px(float dp) {
        float density = Resources.getSystem().getDisplayMetrics().density;
        return (int) (density * dp + 0.5f);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                int x = (int) event.getX();
                int y = (int) event.getY();
                for (Region region : regions) {
                    if (region.contains(x, y)) {

                        isInit = false;

                        int index = regions.indexOf(region);
                        if (!tapIndex.contains(index)) {
                            tapIndex.add(index);
                            tapPoints.add(new Point(x, y));
                        }


                        if (tapIndex.size() == fonts.length()) {
                            StringBuilder s = new StringBuilder();
                            for (Integer i : tapIndex) {
                                s.append(i);
                            }
                            int result = Integer.parseInt(s.toString());
                            if (result == checkCode) {
                                handler.sendEmptyMessage(1);
                            } else {
                                handler.sendEmptyMessage(0);
                            }
                        }

                        invalidate();
                    }
                }
        }
        return false;
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            int result = msg.what;
            switch (result) {
                case 1:
                    Toast.makeText(getContext(), "验证成功!", Toast.LENGTH_SHORT).show();
                    listener.onResult(true);
                    break;
                default:
                    Toast.makeText(getContext(), "验证失败!", Toast.LENGTH_SHORT).show();
                    postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            reDrew();
                            listener.onResult(false);
                        }
                    }, 1000);
                    break;
            }
        }
    };

    private boolean checkCover(int x, int y) {
        for (Region region : regions) {
            if (region.contains(x, y)) {
                return true;
            }
        }
        return false;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        regions.clear();
        canvas.drawBitmap(bgBitmap, null, bgRectF, bgPaint);

        /*在处理点击的时候需要绘制用户点击的顺序,这时候要判断是初始化验证码,还是用户在点击需要绘制点击的序号
        * 如果是初始化验证码,就随机生成文字,绘制文字
        * 如果是用户在点击,需要绘制点击的顺序,这时候就不能重新随机生成坐标点,要让文字位置保持不动才行,否则会出现点击一次,随机生成一次验证码的情况
        * */
        if (isInit) {

            textPoints.clear();
            degrees.clear();
            tapIndex.clear();
            tapPoints.clear();

            for (int i = 0; i < fonts.length(); i++) {
                /*这里把文字倒着写是为了后面的验证方便*/
                String s = String.valueOf(fonts.charAt(fonts.length() - i - 1));
                int textSize = (int) textPaint.measureText(s);
                canvas.save();
                /*在指定范围随机生成坐标点*/
                int x = random.nextInt(width - textSize);
                int y = random.nextInt(height - textSize);

                /*如果检测到点和文字区域有重合,则要重新随机生成点坐标,这里四个条件,分别是如果以(x,y)为文字绘制坐标  的  四个角的位置
                * 这里有一点绕,理解困难的最好在草纸上比划比划*/
                while (checkCover(x, y) || checkCover(x, y + textSize) || checkCover(x + textSize, y) || checkCover(x + textSize, y + textSize)) {
                    x = random.nextInt(width - textSize);
                    y = random.nextInt(height - textSize);
                }

                textPoints.add(new Point(x, y));
                canvas.translate(x, y);
                /*随机生成一个30以内的整数,使文字倾斜一定的角度*/
                int degree = random.nextInt(30);
                degrees.add(degree);
                canvas.rotate(degree);
                canvas.drawText(s, 0, textSize, textPaint);
                regions.add(new Region(x, y, textSize + x, textSize + y));
                canvas.restore();
            }
        } else {
            for (int i = 0; i < fonts.length(); i++) {
                String s = String.valueOf(fonts.charAt(fonts.length() - i - 1));
                int textSize = (int) textPaint.measureText(s);

                canvas.save();

                /*效果图上用户点击文字会出现序号显示这是点击的第几个,而验证码文字没有变化,其实验证码文字这里也重新绘制了,
                只不过还是原来的位置、角度*/
                int x = textPoints.get(i).x;
                int y = textPoints.get(i).y;
                int degree = degrees.get(i);
                canvas.translate(x, y);
                canvas.rotate(degree);
                canvas.drawText(s, 0, textSize, textPaint);
                regions.add(new Region(x, y, textSize + x, textSize + y));
                canvas.restore();
            }

            /*绘制点击的序号*/
            for (Point point : tapPoints) {
                int index = tapPoints.indexOf(point) + 1;
                String s = index + "";
                int textSize = width / 6 / 3;
                selectTextPaint.setTextSize(textSize);
                canvas.drawCircle(point.x, point.y, textSize, selectPaint);


                Rect rect = new Rect();
                selectTextPaint.getTextBounds(s, 0, 1, rect);

                int textWidth = rect.width();
                int textHeight = rect.height();

                canvas.drawText(s, point.x - textWidth / 2, point.y + textHeight / 2, selectTextPaint);
            }
        }
    }

    public void reDrew() {
        textPoints.clear();
        degrees.clear();
        tapIndex.clear();
        tapPoints.clear();

        isInit = true;

        invalidate();
    }

    public void setVerifyText(String s){
        fonts = s;

        checkCode = 0;
        int temp = fonts.length() - 1;
        while (temp > -1) {
            checkCode += temp * Math.pow(10, temp);
            temp--;
        }

        invalidate();
    }

    private OnVerifyListener listener;

    public void setVerifyListener(OnVerifyListener listener) {
        this.listener = listener;
    }

}

然后是组合布局,比较简单,这里只放示例代码:
dialog_verify.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:gravity="center"
    android:orientation="vertical">

    <com.example.qingfengwei.myapplication.TapVerificationView
        android:id="@+id/tap_verify_view"
        android:layout_width="match_parent"
        android:layout_height="250dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="5dp">

        <TextView
            android:id="@+id/verify_text"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/refresh_verify"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:background="@mipmap/jyw_refresh" />
    </LinearLayout>
</LinearLayout>

VerifyCationDialog.java

package com.example.qingfengwei.myapplication;

import android.app.Dialog;
import android.content.Context;
import android.text.Html;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;

import java.util.Random;


public class VerifyCationDialog extends Dialog {

    private int style;
    private TapVerificationView nofTapVerificationView;
    private Button btnRefresh;
    private TextView tvVerifyCode;

    public VerifyCationDialog(Context context, int style) {
        super(context);
        this.style = style;
        init(context);
    }

    private void init(Context context) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        SlidingVerificationView slidingVerificationView = new SlidingVerificationView(context);

        slidingVerificationView.setVerifyListener(new OnVerifyListener() {
            @Override
            public void onResult(boolean isSuccess) {
                if (isSuccess) {
                    dismiss();
                }
                if(listener!=null){
                    listener.onResult(isSuccess);
                }
            }
        });

        if (style == 1) {
            setContentView(R.layout.dialog_verify);
            nofTapVerificationView = findViewById(R.id.tap_verify_view);
            btnRefresh = findViewById(R.id.refresh_verify);
            tvVerifyCode = findViewById(R.id.verify_text);

            setVerifyText();

            nofTapVerificationView.setVerifyListener(new OnVerifyListener() {
                @Override
                public void onResult(boolean isSuccess) {
                    if (isSuccess) {
                        dismiss();
                    } else {
                        setVerifyText();
                    }

                    if(listener!=null){
                        listener.onResult(isSuccess);
                    }

                }
            });

            btnRefresh.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    setVerifyText();
                    nofTapVerificationView.reDrew();
                }
            });

        } else {
            setContentView(slidingVerificationView);
        }

        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        int displayWidth = dm.widthPixels;
        int displayHeight = dm.heightPixels;
        WindowManager.LayoutParams p = getWindow().getAttributes();  //获取对话框当前的参数值
        if (displayWidth > displayHeight) {
            if (style == 1) {
                p.width = (int) (displayWidth * 0.4);
            } else {
                p.width = (int) (displayWidth * 0.6);
            }

        } else {
            if (style == 1) {
                p.width = (int) (displayWidth * 0.7);
            } else {
                p.width = (int) (displayWidth * 0.9);
            }
        }

        getWindow().setGravity(Gravity.CENTER);
        getWindow().setAttributes(p);
        getWindow().setBackgroundDrawableResource(android.R.color.transparent);
        setCanceledOnTouchOutside(true);
        setCancelable(true);
    }

    private void setVerifyText() {
        String baseText = "悛醍躞稂怙恶瓜沱狖独泗薁臧龉龃腌咄圄砭靁针奉饕瀣圭其罚立轭犄时孓气觎鼯绵顶娜旮醐耋孑蘡娉灌瓞臢臬弊袅龙为呶耄茕行踽觊角旯虺蹀餮沆涕休陟莠轩滂囹不婷否龘嗟";
        Random random = new Random();
        int start = random.nextInt(baseText.length() - 4 - 1);
        int end = start + 4;
        String verifyText = baseText.substring(start, end);
        nofTapVerificationView.setVerifyText(verifyText);
        tvVerifyCode.setText(Html.fromHtml("请依次点击 <font color=\"#FF0000\"><b>" + verifyText + "</b></font>"));
    }


    private OnVerifyListener listener;
    public void setVerifyListener(OnVerifyListener listener){
        this.listener = listener;
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249