作者簡(jiǎn)介 原創(chuàng)微信公眾號(hào)郭霖 WeChat ID: guolin_blog
本篇來(lái)自 老老司機(jī)(第六篇了)張旭童的投稿。一直給大家?guī)?lái)實(shí)戰(zhàn)文章的他,今天也毫無(wú)例外,分享了如何實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼功能。希望能幫助有需要的朋友。
張旭童的博客地址:
http://blog.csdn.net/zxt0601
概述
上周一總監(jiān)讓我研究一波滑動(dòng)驗(yàn)證碼,說(shuō)項(xiàng)目可能會(huì)上。我想了一下好像在斗魚(yú)、淘寶都見(jiàn)過(guò),結(jié)果下了這兩個(gè)app,發(fā)現(xiàn)怎么點(diǎn)也出不來(lái)滑動(dòng)驗(yàn)證碼。于是,我就去web端斗魚(yú)看了一下,果然,每次登陸都會(huì)出現(xiàn)驗(yàn)證碼。
好吧,那我們這次的目標(biāo)就定為?在?Android端 app上,自定義View,仿一個(gè)web端滑動(dòng)驗(yàn)證碼吧。
(后話,做到后面發(fā)現(xiàn)我有點(diǎn)蠢了,我應(yīng)該直接模仿app端的,很多效果在web端應(yīng)該很好實(shí)現(xiàn) ,但是在Android端就不那么好整了。,例如驗(yàn)證成功的白光掃過(guò)動(dòng)畫,如下圖。在Android上實(shí)現(xiàn)起來(lái)就不太容易,有些效果還是不如web端酷炫。)
(圖很渣,也忽略底下的SeekBar,這不是重點(diǎn))
一些動(dòng)畫,效果錄不出來(lái)了,大家可以去斗魚(yú)web端看一下,然后下載Demo看一下,效果還是可以的。
我們的Demo和web端基本上一樣。那么本控件包含不僅包含以下功能:
隨機(jī)區(qū)域起點(diǎn)(左上角x,y)生成一個(gè)驗(yàn)證碼陰影。
驗(yàn)證碼拼圖?凹凸圖形會(huì)隨機(jī)變換。
驗(yàn)證碼區(qū)域?qū)捀呖勺远x。
摳圖驗(yàn)證碼區(qū)域,繪制一個(gè)用于聯(lián)動(dòng)滑動(dòng)的驗(yàn)證碼滑塊。
驗(yàn)證失敗,會(huì)閃爍幾下然后回到原點(diǎn)。
驗(yàn)證成功,會(huì)有白光掃過(guò)的動(dòng)畫。
分解一下驗(yàn)證碼核心實(shí)現(xiàn)思路:
控件繼承自 ImageView。理由:
1. 如果放在項(xiàng)目中用,驗(yàn)證碼圖片希望可以是接口返回。ImageView 以及其子類支持花式加載圖片。
2. 繼承自 ImageView,繪制圖片本身不用我們干預(yù),也不用我們操心 scaleType,節(jié)省很多工作。
在?onSizeChanged()?方法中生成 和 控件寬高相關(guān)的屬性值:
1. 初始化時(shí)隨機(jī)生成驗(yàn)證碼區(qū)域起點(diǎn)
2. 生成驗(yàn)證碼區(qū)域Path
3. 生成滑塊Bitmap
onDraw()?時(shí),依次繪制:
1. 驗(yàn)證碼陰影
2. 滑塊
核心工作是以上,可是實(shí)現(xiàn)起來(lái)還是有很多坑的,下面一步一步來(lái)吧。
驗(yàn)證碼區(qū)域的生成
這里我省略自定義View的幾個(gè)基礎(chǔ)步驟:
在 attrs.xml 定義屬性
在View的構(gòu)造函數(shù)里獲取 attrs 屬性
一些 Paint,Path 的初始化工作
首先思考,驗(yàn)證碼區(qū)域包含:
繪制在圖片上的驗(yàn)證碼陰影
可移動(dòng)的驗(yàn)證碼滑塊
生成驗(yàn)證碼陰影
我們用Path存儲(chǔ)驗(yàn)證碼區(qū)域,所以這一步最重要是生成驗(yàn)證碼區(qū)域的Path。查看競(jìng)品(斗魚(yú)web端)如下:
so,我們這里要繪制一個(gè)矩形+四邊可能會(huì)有隨機(jī)的凹凸,凹凸可以用半圓來(lái)替代。我們?nèi)缦戮帉懀?/p>
代碼配有注釋,gap 是指凹凸的起點(diǎn)和頂點(diǎn)的距離。
關(guān)于drawPartCircle(),它的功能是傳入起點(diǎn)、終點(diǎn)坐標(biāo),以及需要凹還是凸,和繪制的Path。它會(huì)在Path上繪制一個(gè)凹、凸的半圓。代碼如下:
這里用的是推導(dǎo)之后的公式,沒(méi)推導(dǎo)前的也在注釋里。
簡(jiǎn)單說(shuō),先計(jì)算出中點(diǎn)和半徑,利用三次貝塞爾曲線繪制一個(gè)圓(c和gap1都是和三次貝塞爾曲線相關(guān))。關(guān)于三次貝塞爾曲線就不展開(kāi)了,網(wǎng)上很多資料,我也是現(xiàn)學(xué)的。
這里關(guān)于繪制驗(yàn)證碼陰影 Path,還有一段曲折心路歷程,繪制出來(lái)的效果如下:
心路歷程(可以不看):
驗(yàn)證碼 Path,猛的一看,似乎很簡(jiǎn)單,不就是 一個(gè)矩形 +上 四個(gè)邊可能出現(xiàn)的凹凸嘛。
凹凸的話,我們就是繪制一個(gè)半圓好了。利用Path的lineTo()+addCircle()似乎可以很輕松的實(shí)現(xiàn)?
最開(kāi)始我是這么做的,結(jié)果發(fā)現(xiàn)畫出來(lái)的 Path 是 多段的Path,閉合后,無(wú)法形成一個(gè)完整陰影區(qū)域。更無(wú)法用于下一步驗(yàn)證碼滑塊bitmap的生成。
好,看來(lái)是addCircle()的鍋,導(dǎo)致了 Path 被分割成多段。那我用arcTo()好了,結(jié)果發(fā)現(xiàn)arcTo()不像addCircle()那樣可以設(shè)置繪圖的方向(順時(shí)針,逆時(shí)針),這當(dāng)時(shí)可把我難住了,因?yàn)椴荒苣鏁r(shí)針的話,上、右邊的凹就畫不出來(lái)。所以我放棄了,我轉(zhuǎn)用貝塞爾曲線繪制這個(gè)凹凸。
文章寫到這里,我突然發(fā)現(xiàn)自己智障了,sweepAngle傳入負(fù)值不就可以逆時(shí)針了嗎。如:arcTo(oval, 180, -180);
所以說(shuō)寫博客是有很大好處的,寫博客時(shí)大腦也是高速旋轉(zhuǎn),因?yàn)樯聦懗鲥e(cuò)誤,一是誤導(dǎo)別人,二是丟人。大腦高速運(yùn)轉(zhuǎn)說(shuō)不定就想通了以前想不通的問(wèn)題。
于是我就腦殘的用sin+二階貝爾賽曲線去繪制這個(gè)半圓了,為什么用它們呢?因?yàn)楫?dāng)初我繪制波浪滾動(dòng)的時(shí)候用的 sin函數(shù)+二階貝塞爾 模擬波浪,于是我就慣性思維的也這么解決了。結(jié)果呢?繪制出來(lái)的凹凸不夠圓啊,sin函數(shù)還是比不過(guò)圓是不是。于是我就走上了用三階貝塞爾曲線模擬圓的路。
看來(lái)我當(dāng)初寫這一塊代碼的時(shí)候,腦子確實(shí)不太清醒,不過(guò)也有收獲。又復(fù)習(xí)了一遍 Path 的幾個(gè)函數(shù)和貝塞爾曲線。
摳圖:驗(yàn)證碼滑塊的生成
驗(yàn)證碼 Path 生成好了后,我要根據(jù) Path 去生成驗(yàn)證碼滑塊。那么第一步就是要摳圖了。代碼如下:
其實(shí)這里我也走了一些曲折的路,我先是用canvas.clipPath(path)摳的圖,結(jié)果發(fā)現(xiàn)有鋸齒,搜了很多資料也沒(méi)搞定。于是我又回到了Xfermode的路上,將其設(shè)置為mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
先繪制 dst,即遮罩驗(yàn)證碼 Path,然后再繪制 src:Bitmap,取交集即可完成摳圖。這里有一些需要注意的地方:
src 的 Bitmap 是 取ImageView本身的bitmap。
創(chuàng)建的新Bitmap的寬高取控件的寬高。
它們兩者的寬高很大可能是不同的,這就是ImageView參數(shù) scaleType 的作用。所以我們取出 ImageView 的 Matrix?用于繪制 src 的 Bitmap。這樣摳出來(lái)的Bitmap區(qū)域就和第1步遮蓋住的區(qū)域是一樣的了。
mMaskShadowBitmap = mMaskBitmap.extractAlpha();這句話是為了在繪制出的滑塊周圍也繪制一圈陰影,加強(qiáng)立體效果。仔細(xì)看下圖效果,周邊又一圈立體陰影的效果:
繪制
onDraw()方法其實(shí)比較簡(jiǎn)單,只不過(guò)在其中加入了一些布爾類型的flag,都是和動(dòng)畫相關(guān)的,代碼如下:
mPaint如下定義: 所以繪制出陰影也有一些陰影效果。

