【Android】仿斗魚滑動拼圖驗證碼控件

本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨家發(fā)布
轉(zhuǎn)載請標明出處: http://www.reibang.com/p/9bf982da6e96
本文出自:【張旭童的簡書】 (http://www.reibang.com/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話捂敌,隨手點個star窖逗。多謝
https://github.com/mcxtzhang/SwipeCaptcha

概述

本篇滑動驗證碼的代碼其實上周四就寫好了,結(jié)果周末趕上找房子喷兼,搬家菇绵,累掉了半條命肄渗,趕緊寫篇博客恢復(fù)恢復(fù)元氣。

另外上次簡書莫名其妙封我號咬最,也不給我一個說法翎嫡,當天又解封了。這個事我也挺不爽的丹诀,吐個槽

上周一總監(jiān)讓我研究一波滑動驗證碼钝的,說項目可能會上。我想了一下好像在斗魚铆遭、淘寶都見過硝桩,結(jié)果下了這兩個app,發(fā)現(xiàn)怎么點也出不來滑動驗證碼枚荣。于是碗脊,我就去web端斗魚看了一下,果然橄妆,每次登陸都會出現(xiàn)驗證碼衙伶。
好吧,那我們這次的目標就定為 在 Android端app上害碾,自定義View矢劲,仿一個web端滑動驗證碼吧
(后話慌随,做到后面發(fā)現(xiàn)我有點蠢了芬沉,我應(yīng)該直接模仿app端的躺同,很多效果在web端應(yīng)該很好實現(xiàn) ,但是在Android端就不那么好整了丸逸。蹋艺,例如驗證成功的白光掃過動畫,如下圖黄刚。在Android上實現(xiàn)起來就不太容易捎谨,有些效果還是不如web端酷炫。)

斗魚web端效果

我們的Demo,Ac娘鎮(zhèn)樓

(圖很渣憔维,也忽略底下的SeekBar涛救,這不是重點)
一些動畫,效果錄不出來了埋同,大家可以去斗魚web端看一下州叠,然后下載Demo看一下,效果還是可以的凶赁。
代碼 傳送門:
https://github.com/mcxtzhang/SwipeCaptcha

我們的Demo和web端基本上一樣咧栗。

那么本控件包含不僅包含以下功能:

  • 隨機區(qū)域起點(左上角x,y)生成一個驗證碼陰影。
  • 驗證碼拼圖 凹凸圖形會隨機變換虱肄。
  • 驗證碼區(qū)域寬高可自定義致板。
  • 摳圖驗證碼區(qū)域,繪制一個用于聯(lián)動滑動的驗證碼滑塊咏窿。
  • 驗證失敗斟或,會閃爍幾下然后回到原點。
  • 驗證成功集嵌,會有白光掃過的動畫萝挤。

分解一下驗證碼核心實現(xiàn)思路:

  • 控件繼承自ImageView。理由:
    1 如果放在項目中用根欧,驗證碼圖片希望可以是接口返回怜珍。ImageView以及其子類支持花式加載圖片。
    2 繼承自ImageView凤粗,繪制圖片本身不用我們干預(yù)酥泛,也不用我們操心scaleType,節(jié)省很多工作嫌拣。
  • onSizeChanged()方法中生成 和 控件寬高相關(guān)的屬性值:
    1 初始化時隨機生成驗證碼區(qū)域起點
    2 生成驗證碼區(qū)域Path
    3 生成滑塊Bitmap
  • onDraw()時柔袁,依次繪制:
    1 驗證碼陰影
    2 滑塊

核心工作是以上,可是實現(xiàn)起來還是有很多坑的异逐,下面一步一步來吧捶索。


驗證碼區(qū)域的生成

這里我省略自定義View的幾個基礎(chǔ)步驟:

  • 在attrs.xml定義屬性
  • 在View的構(gòu)造函數(shù)里獲取attrs屬性
  • 一些Paint,Path的初始化工作

完整代碼在
https://github.com/mcxtzhang/SwipeCaptcha
可以下載后對照閱讀灰瞻,效果更佳情组。

首先思考燥筷,驗證碼區(qū)域包含:

  • 繪制在圖片上的驗證碼陰影
  • 可移動的驗證碼滑塊

1 生成驗證碼陰影

我們用Path存儲驗證碼區(qū)域,
所以這一步最重要是生成驗證碼區(qū)域的Path院崇。
查看競品(斗魚web端)如下,


斗魚驗證碼原型.png

so袍祖,我們這里要繪制一個矩形+四邊可能會有隨機的凹凸底瓣,凹凸可以用半圓來替代。
我們?nèi)缦戮帉懀?br> 代碼配有注釋蕉陋,gap是指凹凸的起點和頂點的距離捐凭。

    //生成驗證碼Path
    private void createCaptchaPath() {
        //原本打算隨機生成gap,后來發(fā)現(xiàn) 寬度/3 效果比較好凳鬓,
        int gap = mRandom.nextInt(mCaptchaWidth / 2);
        gap = mCaptchaWidth / 3;

        //隨機生成驗證碼陰影左上角 x y 點茁肠,
        mCaptchaX = mRandom.nextInt(mWidth - mCaptchaWidth - gap);
        mCaptchaY = mRandom.nextInt(mHeight - mCaptchaHeight - gap);

        mCaptchaPath.reset();
        mCaptchaPath.lineTo(0, 0);

        //從左上角開始 繪制一個不規(guī)則的陰影
        mCaptchaPath.moveTo(mCaptchaX, mCaptchaY);//左上角
        mCaptchaPath.lineTo(mCaptchaX + gap, mCaptchaY);
        //draw一個隨機凹凸的圓
        drawPartCircle(new PointF(mCaptchaX + gap, mCaptchaY),
                new PointF(mCaptchaX + gap * 2, mCaptchaY),
                mCaptchaPath, mRandom.nextBoolean());


        mCaptchaPath.lineTo(mCaptchaX + mCaptchaWidth, mCaptchaY);//右上角
        mCaptchaPath.lineTo(mCaptchaX + mCaptchaWidth, mCaptchaY + gap);
        //draw一個隨機凹凸的圓
        drawPartCircle(new PointF(mCaptchaX + mCaptchaWidth, mCaptchaY + gap),
                new PointF(mCaptchaX + mCaptchaWidth, mCaptchaY + gap * 2),
                mCaptchaPath, mRandom.nextBoolean());


        mCaptchaPath.lineTo(mCaptchaX + mCaptchaWidth, mCaptchaY + mCaptchaHeight);//右下角
        mCaptchaPath.lineTo(mCaptchaX + mCaptchaWidth - gap, mCaptchaY + mCaptchaHeight);
        //draw一個隨機凹凸的圓
        drawPartCircle(new PointF(mCaptchaX + mCaptchaWidth - gap, mCaptchaY + mCaptchaHeight),
                new PointF(mCaptchaX + mCaptchaWidth - gap * 2, mCaptchaY + mCaptchaHeight),
                mCaptchaPath, mRandom.nextBoolean());


        mCaptchaPath.lineTo(mCaptchaX, mCaptchaY + mCaptchaHeight);//左下角
        mCaptchaPath.lineTo(mCaptchaX, mCaptchaY + mCaptchaHeight - gap);
        //draw一個隨機凹凸的圓
        drawPartCircle(new PointF(mCaptchaX, mCaptchaY + mCaptchaHeight - gap),
                new PointF(mCaptchaX, mCaptchaY + mCaptchaHeight - gap * 2),
                mCaptchaPath, mRandom.nextBoolean());


        mCaptchaPath.close();
    }

