1. 程式人生 > >Android拼圖滑塊驗證碼控制元件

Android拼圖滑塊驗證碼控制元件

轉自:https://blog.csdn.net/sdfsdfdfa/article/details/79120665

概述

      驗證碼是可以區分使用者是人還是計算機。可以防止破解密碼、刷票等惡意行為。客戶端上多數用在關鍵操作上,比如購買、登入、註冊等場景。本文將介紹Android拼圖滑塊驗證碼控制元件是如何一步一步編寫形成的。希望能幫助到大家。 
       當然,如果僅僅是實現效果如何實現就太沒技術含量了。本文將通過介紹如何編寫此控制元件的同時,介紹Android自定義控制元件的一些技巧。

效果圖

分析

      由效果圖可以看到一張圖片,一個可拖動滑塊條,拖動滑塊條,滑塊拼圖跟隨滑動,滑動拼圖到正確位置並鬆開手指,圖下方會出現驗證資訊,滑動到錯誤位置並鬆開,也會出現驗證資訊。 
      看到這樣的效果,很多人估計都能看出來,這是一個ViewGroup包裝了一個ImageView和Seekbar。沒錯,控制元件確實是LinearLayout包裹著一個ImageView和Seekar,然而Android自帶的ImageView和Seekbar並不能實現效果,需要改造。如何改造後面再說。現在來看看整體控制元件的結構: 


      從圖看到,控制元件是由PictureVertifyView和TextSeekbar組合在一個Captcha裡面。也就是前面所說的一個LinearLayout包裹一個ImageView和Seekbar,並將這些形成一個小團體。其中團體的老大我就叫他Captcha,PictureVertifyView和TextSeekbar就是他的小跟班,社團跟客戶談業務只能由老大進行,老大將工作吩咐給跟班,跟班們根據自己的能力做自己的本職,PictureVertifyView就是做拼圖這塊的,TextSeekbar就是做滑動條這塊的,最終組合成一個能完成客戶需求的整體。 
      廢話不多說,開始寫乾貨。

實現

1. 編寫PictureVertifyView類

      PictureVertifyView就是效果圖上部分的拼圖塊,也是Captcha控制元件的核心。主要完成拼圖的繪製,邏輯。

1.1PictureVertifyView類的狀態

      首先PictureVertifyView有6種狀態(點選、滑動、鬆開手指、驗證成功、驗證失敗、初始化),這些狀態將影響PictureVertifyView的繪製。至於怎麼影響,後面將會說明。

    private static final int STATE_DOWN = 1;
    private static final int STATE_MOVE = 2;
    private static final int STATE_LOOSEN = 3;
    private static final int STATE_IDEL = 4;
    private static final int STATE_ACCESS = 5;
    private static final int STATE_UNACCESS = 6;
    private int mState = STATE_IDEL;

 //手指按下
 void down(int progress) {
        startTouchTime = System.currentTimeMillis();
        mState = STATE_DOWN;
        currentPosition = (int) (progress / 100f * (getWidth() -                     Utils.dp2px(getContext(), 50)));
        invalidate();
    }
   //手指滑動,改變缺塊圖片位置
    void move(int progress) {
        mState = STATE_MOVE;
        currentPosition = (int) (progress / 100f * (getWidth() - Utils.dp2px(getContext(), 50)));
        invalidate();
    }
   //手指鬆開,進行驗證
    void loose() {
        mState = STATE_LOOSEN;
        looseTime = System.currentTimeMillis();
        checkAccess();
        invalidate();
    }

  //復位到初始狀態,當復位的初始狀態,所有東西都會重置
    void reset() {
        mState = STATE_IDEL;
        verfityBlock.recycle();
        verfityBlock = null;
        info = null;
        blockShape = null;
        invalidate();
    }
   //驗證不通過
    void unAccess() {
        mState = STATE_UNACCESS;
        invalidate();
    }
   //驗證通過
    void access() {
        mState = STATE_ACCESS;
        invalidate();
    }