值得說(shuō)的就是,配合滑塊滑動(dòng),是利用mDragerOffset,默認(rèn)是0,滑動(dòng)時(shí)mDragerOffset增加,滑塊右移,反之亦然。
驗(yàn)證成功的白光掃過(guò)動(dòng)畫,是利用canvas.translate()做的,mSuccessPath和mSuccessPaint如下:
滑動(dòng)、驗(yàn)證、動(dòng)畫
上一節(jié)完成后,我們的滑動(dòng)驗(yàn)證碼View已經(jīng)可以正常繪制出來(lái)了,現(xiàn)在我們?yōu)樗黾右恍┓椒?,讓它可以?lián)動(dòng)滑動(dòng)、驗(yàn)證功能和動(dòng)畫。
上一節(jié)也提到,滑動(dòng)主要是改變mDragerOffset的值,然后重繪自己->ondraw(),根據(jù)mDragerOffset偏移滑塊Bitmap的繪制。
校驗(yàn)
校驗(yàn)的話,需要引入一個(gè)回調(diào)接口:
成功、失敗的回調(diào)是在動(dòng)畫結(jié)束時(shí)通知的。
動(dòng)畫
動(dòng)畫里要用到寬高,所以它是在onSizeChanged()方法里被調(diào)用的。
代碼很簡(jiǎn)單,修改的一些 布爾值 flag,在?onDraw()?方法里會(huì)用到,結(jié)合?onDraw()?一看便懂。
Demo
這一節(jié),我們聯(lián)動(dòng)SeekBar滑動(dòng)起來(lái)。xml如下:
UI就是文首那張圖的樣子,完整Activity代碼:
總結(jié)
代碼傳送門,喜歡的話,隨手點(diǎn)個(gè)star:
https://github.com/mcxtzhang/SwipeCaptcha
包含完整的Demo源碼和SwipeCaptchaView。
利用一些工具發(fā)現(xiàn)web端斗魚(yú),驗(yàn)證碼圖片和滑塊圖片都是接口返回的。推測(cè)前端其實(shí)只返回后臺(tái):用戶移動(dòng)的距離或者距離的百分比。
本例完全由前端實(shí)現(xiàn)驗(yàn)證碼生成、驗(yàn)證功能,是因?yàn)椋?/p>
1.練習(xí)自定義VIew,自己全部實(shí)現(xiàn)摳圖 驗(yàn)證 繪制,感覺(jué)很酷。
2.我不會(huì)做后臺(tái),手動(dòng)微笑。
核心點(diǎn):
1.不規(guī)則圖形Path的生成。
2.指定Path對(duì)Bitmap摳圖,抗鋸齒。
3.適配ImageView的ScaleType。
4.成功、失敗的動(dòng)畫
完。。。。。。。。。。。。。。。。。。。。。
文章原創(chuàng)作者GuoLin 書籍推薦
郭林大神原創(chuàng)android 書籍:《第一行代碼 android》