關(guān)于drawPartCircle(),它的功能是傳入起點缩举、終點坐標垦梆,以及需要凹還是凸,和繪制的Path仅孩。它會在Path上繪制一個凹托猩、凸的半圓。
代碼如下:

 /**
     * 傳入起點辽慕、終點 坐標京腥、凹凸和Path。
     * 會自動繪制凹凸的半圓弧
     *
     * @param start 起點坐標
     * @param end   終點坐標
     * @param path  半圓會繪制在這個path上
     * @param outer 是否凸半圓
     */
    public static void drawPartCircle(PointF start, PointF end, Path path, boolean outer) {
        float c = 0.551915024494f;
        //中點
        PointF middle = new PointF(start.x + (end.x - start.x) / 2, start.y + (end.y - start.y) / 2);
        //半徑
        float r1 = (float) Math.sqrt(Math.pow((middle.x - start.x), 2) + Math.pow((middle.y - start.y), 2));
        //gap值
        float gap1 = r1 * c;

        if (start.x == end.x) {
            //繪制豎直方向的

            //是否是從上到下
            boolean topToBottom = end.y - start.y > 0 ? true : false;
            //以下是我寫出了所有的計算公式后推的溅蛉,不要問我過程公浪,只可意會。
            int flag;//旋轉(zhuǎn)系數(shù)
            if (topToBottom) {
                flag = 1;
            } else {
                flag = -1;
            }
            if (outer) {
                //凸的 兩個半圓
                path.cubicTo(start.x + gap1 * flag, start.y,
                        middle.x + r1 * flag, middle.y - gap1 * flag,
                        middle.x + r1 * flag, middle.y);
                path.cubicTo(middle.x + r1 * flag, middle.y + gap1 * flag,
                        end.x + gap1 * flag, end.y,
                        end.x, end.y);
            } else {
                //凹的 兩個半圓
                path.cubicTo(start.x - gap1 * flag, start.y,
                        middle.x - r1 * flag, middle.y - gap1 * flag,
                        middle.x - r1 * flag, middle.y);
                path.cubicTo(middle.x - r1 * flag, middle.y + gap1 * flag,
                        end.x - gap1 * flag, end.y,
                        end.x, end.y);
            }
        } else {
            //繪制水平方向的

            //是否是從左到右
            boolean leftToRight = end.x - start.x > 0 ? true : false;
            //以下是我寫出了所有的計算公式后推的船侧,不要問我過程欠气,只可意會。
            int flag;//旋轉(zhuǎn)系數(shù)
            if (leftToRight) {
                flag = 1;
            } else {
                flag = -1;
            }
            if (outer) {
                //凸 兩個半圓
                path.cubicTo(start.x, start.y - gap1 * flag,
                        middle.x - gap1 * flag, middle.y - r1 * flag,
                        middle.x, middle.y - r1 * flag);
                path.cubicTo(middle.x + gap1 * flag, middle.y - r1 * flag,
                        end.x, end.y - gap1 * flag,
                        end.x, end.y);
            } else {
                //凹 兩個半圓
                path.cubicTo(start.x, start.y + gap1 * flag,
                        middle.x - gap1 * flag, middle.y + r1 * flag,
                        middle.x, middle.y + r1 * flag);
                path.cubicTo(middle.x + gap1 * flag, middle.y + r1 * flag,
                        end.x, end.y + gap1 * flag,
                        end.x, end.y);
            }


/*
            沒推導(dǎo)之前的公式在這里
            if (start.x < end.x) {
                if (outer) {
                    //上左半圓 順時針
                    path.cubicTo(start.x, start.y - gap1,
                            middle.x - gap1, middle.y - r1,
                            middle.x, middle.y - r1);

                    //上右半圓:順時針
                    path.cubicTo(middle.x + gap1, middle.y - r1,
                            end.x, end.y - gap1,
                            end.x, end.y);
                } else {
                    //下左半圓 逆時針
                    path.cubicTo(start.x, start.y + gap1,
                            middle.x - gap1, middle.y + r1,
                            middle.x, middle.y + r1);

                    //下右半圓 逆時針
                    path.cubicTo(middle.x + gap1, middle.y + r1,
                            end.x, end.y + gap1,
                            end.x, end.y);
                }
            } else {
                if (outer) {
                    //下右半圓 順時針
                    path.cubicTo(start.x, start.y + gap1,
                            middle.x + gap1, middle.y + r1,
                            middle.x, middle.y + r1);
                    //下左半圓 順時針
                    path.cubicTo(middle.x - gap1, middle.y + r1,
                            end.x, end.y + gap1,
                            end.x, end.y);
                }
            }*/
        }
    }