1.2拼圖缺塊位置資訊生成

      拼圖缺塊資訊既是拼圖缺塊的位置,這裡將封裝成一個實體類PositionINfo如下:

 public class PositionInfo {
        //缺塊在整張圖片的左上角x軸位置
        int left;
        //缺塊在整張圖片的左上角y軸位置
        int top;

        public PositionInfo(int left, int top) {
            this.left = left;
            this.top = top;
        }
    }

      對於驗證碼拼圖缺塊的位置是要在圖片範圍內隨機生成的。程式碼如下:

 @Override
    public PositionInfo getBlockPostionInfo(int width, int height) {
        Random random = new Random();
        int edge = Utils.dp2px(getContext(), 50);
        int left = random.nextInt(width - edge);
        //Avoid robot frequently and quickly click the start point to access the captcha.
        if (left < edge) {
            left = edge;
        }
        int top = random.nextInt(height - edge);
        if (top < 0) {
            top = 0;
        }
        return new PositionInfo(left, top);
    }

      這裡面對缺塊作了邊界限制,如下圖: 


      從圖上看到,圖片的左上角的點座標限制在裡面的矩形中,這樣做的目的是防止缺塊超出圖片邊界。看到這裡有人要疑惑,為了防止缺塊超出整張圖片範圍,限制右、下邊界可以理解,為什麼還要限制左邊界。原因很簡單,Captcha是基於按下和鬆開TextSeekbar來判斷是否要進行驗證。萬一機器人模擬快速點選滑動條的初始位置,而缺塊正好又生成在圖片最左邊,這樣不就不需要滑動就能通過驗證了嗎。雖然缺塊生成在圖片的最左邊機率很小,但是我還是拒絕這種歐氣的產生。

1.3缺塊的形狀

      缺塊的形狀就是一個Path類,如何編寫?這還用問嗎?你弄個圓也行,弄個三角形也行,這裡我就弄個不知道名字的形狀好了,程式碼如下:

    public Path getBlockShape(int blockSize) {
        int gap = Utils.dp2px(getContext(), blockSize/5f);
        Path path = new Path();
        path.moveTo(0, gap);
        path.rLineTo(Utils.dp2px(getContext(), blockSize/2.5f), 0);
        path.rLineTo(0, -gap);
        path.rLineTo(gap, 0);
        path.rLineTo(0, gap);
        path.rLineTo(2 * gap, 0);
        path.rLineTo(0, 4 * gap);
        path.rLineTo(-5 * gap, 0);
        path.rLineTo(0, -1.5f * gap);
        path.rLineTo(gap, 0);
        path.rLineTo(0, -gap);
        path.rLineTo(-gap, 0);
        path.close();
        return path;
    }
//不過注意了,你設計的Path的狂傲要限制在blockSize(缺塊大小)內。

1.4生成缺塊圖片

      上面介紹了缺塊的形狀,這裡我們講講如何生成缺塊圖片。缺塊圖片是要在原圖根據缺塊形狀裁的,至於怎麼裁,程式碼如下:

 private Bitmap createBlockBitmap() {
       //建立一張白紙
        Bitmap tempBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        //將這張白紙片放入畫布中
        Canvas canvas = new Canvas(tempBitmap);
        //由於PictureVertifyView是繼承ImageView,這裡我們直接拿到ImageView的Drawable圖片並限制其寬高以達到Drawable是和ImageView的寬高是一致的
        getDrawable().setBounds(0, 0, getWidth(), getHeight());
        //把畫布的可畫範圍限制在缺塊形狀中,當然這個blockShape已經根據缺塊位置進行偏移
        canvas.clipPath(blockShape);
        //畫畫
        getDrawable().draw(canvas);
        //先忽略下面這句好嗎
        mStrategy.decoreateSwipeBlockBitmap(canvas,blockShape);
        return cropBitmap(tempBitmap);
    }

      這時tempBitmap是這樣的:外矩形是透明的,內矩形就是缺塊的內容,而整張tempBitmap的大小跟ImageView的大小一樣,很多無用面積,而我們需要的是裡面那張。別急cropBitmp方法就是幫我們裁出裡面那部分。 


    private Bitmap cropBitmap(Bitmap bmp) {
        Bitmap result = null;
        int size = Utils.dp2px(getContext(), blockSize);
        //一句程式碼就裁出來了,就是這麼簡單
        result = Bitmap.createBitmap(bmp, info.left, info.top, size, size);
        bmp.recycle();
        return result;
    }

