Android自定義點(diǎn)選驗(yàn)證碼

上一篇文章里留瞳,通過簡(jiǎn)單的思路分析和代碼演示胡陪,最終實(shí)現(xiàn)了自定義拼圖驗(yàn)證碼
的效果。接下來颤练,我們要實(shí)現(xiàn)的是如下圖所示的漢字點(diǎn)選驗(yàn)證碼:

ezgif-3-a89a55a08fab.gif

分析

效果圖可以分成上下兩個(gè)部分既忆,其中第 2 可以通過常規(guī)組合布局的方式實(shí)現(xiàn),我們只需重點(diǎn)關(guān)注第 1 即可嗦玖,實(shí)現(xiàn)了第 1 患雇,然后通過組合布局的方式,把上下兩個(gè)部分放一起宇挫,驗(yàn)證碼就算是大功告成了苛吱。


QQ20190705-093535.png

第 1 部分思路分析:

  1. 準(zhǔn)備一張圖片,通過canvas.drawBitmap()方法畫出背景圖
  2. 隨機(jī)生成4個(gè)坐標(biāo)點(diǎn)器瘪,通過canvas.drawText()方法依次把預(yù)設(shè)的漢字寫到畫板上翠储。這里的隨機(jī)绘雁,你要考慮以下情況
    • 邊界。如果生成的左邊剛好在(x彰亥,fontSize)怎么辦咧七,這時(shí)候文字可能跑到畫布邊界外面去了。
      繪制越界.png
  • 重合任斋。如果第一次生成的坐標(biāo)是(100继阻,100), 第二次生成的坐標(biāo)也是(100废酷,100)瘟檩,或者(101,101)澈蟆,那兩個(gè)漢字豈不是繪制在同一位置了墨辛。
    繪制正常.png

    繪制重合.png

    所以,除了要結(jié)合畫布大小和文字大小趴俘,計(jì)算出一塊安全區(qū)域睹簇,在安全區(qū)域內(nèi)隨機(jī)生成坐標(biāo)點(diǎn),還要保存已繪制文字區(qū)域范圍 region寥闪,對(duì)下一次要隨機(jī)生成的坐標(biāo)點(diǎn)太惠,先判斷是不是在已繪制文字區(qū)域內(nèi),避免文字在同一塊區(qū)域重復(fù)繪制疲憋。
  1. 在用戶交互上凿渊,我們希望點(diǎn)擊點(diǎn)如果在文字區(qū)域內(nèi),則顯示用戶點(diǎn)擊的順序缚柳,如果不在埃脏,則不處理點(diǎn)擊事件。這里可以點(diǎn)擊點(diǎn)為中心畫圓背景秋忙,然后結(jié)合圓背景大小彩掐、序號(hào)文字大小,計(jì)算序號(hào)文字需要顯示的位置翰绊,盡量顯示在圓正中佩谷。
  2. 完成界面的繪制后,剩下的就是邏輯判斷和回調(diào)接口處理监嗜,根據(jù)記錄的文字和點(diǎn)擊順序判斷就可以了。對(duì)外暴露設(shè)置文字抡谐、點(diǎn)擊刷新和判斷結(jié)果回調(diào)裁奇。

