Android 高仿斗魚滑動(dòng)驗(yàn)證碼
如下圖。在Android上實(shí)現(xiàn)起來(lái)就不太容易,有些效果還是不如web端酷炫。)


我們的Demo,Ac娘鎮(zhèn)樓
(圖很渣,也忽略底下的SeekBar,這不是重點(diǎn))
一些動(dòng)畫,效果錄不出來(lái)了,大家可以去斗魚web端看一下,然后下載Demo看一下,效果還是可以的。
代碼 傳送門:
https://github.com/mcxtzhang/SwipeCaptcha
我們的Demo和web端基本上一樣。
那么本控件包含不僅包含以下功能:
隨機(jī)區(qū)域起點(diǎn)(左上角x,y)生成一個(gè)驗(yàn)證碼陰影。驗(yàn)證碼拼圖 凹凸圖形會(huì)隨機(jī)變換。驗(yàn)證碼區(qū)域寬高可自定義。摳圖驗(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 生成滑塊BitmaponDraw()時(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的初始化工作
完整代碼在
https://github.com/mcxtzhang/SwipeCaptcha
可以下載后對(duì)照閱讀,效果更佳。
首先思考,驗(yàn)證碼區(qū)域包含:
繪制在圖片上的驗(yàn)證碼陰影
可移動(dòng)的驗(yàn)證碼滑塊
1 生成驗(yàn)證碼陰影
我們用Path存儲(chǔ)驗(yàn)證碼區(qū)域,
所以這一步最重要是生成驗(yàn)證碼區(qū)域的Path。
查看競(jìng)品(斗魚web端)如下,

so,我們這里要繪制一個(gè)矩形+四邊可能會(huì)有隨機(jī)的凹凸,凹凸可以用半圓來(lái)替代。
我們?nèi)缦戮帉懀?br />
代碼配有注釋,gap是指凹凸的起點(diǎn)和頂點(diǎn)的距離。
//生成驗(yàn)證碼Path
private void createCaptchaPath() {
//原本打算隨機(jī)生成gap,后來(lái)發(fā)現(xiàn) 寬度/3 效果比較好,
int gap = mRandom.nextInt(mCaptchaWidth / 2);
gap = mCaptchaWidth / 3;
//隨機(jī)生成驗(yàn)證碼陰影左上角 x y 點(diǎn),
mCaptchaX = mRandom.nextInt(mWidth - mCaptchaWidth - gap);
mCaptchaY = mRandom.nextInt(mHeight - mCaptchaHeight - gap);
mCaptchaPath.reset();
mCaptchaPath.lineTo(0, 0);
//從左上角開(kāi)始 繪制一個(gè)不規(guī)則的陰影
mCaptchaPath.moveTo(mCaptchaX, mCaptchaY);//左上角
mCaptchaPath.lineTo(mCaptchaX + gap, mCaptchaY);
//draw一個(gè)隨機(jī)凹凸的圓
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一個(gè)隨機(jī)凹凸的圓
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一個(gè)隨機(jī)凹凸的圓
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一個(gè)隨機(jī)凹凸的圓
drawPartCircle(new PointF(mCaptchaX, mCaptchaY + mCaptchaHeight - gap),
new PointF(mCaptchaX, mCaptchaY + mCaptchaHeight - gap * 2),
mCaptchaPath, mRandom.nextBoolean());
mCaptchaPath.close();
}
關(guān)于drawPartCircle(),它的功能是傳入起點(diǎn)、終點(diǎn)坐標(biāo),以及需要凹還是凸,和繪制的Path。它會(huì)在Path上繪制一個(gè)凹、凸的半圓。
代碼如下:
/**
* 傳入起點(diǎn)、終點(diǎn) 坐標(biāo)、凹凸和Path。
* 會(huì)自動(dòng)繪制凹凸的半圓弧
*
* @param start 起點(diǎn)坐標(biāo)
* @param end 終點(diǎn)坐標(biāo)
* @param path 半圓會(huì)繪制在這個(gè)path上
* @param outer 是否凸半圓
*/
public static void drawPartCircle(PointF start, PointF end, Path path, boolean outer) {
float c = 0.551915024494f;
//中點(diǎn)
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;
//以下是我寫出了所有的計(jì)算公式后推的,不要問(wèn)我過(guò)程,只可意會(huì)。
int flag;//旋轉(zhuǎn)系數(shù)
if (topToBottom) {
flag = 1;
} else {
flag = -1;
}
if (outer) {
//凸的 兩個(gè)半圓
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 {
//凹的 兩個(gè)半圓
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;
//以下是我寫出了所有的計(jì)算公式后推的,不要問(wèn)我過(guò)程,只可意會(huì)。
int flag;//旋轉(zhuǎn)系數(shù)
if (leftToRight) {
flag = 1;
} else {
flag = -1;
}
if (outer) {
//凸 兩個(gè)半圓
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 {
//凹 兩個(gè)半圓
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);
}
/*
沒(méi)推導(dǎo)之前的公式在這里
if (start.x < end.x) {
if (outer) {
//上左半圓 順時(shí)針
path.cubicTo(start.x, start.y - gap1,
middle.x - gap1, middle.y - r1,
middle.x, middle.y - r1);
//上右半圓:順時(shí)針
path.cubicTo(middle.x + gap1, middle.y - r1,
end.x, end.y - gap1,
end.x, end.y);
} else {
//下左半圓 逆時(shí)針
path.cubicTo(start.x, start.y + gap1,
middle.x - gap1, middle.y + r1,
middle.x, middle.y + r1);
//下右半圓 逆時(shí)針
path.cubicTo(middle.x + gap1, middle.y + r1,
end.x, end.y + gap1,
end.x, end.y);
}
} else {
if (outer) {
//下右半圓 順時(shí)針
path.cubicTo(start.x, start.y + gap1,
middle.x + gap1, middle.y + r1,
middle.x, middle.y + r1);
//下左半圓 順時(shí)針
path.cubicTo(middle.x - gap1, middle.y + r1,
end.x, end.y + gap1,
end.x, end.y);
}
}*/
}
}
這里用的是推導(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ò)圓是不是。
于是我就走上了用三節(jié)貝塞爾曲線模擬圓的路。
看來(lái)我當(dāng)初寫這一塊代碼的時(shí)候,腦子確實(shí)不太清醒,不過(guò)也有收獲。又復(fù)習(xí)了一遍Path的幾個(gè)函數(shù)和貝塞爾曲線。
2 摳圖:驗(yàn)證碼滑塊的生成
驗(yàn)證碼Path生成好了后,我要根據(jù)Path去生成驗(yàn)證碼滑塊。那么第一步就是要摳圖了。
代碼如下:
//生成滑塊
private void craeteMask() {
mMaskBitmap = getMaskBitmap(((BitmapDrawable) getDrawable()).getBitmap(), mCaptchaPath);
//滑塊陰影
mMaskShadowBitmap = mMaskBitmap.extractAlpha();
//拖動(dòng)的位移重置
mDragerOffset = 0;
//isDrawMask 繪制失敗閃爍動(dòng)畫用
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);
//有鋸齒 且無(wú)法解決,所以換成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對(duì)Bitmap進(jìn)行縮放
mCanvas.drawBitmap(mBitmap, getImageMatrix(), mMaskPaint);
mMaskPaint.setXfermode(null);
return tempBitmap;
}
其實(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的作用。所以我們?nèi)〕鯥mageView的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)的:
代碼如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繼承自ImageView,所以Bitmap,ImageView已經(jīng)幫我們draw好了。
//我只在上面繪制和驗(yàn)證碼相關(guān)的部分,
//是否處于驗(yàn)證模式,在驗(yàn)證成功后 為false,其余情況為true
if (isMatchMode) {
//首先繪制驗(yàn)證碼陰影
if (mCaptchaPath != null) {
canvas.drawPath(mCaptchaPath, mPaint);
}
//繪制滑塊
// isDrawMask 繪制失敗閃爍動(dòng)畫用
if (null != mMaskBitmap && null != mMaskShadowBitmap && isDrawMask) {
// 先繪制陰影
canvas.drawBitmap(mMaskShadowBitmap, -mCaptchaX + mDragerOffset, 0, mMaskShadowPaint);
canvas.drawBitmap(mMaskBitmap, -mCaptchaX + mDragerOffset, 0, null);
}
//驗(yàn)證成功,白光掃過(guò)的動(dòng)畫,這一塊動(dòng)畫感覺(jué)不完美,有提高空間
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));
值得說(shuō)的就是,配合滑塊滑動(dòng),是利用mDragerOffset,默認(rèn)是0,滑動(dòng)時(shí)mDragerOffset增加,滑塊右移,反之亦然。
驗(yàn)證成功的白光掃過(guò)動(dòng)畫,是利用canvas.translate()做的,mSuccessPath和mSuccessPaint如下:
mSuccessPaint = new Paint();
mSuccessPaint.setShader(new LinearGradient(0, 0, width, 0, new int[]{
0x11ffffff, 0x88ffffff}, null,
Shader.TileMode.MIRROR));
//模仿斗魚 是一個(gè)平行四邊形滾動(dòng)過(guò)去
mSuccessPath = new Path();
mSuccessPath.moveTo(0, 0);
mSuccessPath.rLineTo(width, 0);
mSuccessPath.rLineTo(width / 2, mHeight);
mSuccessPath.rLineTo(-width, 0);
mSuccessPath.close();
滑動(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)畫。
聯(lián)動(dòng)滑動(dòng):
上一節(jié)也提到,滑動(dòng)主要是改變mDragerOffset的值,然后重繪自己->ondraw(),根據(jù)mDragerOffset偏移滑塊Bitmap的繪制。
/**
* 重置驗(yàn)證碼滑動(dòng)距離,(一般用于驗(yàn)證失敗)
*/
public void resetCaptcha() {
mDragerOffset = 0;
invalidate();
}
/**
* 最大可滑動(dòng)值
* @return
*/
public int getMaxSwipeValue() {
//return ((BitmapDrawable) getDrawable()).getBitmap().getWidth() - mCaptchaWidth;
//返回控件寬度
return mWidth - mCaptchaWidth;
}
/**
* 設(shè)置當(dāng)前滑動(dòng)值
* @param value
*/
public void setCurrentSwipeValue(int value) {
mDragerOffset = value;
invalidate();
}
校驗(yàn):
校驗(yàn)的話,需要引入一個(gè)回調(diào)接口:
public interface OnCaptchaMatchCallback {
void matchSuccess(SwipeCaptchaView swipeCaptchaView);
void matchFailed(SwipeCaptchaView swipeCaptchaView);
}
/**
* 驗(yàn)證碼驗(yàn)證的回調(diào)
*/
private OnCaptchaMatchCallback onCaptchaMatchCallback;
public OnCaptchaMatchCallback getOnCaptchaMatchCallback() {
return onCaptchaMatchCallback;
}
/**
* 設(shè)置驗(yàn)證碼驗(yàn)證回調(diào)
*
* @param onCaptchaMatchCallback
* @return
*/
public SwipeCaptchaView setOnCaptchaMatchCallback(OnCaptchaMatchCallback onCaptchaMatchCallback) {
this.onCaptchaMatchCallback = onCaptchaMatchCallback;
return this;
}
/**
* 校驗(yàn)
*/
public void matchCaptcha() {
if (null != onCaptchaMatchCallback && isMatchMode) {
//這里驗(yàn)證邏輯,是通過(guò)比較,拖拽的距離 和 驗(yàn)證碼起點(diǎn)x坐標(biāo)。 默認(rèn)3dp以內(nèi)算是驗(yàn)證成功。
if (Math.abs(mDragerOffset - mCaptchaX) < mMatchDeviation) {
//成功的動(dòng)畫
mSuccessAnim.start();
} else {
mFailAnim.start();
}
}
}
成功、失敗的回調(diào)是在動(dòng)畫結(jié)束時(shí)通知的。
動(dòng)畫:
動(dòng)畫里要用到寬高,所以它是在onSizeChanged()方法里被調(diào)用的。
//驗(yàn)證動(dòng)畫初始化區(qū)域
private void createMatchAnim() {
mFailAnim = ValueAnimator.ofFloat(0, 1);
mFailAnim.setDuration(100)
.setRepeatCount(4);
mFailAnim.setRepeatMode(ValueAnimator.REVERSE);
//失敗的時(shí)候先閃一閃動(dòng)畫 斗魚是 隱藏-顯示 -隱藏 -顯示
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));
//模仿斗魚 是一個(gè)平行四邊形滾動(dòng)過(guò)去
mSuccessPath = new Path();
mSuccessPath.moveTo(0, 0);
mSuccessPath.rLineTo(width, 0);
mSuccessPath.rLineTo(width / 2, mHeight);
mSuccessPath.rLineTo(-width, 0);
mSuccessPath.close();
}
代碼很簡(jiǎn)單,修改的一些布爾值flag,在onDraw()方法里會(huì)用到,結(jié)合onDraw()一看便懂。
Demo
這一節(jié),我們聯(lián)動(dòng)SeekBar滑動(dòng)起來(lái)。
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, "恭喜你啊 驗(yàn)證成功 可以搞事情了", Toast.LENGTH_SHORT).show();
mSeekBar.setEnabled(false);
}
@Override
public void matchFailed(SwipeCaptchaView swipeCaptchaView) {
Toast.makeText(MainActivity.this, "你有80%的可能是機(jī)器人,現(xiàn)在走還來(lái)得及", 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) {
//隨便放這里是因?yàn)榭丶?
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é)
代碼傳送門 喜歡的話,隨手點(diǎn)個(gè)star。多謝
https://github.com/mcxtzhang/SwipeCaptcha
包含完整Demo和SwipeCaptchaView。
利用一些工具發(fā)現(xiàn)web端斗魚,驗(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)畫
以上所述是小編給大家介紹的Android 高仿斗魚滑動(dòng)驗(yàn)證碼,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
Android中解決WebView上下滑動(dòng)監(jiān)聽(tīng)問(wèn)題
本篇文章主要介紹了Android中解決WebView滑動(dòng)監(jiān)聽(tīng)問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04
Android WebView 詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android WebView 詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-04-04
Flutter開(kāi)發(fā)之動(dòng)態(tài)權(quán)限的使用
眾所周知,Android在6.0版本后將權(quán)限修改成了動(dòng)態(tài)權(quán)限,而iOS則一直使用的是動(dòng)態(tài)權(quán)限,所以在Flutter應(yīng)用開(kāi)發(fā)中如果涉及到一些危險(xiǎn)權(quán)限,就需要進(jìn)行動(dòng)態(tài)申請(qǐng),本文就詳細(xì)的介紹一下,感興趣的可以了解一下2021-09-09
Android支持國(guó)際化多語(yǔ)言那點(diǎn)事(支持8.0+)
我們?cè)陂_(kāi)發(fā)app可能會(huì)拓展國(guó)外市場(chǎng),那么對(duì)包含英語(yǔ)在內(nèi)的其它語(yǔ)言支持就很有必要了。這篇文章主要介紹了Android支持國(guó)際化多語(yǔ)言那點(diǎn)事(支持8.0+),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06