這里用的是推導(dǎo)之后的公式勺爱,沒推導(dǎo)前的也在注釋里晃琳。
簡單說,先計算出中點和半徑琐鲁,利用三次貝塞爾曲線繪制一個圓(c和gap1 都是和三次貝塞爾曲線相關(guān))卫旱。關(guān)于三次貝塞爾曲線就不展開了,網(wǎng)上很多資料围段,我也是現(xiàn)學的顾翼。
這里關(guān)于繪制驗證碼陰影Path,還有一段曲折心路歷程奈泪,
繪制出來的效果如下:

左邊是滑塊适贸,右邊是陰影

)

心路歷程(可以不看)
驗證碼Path灸芳,猛的一看,似乎很簡單拜姿,不就是一個矩形+上四個邊可能出現(xiàn)的凹凸嘛烙样。
凹凸的話,我們就是繪制一個半圓好了蕊肥。
利用PathlineTo()+addCircle()似乎可以很輕松的實現(xiàn)谒获?
最開始我是這么做的,結(jié)果發(fā)現(xiàn)畫出來的Path是多段的Path壁却,閉合后批狱,無法形成一個完整陰影區(qū)域。更無法用于下一步驗證碼滑塊bitmap的生成展东。
好赔硫,看來是addCircle()的鍋,導(dǎo)致了Path被分割成多段盐肃。那我用arcTo()好了爪膊,結(jié)果發(fā)現(xiàn)arcTo不像addCircle()那樣可以設(shè)置繪圖的方向,(順時針恼蓬,逆時針)惊完,這當時可把我難住了,因為不能逆時針的話处硬,上小槐、右邊的凹就畫不出來。所以我放棄了荷辕,我轉(zhuǎn)用貝塞爾曲線繪制這個凹凸凿跳。
文章寫到這里,我突然發(fā)現(xiàn)自己智障了疮方,sweepAngle傳入負值不就可以逆時針了嗎控嗜。如:arcTo(oval, 180, -180);
所以說寫博客是有很大好處的,寫博客時大腦也是高速旋轉(zhuǎn)骡显,因為生怕寫出錯誤疆栏,一是誤導(dǎo)別人,二是丟人惫谤。大腦高速運轉(zhuǎn)說不定就想通了以前想不通的問題壁顶。
于是我就腦殘的用sin+二階貝爾賽曲線去繪制這個半圓了,為什么用它們呢溜歪?因為當初我繪制波浪滾動的時候用的sin函數(shù)+二階貝塞爾模擬波浪若专,于是我就慣性思維的也這么解決了。結(jié)果呢蝴猪?繪制出來的凹凸不夠圓啊调衰,sin函數(shù)還是比不過圓是不是膊爪。
于是我就走上了用三節(jié)貝塞爾曲線模擬圓的路。
看來我當初寫這一塊代碼的時候嚎莉,腦子確實不太清醒米酬,不過也有收獲。又復(fù)習了一遍Path的幾個函數(shù)和貝塞爾曲線趋箩。

