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
版權宣告:本文為博主原創文章,轉載請附上博文連結!