代碼實(shí)現(xiàn)

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;
    /*根據(jù)準(zhǔn)備的圖片重新調(diào)整尺寸后的背景圖*/
    private Bitmap bgBitmap;
    private Paint bgPaint;
    private RectF bgRectF;

    /*驗(yàn)證碼文字畫筆*/
    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(), "驗(yàn)證成功!", Toast.LENGTH_SHORT).show();
                    listener.onResult(true);
                    break;
                default:
                    Toast.makeText(getContext(), "驗(yàn)證失敗!", 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);

        /*在處理點(diǎn)擊的時(shí)候需要繪制用戶點(diǎn)擊的順序,這時(shí)候要判斷是初始化驗(yàn)證碼麦撵,還是用戶在點(diǎn)擊需要繪制點(diǎn)擊的序號(hào)
        * 如果是初始化驗(yàn)證碼刽肠,就隨機(jī)生成文字溃肪,繪制文字
        * 如果是用戶在點(diǎn)擊,需要繪制點(diǎn)擊的順序音五,這時(shí)候就不能重新隨機(jī)生成坐標(biāo)點(diǎn)惫撰,要讓文字位置保持不動(dòng)才行,否則會(huì)出現(xiàn)點(diǎn)擊一次躺涝,隨機(jī)生成一次驗(yàn)證碼的情況
        * */
        if (isInit) {

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

            for (int i = 0; i < fonts.length(); i++) {
                /*這里把文字倒著寫是為了后面的驗(yàn)證方便*/
                String s = String.valueOf(fonts.charAt(fonts.length() - i - 1));
                int textSize = (int) textPaint.measureText(s);
                canvas.save();
                /*在指定范圍隨機(jī)生成坐標(biāo)點(diǎn)*/
                int x = random.nextInt(width - textSize);
                int y = random.nextInt(height - textSize);

                /*如果檢測(cè)到點(diǎn)和文字區(qū)域有重合厨钻,則要重新隨機(jī)生成點(diǎn)坐標(biāo),這里四個(gè)條件坚嗜,分別是如果以(x,y)為文字繪制坐標(biāo)  的  四個(gè)角的位置
                * 這里有一點(diǎn)繞夯膀,理解困難的最好在草紙上比劃比劃*/
                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);
                /*隨機(jī)生成一個(gè)30以內(nèi)的整數(shù),使文字傾斜一定的角度*/
                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();

                /*效果圖上用戶點(diǎn)擊文字會(huì)出現(xiàn)序號(hào)顯示這是點(diǎn)擊的第幾個(gè)苍蔬,而驗(yàn)證碼文字沒有變化诱建,其實(shí)驗(yàn)證碼文字這里也重新繪制了,
                只不過還是原來的位置碟绑、角度*/
                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();
            }

            /*繪制點(diǎn)擊的序號(hào)*/
            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;
    }

}

然后是組合布局俺猿,比較簡(jiǎn)單,這里只放示例代碼:
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();  //獲取對(duì)話框當(dāng)前的參數(shù)值
        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 = "悛醍躞稂怙惡瓜沱狖獨(dú)泗薁臧齬齟腌咄圄砭靁針奉饕瀣圭其罰立軛犄時(shí)孓氣覦鼯綿頂娜旮醐耋孑蘡娉灌瓞臢臬弊裊龍為呶耄煢行踽覬角旯虺蹀餮沆涕休陟莠軒滂囹不婷否龘嗟";
        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("請(qǐng)依次點(diǎn)擊 <font color=\"#FF0000\"><b>" + verifyText + "</b></font>"));
    }


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末格仲,一起剝皮案震驚了整個(gè)濱河市押袍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抓狭,老刑警劉巖伯病,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異否过,居然都是意外死亡午笛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門苗桂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來药磺,“玉大人,你說我怎么就攤上這事。” “怎么了齐饮?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵劝堪,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我峦剔,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任姚建,我火速辦了婚禮,結(jié)果婚禮上吱殉,老公的妹妹穿的比我還像新娘掸冤。我一直安慰自己厘托,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布稿湿。 她就那樣靜靜地躺著铅匹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪饺藤。 梳的紋絲不亂的頭發(fā)上包斑,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音策精,去河邊找鬼舰始。 笑死,一個(gè)胖子當(dāng)著我的面吹牛咽袜,可吹牛的內(nèi)容都是我干的丸卷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼询刹,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼谜嫉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起凹联,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤沐兰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蔽挠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體住闯,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年澳淑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了比原。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杠巡,死狀恐怖量窘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情氢拥,我是刑警寧澤蚌铜,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站嫩海,受9級(jí)特大地震影響冬殃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叁怪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一造壮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骂束,春花似錦耳璧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至混驰,卻和暖如春攀隔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背栖榨。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工昆汹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人婴栽。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓满粗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親愚争。 傳聞我的和親對(duì)象是個(gè)殘疾皇子映皆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353