2 摳圖:驗證碼滑塊的生成

驗證碼Path生成好了后淮逻,我要根據(jù)Path去生成驗證碼滑塊。那么第一步就是要摳圖了阁簸。
代碼如下:

    //生成滑塊
    private void craeteMask() {
        mMaskBitmap = getMaskBitmap(((BitmapDrawable) getDrawable()).getBitmap(), mCaptchaPath);
        //滑塊陰影
        mMaskShadowBitmap = mMaskBitmap.extractAlpha();
        //拖動的位移重置
        mDragerOffset = 0;
        //isDrawMask  繪制失敗閃爍動畫用
        isDrawMask = true;
    }
    //摳圖
    private Bitmap getMaskBitmap(Bitmap mBitmap, Path mask) {
        //以控件寬高 create一塊bitmap
        Bitmap tempBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        //把創(chuàng)建的bitmap作為畫板
        Canvas mCanvas = new Canvas(tempBitmap);
        //有鋸齒 且無法解決,所以換成XFermode的方法做
        //mCanvas.clipPath(mask);
        // 抗鋸齒
        mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
        //繪制用于遮罩的圓形
        mCanvas.drawPath(mask, mMaskPaint);
        //設(shè)置遮罩模式(圖像混合模式)
        mMaskPaint.setXfermode(mPorterDuffXfermode);
        //★考慮到scaleType等因素,要用Matrix對Bitmap進行縮放
        mCanvas.drawBitmap(mBitmap, getImageMatrix(), mMaskPaint);
        mMaskPaint.setXfermode(null);
        return tempBitmap;
    }