1.5 繪製

      在看繪製程式碼前,我們先來看看下面這張圖,看看怎麼繪製。 


      實際上繪製並不複雜,就是在ImageView的基礎上加上一個缺塊的陰影遮蓋原來圖片的一部分,再加上裁出來的缺塊圖片。由於本控制元件的滑動條是水平滑動的,所以缺塊圖片和缺塊陰影在同一水平線上,只是水平位置不同。驗證是否通過是判斷currentPostion和left是否大概相等。怎麼個大概?我程式碼是寫10個畫素。 
      好了,貼程式碼:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪製前,我們要判斷是否有缺塊的位置資訊,缺塊形狀和圖片,沒有的話要先獲得它們的例項,其中mStrategy的作用,我們後面將會說到。
        if (info == null) {
            info = mStrategy.getBlockPostionInfo(getWidth(), getHeight());
        }
        if (blockShape == null) {
            blockShape = mStrategy.getBlockShape(blockSize);
            blockShape.offset(info.left, info.top);
        }
        if (verfityBlock == null) {
            verfityBlock = createBlockBitmap();
        }
        //上文已經說過,狀態會影響繪製內容,其中當狀態為非驗證成功時,會繪製陰影,當狀態為滑動或初始時繪製滑動的缺塊圖片(寫程式碼最怕為改名字,有些名字很尷尬,請多多包涵)
        if (mState != STATE_ACCESS) {
            canvas.drawPath(blockShape, shadowPaint);
        }
        if (mState == STATE_MOVE || mState == STATE_IDEL) 
            canvas.drawBitmap(verfityBlock, currentPosition, info.top, bitmapPaint);
        }

    }

1.6 驗證演算法

       前面提到,當鬆開手指,缺塊圖片和缺塊陰影重合的時候就是驗證通過,否則失敗,其程式碼很簡單:

private void checkAccess() {
//判斷currentPostion(滑塊水平位置)和info.left(陰影水平位置)是否在容差以內
        if (Math.abs(currentPosition - info.left) < TOLERANCE) {
            access();
            //listener監聽器用於監聽驗證成功失敗事件
            if (listener != null) {
            //小操作,用於記錄使用者驗證所需要的時間,詳細看工程程式碼
                long deltaTime = looseTime - startTouchTime;
                listener.onAccess(deltaTime);
            }
        } else {
            unAccess();
            if (listener != null) {
                listener.onFailed();
            }
        }
    }

2.編寫TextSeekbar

      TextSeekbar就是效果圖下面那個滑動條,它相當於團隊里老板的市場調研,觀察客戶的喜好再告訴給老闆,老闆再叫PictureVertifyView做什麼。TextSeekbar繼承於Seekbar,功能很簡單,就是在繪製完Seekbar的時候多繪製一行字,程式碼如下:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("向右滑動滑塊完成拼圖", getWidth() / 2, getHeight() / 2 + textPaint.getTextSize() / 2 - 4, textPaint);
    }
//注意,這個地方有點偷工減料了,文字垂直方向的偏移並沒有準確計算,只是在手機上除錯視覺上就是差不多這樣,這裡深感抱歉。