其實這里我也走了一些曲折的路哼丈,我先是用canvas.clipPath(path)摳的圖启妹,結(jié)果發(fā)現(xiàn)有鋸齒,搜了很多資料也沒搞定醉旦。于是我又回到了Xfermode的路上饶米,將其設(shè)置為mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
先繪制dst,即遮罩驗證碼Path车胡,然后再繪制src:Bitmap檬输,取交集即可完成摳圖。
這里有一些需要注意的地方:

  • src的Bitmap是取ImageView本身的bitmap匈棘。
  • 創(chuàng)建的新Bitmap的寬高取控件的寬高
  • 它們兩者的寬高很大可能是不同的丧慈,這就是ImageView參數(shù)scaleType的作用。所以我們取出ImageView的Matrix 用于繪制src的Bitmap主卫。這樣摳出來的Bitmap區(qū)域就和第1步遮蓋住的區(qū)域是一樣的了逃默。

mMaskShadowBitmap = mMaskBitmap.extractAlpha();這句話是為了在繪制出的滑塊周圍也繪制一圈陰影,加強立體效果簇搅。
仔細看下圖效果完域,周邊又一圈立體陰影的效果:

繪制

onDraw()方法其實比較簡單,只不過在其中加入了一些布爾類型的flag瘩将,都是和動畫相關(guān)的:
代碼如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繼承自ImageView吟税,所以Bitmap,ImageView已經(jīng)幫我們draw好了姿现。
        //我只在上面繪制和驗證碼相關(guān)的部分肠仪,

        //是否處于驗證模式系羞,在驗證成功后 為false豫柬,其余情況為true
        if (isMatchMode) {
            //首先繪制驗證碼陰影
            if (mCaptchaPath != null) {
                canvas.drawPath(mCaptchaPath, mPaint);
            }
            //繪制滑塊
            // isDrawMask  繪制失敗閃爍動畫用
            if (null != mMaskBitmap && null != mMaskShadowBitmap && isDrawMask) {
                // 先繪制陰影
                canvas.drawBitmap(mMaskShadowBitmap, -mCaptchaX + mDragerOffset, 0, mMaskShadowPaint);
                canvas.drawBitmap(mMaskBitmap, -mCaptchaX + mDragerOffset, 0, null);
            }
            //驗證成功磷醋,白光掃過的動畫庙曙,這一塊動畫感覺不完美蒸苇,有提高空間
            if (isShowSuccessAnim) {
                canvas.translate(mSuccessAnimOffset, 0);
                canvas.drawPath(mSuccessPath, mSuccessPaint);
            }
        }
    }

mPaint如下定義: 所以繪制出陰影也有一些陰影效果。

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setColor(0x77000000);
        //mPaint.setStyle(Paint.Style.STROKE);
        // 設(shè)置畫筆遮罩濾鏡
        mPaint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.SOLID));

值得說的就是缘厢,配合滑塊滑動台谍,是利用mDragerOffset,默認是0,滑動時mDragerOffset增加匹涮,滑塊右移天试,反之亦然。
驗證成功的白光掃過動畫然低,是利用canvas.translate()做的喜每,mSuccessPathmSuccessPaint如下:

        mSuccessPaint = new Paint();
        mSuccessPaint.setShader(new LinearGradient(0, 0, width, 0, new int[]{
                0x11ffffff, 0x88ffffff}, null,
                Shader.TileMode.MIRROR));
        //模仿斗魚 是一個平行四邊形滾動過去
        mSuccessPath = new Path();
        mSuccessPath.moveTo(0, 0);
        mSuccessPath.rLineTo(width, 0);
        mSuccessPath.rLineTo(width / 2, mHeight);
        mSuccessPath.rLineTo(-width, 0);
        mSuccessPath.close();

滑動、驗證雳攘、動畫

上一節(jié)完成后带兜,我們的滑動驗證碼View已經(jīng)可以正常繪制出來了,現(xiàn)在我們?yōu)樗黾右恍┓椒ǘ置穑屗梢月?lián)動滑動刚照、驗證功能和動畫。

聯(lián)動滑動:

上一節(jié)也提到喧兄,滑動主要是改變mDragerOffset的值无畔,然后重繪自己->ondraw(),根據(jù)mDragerOffset偏移滑塊Bitmap的繪制。

    /**
     * 重置驗證碼滑動距離,(一般用于驗證失敗)
     */
    public void resetCaptcha() {
        mDragerOffset = 0;
        invalidate();
    }

    /**
     * 最大可滑動值
     * @return
     */
    public int getMaxSwipeValue() {
        //return ((BitmapDrawable) getDrawable()).getBitmap().getWidth() - mCaptchaWidth;
        //返回控件寬度
        return mWidth - mCaptchaWidth;
    }

    /**
     * 設(shè)置當前滑動值
     * @param value
     */
    public void setCurrentSwipeValue(int value) {
        mDragerOffset = value;
        invalidate();
    }

校驗:

校驗的話吠冤,需要引入一個回調(diào)接口:


    public interface OnCaptchaMatchCallback {
        void matchSuccess(SwipeCaptchaView swipeCaptchaView);

        void matchFailed(SwipeCaptchaView swipeCaptchaView);
    }

    /**
     * 驗證碼驗證的回調(diào)
     */
    private OnCaptchaMatchCallback onCaptchaMatchCallback;

    public OnCaptchaMatchCallback getOnCaptchaMatchCallback() {
        return onCaptchaMatchCallback;
    }

    /**
     * 設(shè)置驗證碼驗證回調(diào)
     *
     * @param onCaptchaMatchCallback
     * @return
     */
    public SwipeCaptchaView setOnCaptchaMatchCallback(OnCaptchaMatchCallback onCaptchaMatchCallback) {
        this.onCaptchaMatchCallback = onCaptchaMatchCallback;
        return this;
    }
    /**
     * 校驗
     */
    public void matchCaptcha() {
        if (null != onCaptchaMatchCallback && isMatchMode) {
            //這里驗證邏輯浑彰,是通過比較,拖拽的距離 和 驗證碼起點x坐標拯辙。 默認3dp以內(nèi)算是驗證成功郭变。
            if (Math.abs(mDragerOffset - mCaptchaX) < mMatchDeviation) {
                //成功的動畫
                mSuccessAnim.start();
            } else {
                mFailAnim.start();
            }
        }

    }

成功、失敗的回調(diào)是在動畫結(jié)束時通知的薄风。

動畫:

動畫里要用到寬高饵较,所以它是在onSizeChanged()方法里被調(diào)用的。