3.編寫Captcha老大

      前文說過,Captcha就是團隊老大,外面客戶談業務只能跟它談,再將工作分配給小弟。Captcha就相當於團隊的門面,其實門面嘛是可有可無的。有了PictureVertifyView和TextSeekbar就可以進行業務工作,但是國不能一日無君,社團不能一日無老大。老大是對內統籌下屬工作,對外是外界與團隊進行溝通的物件。 
      Captcha其實就是一個LinearLayout包裹PictureVertifyView和TextSeekbar的組合控制元件,當然也可以是FrameLayout等。 
      Captcha的編寫與編寫組合控制元件的步驟一樣,編寫xml佈局檔案,編寫控制邏輯。

3.1xml佈局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/container_backgroud"
    android:padding="10dp"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:layout_height="200dp">

        <com.luozm.captcha.PictureVertifyView
            android:id="@+id/vertifyView"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:scaleType="fitXY" />

        <LinearLayout
            android:visibility="gone"
            android:id="@+id/accessRight"
            android:background="#7F000000"
            android:orientation="horizontal"
            android:layout_gravity="bottom"
            android:layout_width="match_parent"
            android:layout_height="28dp">

            <ImageView
                android:src="@drawable/right"
                android:layout_marginLeft="10dp"
                android:layout_width="20dp"
                android:layout_gravity="center"
                android:layout_height="20dp" />

            <TextView
                android:id="@+id/accessText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginLeft="10dp"
                android:textColor="#FFFFFF"
                android:text="驗證通過,耗時1000毫秒"
                android:textSize="14sp"/>

        </LinearLayout>

        <LinearLayout
            android:id="@+id/accessFailed"
            android:background="#7F000000"
            android:orientation="horizontal"
            android:visibility="gone"
            android:layout_gravity="bottom"
            android:layout_width="match_parent"
            android:layout_height="28dp">

            <ImageView
                android:src="@drawable/wrong"
                android:layout_marginLeft="10dp"
                android:layout_width="20dp"
                android:layout_gravity="center"
                android:layout_height="20dp" />

            <TextView
                android:id="@+id/accessFailedText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginLeft="10dp"
                android:textColor="#FFFFFF"
                android:textSize="14sp"/>

        </LinearLayout>


    </FrameLayout>


    <com.luozm.captcha.TextSeekbar
        android:id="@+id/seekbar"
        android:layout_gravity="center"
        style="@style/MySeekbarSytle"
        android:splitTrack="false"
        android:thumbOffset="0dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp" />

</LinearLayout>

      佈局很簡單,除了前文介紹的TextSeekbar和PictureVertifyView外還有一個覆蓋在PictureVertifyView底部的用於顯示驗證資訊的佈局。

3.2控制邏輯

      Captcha的作用就是統籌子控制元件的邏輯,相當於一個Activity介面統籌各個控制元件工作完成一個功能。在這裡,Captcha主要是通過TextSeekbar的拖動事件觸發PictureVertifyView狀態的改變。其程式碼如下:

seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (isDown) {
                    isDown = false;
                    if (progress > 10) {
                        isResponse = false;
                    } else {
                        isResponse = true;
                        accessFailed.setVisibility(GONE);
                        vertifyView.down(0);
                    }
                }
                if (isResponse) {
                    vertifyView.move(progress);
                } else {
                    seekBar.setProgress(0);
                    isResponse = false;
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                isDown = true;
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                if (isResponse) {
                    vertifyView.loose();
                }
            }
        });

      由於Android自帶的Seekbar的progress值是隨使用者手指的位置改變,即使手指並非按下滑動塊也能改變其值。因此,上面程式碼作了使用者按下滑動塊才能拖動,避免progress值的瞬變。

4.策略模式

      剛寫這個控制元件的時候,只是為了完成專案中登入驗證的功能,PictureVertifyView的拼圖形狀,拼圖效果都是寫死的。而為了讓控制元件的使用者可以自行定製想要的拼圖效果,採用策略模式對控制元件進行大改造,將定義拼圖效果的方法移至策略類當中。其類圖大概如下圖: 
 
      讀者在前文中應該留意到mStrategy這個引用。這就是PictureVertifyView通過引用CaptchaStrategy的一個實現類從而確定自己要用到的拼圖效果。CaptchaStrategy的程式碼如下:

public abstract class CaptchaStrategy {

    protected Context mContext;

    public CaptchaStrategy(Context ctx) {
        this.mContext = ctx;
    }

    protected Context getContext() {
        return mContext;
    }

    /**
     * 定義缺塊的形狀
     *
     * @param blockSize 單位dp,注意轉化為px
     * @return path of the shape
     */
    public abstract Path getBlockShape(int blockSize);

    /**
     * 定義缺塊的位置資訊生成演算法
     *
     * @param width  picture width
     * @param height picture height
     * @return position info of the block
     */
    public abstract PositionInfo getBlockPostionInfo(int width, int height);

    /**
     * 獲得缺塊陰影的Paint
     */
    public abstract Paint getBlockShadowPaint();

    /**
     * 獲得滑塊圖片的Paint
     */
    public abstract Paint getBlockBitmapPaint();

    /**
     * 裝飾滑塊圖片,在繪製圖片後執行,即繪製滑塊前景
     */
    public void decoreateSwipeBlockBitmap(Canvas canvas,Path shape) {

    }
}

      它的預設實現類為DefaultCaptchaStrategy,程式碼如下:

public class DefaultCaptchaStrategy extends CaptchaStrategy {

    public DefaultCaptchaStrategy(Context ctx) {
        super(ctx);
    }

    @Override
    public Path getBlockShape(int blockSize) {
        int gap = Utils.dp2px(getContext(), blockSize/5f);
        Path path = new Path();
        path.moveTo(0, gap);
        path.rLineTo(Utils.dp2px(getContext(), blockSize/2.5f), 0);
        path.rLineTo(0, -gap);
        path.rLineTo(gap, 0);
        path.rLineTo(0, gap);
        path.rLineTo(2 * gap, 0);
        path.rLineTo(0, 4 * gap);
        path.rLineTo(-5 * gap, 0);
        path.rLineTo(0, -1.5f * gap);
        path.rLineTo(gap, 0);
        path.rLineTo(0, -gap);
        path.rLineTo(-gap, 0);
        path.close();
        return path;
    }

    @Override
    public PictureVertifyView.PositionInfo getBlockPostionInfo(int width, int height) {
        Random random = new Random();
        int edge = Utils.dp2px(getContext(), 50);
        int left = random.nextInt(width - edge);
        //Avoid robot frequently and quickly click the start point to access the captcha.
        if (left < edge) {
            left = edge;
        }
        int top = random.nextInt(height - edge);
        if (top < 0) {
            top = 0;
        }
        return new PositionInfo(left, top);
    }

    @Override
    public Paint getBlockShadowPaint() {
        Paint shadowPaint = new Paint();
        shadowPaint.setColor(Color.parseColor("#000000"));
        shadowPaint.setAlpha(165);
        return shadowPaint;
    }

    @Override
    public Paint getBlockBitmapPaint() {
        Paint paint = new Paint();
        return paint;
    }


    @Override
    public void decoreateSwipeBlockBitmap(Canvas canvas, Path shape) {
        Paint paint = new Paint();
        paint.setColor(Color.parseColor("#FFFFFF"));
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
        paint.setPathEffect(new DashPathEffect(new float[]{20,20},10));
        Path path = new Path(shape);
        canvas.drawPath(path,paint);
    }
}

總結

      通過拼圖滑塊驗證碼控制元件的編寫,瞭解到自定義控制元件的一些技巧和步驟。此外還能免費在專案中用到拼圖驗證碼(因為市面上網易的雲盾驗證碼,極驗都是付費的)。 
      最後,給伸手黨一個福利,控制元件已上傳到jcenter。使用者可移至gayhub檢視使用。 
Captcha Github
--------------------- 
作者:LawCoder 
來源:CSDN 
原文:https://blog.csdn.net/sdfsdfdfa/article/details/79120665 
版權宣告:本文為博主原創文章,轉載請附上博文連結!