//驗證動畫初始化區(qū)域
    private void createMatchAnim() {
        mFailAnim = ValueAnimator.ofFloat(0, 1);
        mFailAnim.setDuration(100)
                .setRepeatCount(4);
        mFailAnim.setRepeatMode(ValueAnimator.REVERSE);
        //失敗的時候先閃一閃動畫 斗魚是 隱藏-顯示 -隱藏 -顯示
        mFailAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                onCaptchaMatchCallback.matchFailed(SwipeCaptchaView.this);
            }
        });
        mFailAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatedValue = (float) animation.getAnimatedValue();
                if (animatedValue < 0.5f) {
                    isDrawMask = false;
                } else {
                    isDrawMask = true;
                }
                invalidate();
            }
        });

        int width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
        mSuccessAnim = ValueAnimator.ofInt(mWidth + width, 0);
        mSuccessAnim.setDuration(500);
        mSuccessAnim.setInterpolator(new FastOutLinearInInterpolator());
        mSuccessAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mSuccessAnimOffset = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        mSuccessAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                isShowSuccessAnim = true;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                onCaptchaMatchCallback.matchSuccess(SwipeCaptchaView.this);
                isShowSuccessAnim = false;
                isMatchMode = false;
            }
        });
        mSuccessPaint = new Paint();
        mSuccessPaint.setShader(new LinearGradient(0, 0, width, 0, new int[]{
                0x11ffffff, 0x88ffffff}, null,
                Shader.TileMode.MIRROR));
        //模仿斗魚 是一個平行四邊形滾動過去
        mSuccessPath = new Path();
        mSuccessPath.moveTo(0, 0);
        mSuccessPath.rLineTo(width, 0);
        mSuccessPath.rLineTo(width / 2, mHeight);
        mSuccessPath.rLineTo(-width, 0);
        mSuccessPath.close();
    }

代碼很簡單遭赂,修改的一些布爾值flag循诉,在onDraw()方法里會用到,結(jié)合onDraw()一看便懂撇他。


Demo

這一節(jié)茄猫,我們聯(lián)動SeekBar滑動起來。
xml如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    ......
>

    <com.mcxtzhang.captchalib.SwipeCaptchaView
        android:id="@+id/swipeCaptchaView"
        android:layout_width="300dp"
        android:layout_height="150dp"
        android:layout_centerHorizontal="true"
        android:scaleType="centerCrop"
        android:src="@drawable/pic11"
        app:captchaHeight="30dp"
        app:captchaWidth="30dp"/>

    <SeekBar
        android:id="@+id/dragBar"
        android:layout_width="320dp"
        android:layout_height="60dp"
        android:layout_below="@id/swipeCaptchaView"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="30dp"
        android:progressDrawable="@drawable/dragbg"
        android:thumb="@drawable/thumb_bg"/>

    <Button
        android:id="@+id/btnChange"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="老板換碼"/>
</RelativeLayout>

UI就是文首那張圖的樣子困肩,
完整Activity代碼:

public class MainActivity extends AppCompatActivity {
    SwipeCaptchaView mSwipeCaptchaView;
    SeekBar mSeekBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mSwipeCaptchaView = (SwipeCaptchaView) findViewById(R.id.swipeCaptchaView);
        mSeekBar = (SeekBar) findViewById(R.id.dragBar);
        findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mSwipeCaptchaView.createCaptcha();
                mSeekBar.setEnabled(true);
                mSeekBar.setProgress(0);
            }
        });
        mSwipeCaptchaView.setOnCaptchaMatchCallback(new SwipeCaptchaView.OnCaptchaMatchCallback() {
            @Override
            public void matchSuccess(SwipeCaptchaView swipeCaptchaView) {
                Toast.makeText(MainActivity.this, "恭喜你啊 驗證成功 可以搞事情了", Toast.LENGTH_SHORT).show();
                mSeekBar.setEnabled(false);
            }

            @Override
            public void matchFailed(SwipeCaptchaView swipeCaptchaView) {
                Toast.makeText(MainActivity.this, "你有80%的可能是機器人划纽,現(xiàn)在走還來得及", Toast.LENGTH_SHORT).show();
                swipeCaptchaView.resetCaptcha();
                mSeekBar.setProgress(0);
            }
        });
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {

            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                mSwipeCaptchaView.setCurrentSwipeValue(progress);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                //隨便放這里是因為控件
                mSeekBar.setMax(mSwipeCaptchaView.getMaxSwipeValue());
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                Log.d("zxt", "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
                mSwipeCaptchaView.matchCaptcha();
            }
        });
        
        //從網(wǎng)絡(luò)加載圖片也ok
        Glide.with(this)
                .load("http://www.investide.cn/data/edata/image/20151201/20151201180507_281.jpg")
                .asBitmap()
                .into(new SimpleTarget<Bitmap>() {
                    @Override
                    public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                        mSwipeCaptchaView.setImageBitmap(resource);
                        mSwipeCaptchaView.createCaptcha();
                    }
                });
    }
}

總結(jié)

代碼傳送門 喜歡的話,隨手點個star锌畸。多謝
https://github.com/mcxtzhang/SwipeCaptcha
包含完整Demo和SwipeCaptchaView勇劣。

利用一些工具發(fā)現(xiàn)web端斗魚,驗證碼圖片和滑塊圖片都是接口返回的。
推測前端其實只返回后臺:用戶移動的距離或者距離的百分比比默。

本例完全由前端實現(xiàn)驗證碼生成幻捏、驗證功能,是因為:
1 練習自定義VIew命咐,自己全部實現(xiàn)摳圖 驗證 繪制篡九,感覺很酷。
2 我不會做后臺醋奠,手動微笑榛臼。

核心點:
1 不規(guī)則圖形Path的生成。
2 指定Path對Bitmap摳圖窜司,抗鋸齒沛善。
3 適配ImageView的ScaleType。
4 成功塞祈、失敗的動畫

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末路呜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子织咧,更是在濱河造成了極大的恐慌,老刑警劉巖漠秋,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笙蒙,死亡現(xiàn)場離奇詭異,居然都是意外死亡庆锦,警方通過查閱死者的電腦和手機捅位,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搂抒,“玉大人艇搀,你說我怎么就攤上這事∏缶В” “怎么了焰雕?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長芳杏。 經(jīng)常有香客問我矩屁,道長,這世上最難降的妖魔是什么爵赵? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任吝秕,我火速辦了婚禮,結(jié)果婚禮上空幻,老公的妹妹穿的比我還像新娘烁峭。我一直安慰自己,他們只是感情好秕铛,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布约郁。 她就那樣靜靜地躺著缩挑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棍现。 梳的紋絲不亂的頭發(fā)上调煎,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機與錄音己肮,去河邊找鬼士袄。 笑死,一個胖子當著我的面吹牛谎僻,可吹牛的內(nèi)容都是我干的娄柳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼艘绍,長吁一口氣:“原來是場噩夢啊……” “哼赤拒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起诱鞠,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤挎挖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后航夺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蕉朵,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年阳掐,在試婚紗的時候發(fā)現(xiàn)自己被綠了始衅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡缭保,死狀恐怖汛闸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艺骂,我是刑警寧澤诸老,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站钳恕,受9級特大地震影響孕锄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苞尝,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一畸肆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宙址,春花似錦轴脐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恬涧。三九已至,卻和暖如春碴巾,著一層夾襖步出監(jiān)牢的瞬間溯捆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工厦瓢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留提揍,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓煮仇,卻偏偏與公主長得像劳跃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子浙垫,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 作者簡介 原創(chuàng)微信公眾號郭霖 WeChat ID: guolin_blog 本篇來自 老老司機(第六篇了)張旭童的...
    木木00閱讀 3,120評論 0 15
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,283評論 25 707
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點贊按鈕進度條TabLayout圖標下拉刷新...
    皇小弟閱讀 46,791評論 22 665
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫刨仑、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,119評論 4 61
  • 下了鋼琴課夹姥,這一天的學習任務(wù)就算結(jié)束了杉武。 無事心自輕,更添閑情辙售。和兒子兩人繞道北山公園艺智,那里濃郁的綠色、大片的陰涼...
    鉛筆芒種閱讀 344評論 0 1