前言
基于安卓平臺的滑動拼## 二級標(biāo)題圖驗(yàn)證組件SwipeCaptcha(https://github.com/mcxtzhang/SwipeCaptcha ),實(shí)現(xiàn)了鴻蒙化遷移和重構(gòu),代碼已經(jīng)開源到(https://gitee.com/isrc_ohos/swipe-captcha_ohos ),目前已經(jīng)獲得了很多人的Star和Fork ,歡迎各位下載使用并提出寶貴意見!
背景
前一期SwipeCaptcha_ohos2.0文章(https://harmonyos.51cto.com/posts/8787 )中介紹過,系統(tǒng)為了確保在注冊或登錄頁面時(shí)不是機(jī)器人在操作(若要實(shí)現(xiàn)防機(jī)器人操作效果,需要增加加密算法,本期介紹的組件中不包含此部分),通常需要用戶進(jìn)行手動驗(yàn)證,本期的SwipeCaptcha_ohos3.0是對前兩版本驗(yàn)證方式進(jìn)行功能升級,得到的一種新的驗(yàn)證方式——旋轉(zhuǎn)驗(yàn)證。
此驗(yàn)證方式將圖片作為背景,通過把旋轉(zhuǎn)塊旋轉(zhuǎn)至能夠與背景圖片無縫拼接來完成驗(yàn)證,操作簡單,安全性強(qiáng),可被應(yīng)用于各種網(wǎng)站的登錄、注冊、找回密碼或投票等場景中。
組件效果展示
成功運(yùn)行組件后,可以通過將旋轉(zhuǎn)塊旋轉(zhuǎn)至能夠與背景圖片拼接成一張完整圖片,從而完成驗(yàn)證。對應(yīng)圖1所示的運(yùn)行效果圖,本組件主要提供的功能是:
- 拖動圖片下方的滑動條,可以改變旋轉(zhuǎn)塊角度;
- 若旋轉(zhuǎn)塊旋轉(zhuǎn)后與原背景圖片的誤差值小于既定的閾值,則驗(yàn)證成功,反之則失敗;
- 在驗(yàn)證完成后,可以點(diǎn)擊滑動條下方的條狀按鈕重新生成驗(yàn)證碼(即旋轉(zhuǎn)塊的角度隨機(jī)設(shè)置)。
::: hljs-center
:::
::: hljs-center
圖1 旋轉(zhuǎn)驗(yàn)證運(yùn)行效果
:::
Sample解析
通過上文相信大家已經(jīng)了解SwipeCaptcha_ohos3.0組件的使用效果,下面將具體講解其使用方法。其使用方法和SwipeCaptcha_ohos2.0組件類似,在此我們簡單回顧一下,共分為5個(gè)步驟:
步驟1. 在xml文件中添加RotateCaptchaView控件。
步驟2. 導(dǎo)入RotateCaptchaView類并聲明類對象。
步驟3. 綁定RotateCaptchaView控件并設(shè)置組件背景圖片。
步驟4. 設(shè)置回調(diào)處理函數(shù)。
步驟5. 設(shè)置Button控件監(jiān)聽事件,重新生成驗(yàn)證區(qū)域
(1)在xml文件中添加RotateCaptchaView控件
在xml文件中添加RotateCaptchaView控件,用于顯示旋轉(zhuǎn)驗(yàn)證的動態(tài)效果。先設(shè)置該控件的高和寬,此處將寬度定為跟隨父控件的大小(match_parent),高度設(shè)置為220vp;再設(shè)置組件左右邊距分別為12vp,旋轉(zhuǎn)塊的半徑為80vp。
```html/xml
ohos:height="220vp"http://控件的高
ohos:width="match_parent"http://控件的寬
ohos:left_margin="12vp"http://左邊距
ohos:right_margin="12vp"http://右邊距
app:captchaRadius="80vp"/>//旋轉(zhuǎn)塊半徑
**(2)導(dǎo)入SwipeCaptchaView類并聲明類對象**在MainAbilitySlice.java文件中,通過import關(guān)鍵字導(dǎo)入RotateCaptchaView類,該類用于在后續(xù)為驗(yàn)證結(jié)果設(shè)置回調(diào)并重新生成旋轉(zhuǎn)塊角度。```java//導(dǎo)入SwipeCaptchaView類import com.huawei.swipecaptchaview.lib.RotateCaptchaView;public class MainAbilitySlice extends AbilitySlice {//聲明SwipeCaptchaView類對象SwipeCaptchaView swipeCaptchaView; ......}
(3)綁定SwipeCaptchaView控件并設(shè)置組件背景圖片
在MainAbilitySlice.java的onStart()方法中,使用findComponentById()方法將xml文件中RotateCaptchaView控件與RotateCaptchaView類對象綁定;再調(diào)用setImageId()方法設(shè)置組件的背景圖片。
//根據(jù)id綁定相應(yīng)的控件rotateCaptchaView = (RotateCaptchaView) findComponentById(ResourceTable.Id_rotateCaptchaView);...//設(shè)置背景圖片rotateCaptchaView.setImageId(ResourceTable.Media_pic02);
(4)設(shè)置回調(diào)處理函數(shù)
設(shè)置SwipeCaptchaView組件的回調(diào)處理函數(shù),來提示用戶旋轉(zhuǎn)驗(yàn)證結(jié)果。以提示用戶“驗(yàn)證成功”這個(gè)功能為例:需要重寫matchSuccess()方法,設(shè)置驗(yàn)證成功后的提示信息。在上述方法中實(shí)例化一個(gè)ToastDialog提示框?qū)ο螅褂迷搶ο蟮膕etText()方法設(shè)置顯示文字為“驗(yàn)證成功!”;setAlignment()方法設(shè)置提示框的布局位置在整體布局的中央;show()方法用于顯示提示框。
設(shè)置驗(yàn)證失敗的情況和驗(yàn)證成功同理,只需重寫matchFailed()方法,同時(shí)將文字信息設(shè)置為“驗(yàn)證失敗!”即可。
//每次旋轉(zhuǎn)結(jié)束后會根據(jù)相應(yīng)的回調(diào)提示用戶驗(yàn)證結(jié)果rotateCaptchaView.setOnCaptchaMatchCallback(new RotateCaptchaView.OnCaptchaMatchCallback() { @Override public void matchSuccess(RotateCaptchaView rotateCaptchaView) { new ToastDialog(getContext()) .setText(" 驗(yàn)證成功!") .setAlignment(LayoutAlignment.CENTER) .show(); }...}
(5)設(shè)置Button控件監(jiān)聽事件,重新生成驗(yàn)證區(qū)域
綁定button對象和xml文件中“重新生成驗(yàn)證碼”Button控件;為button設(shè)置監(jiān)聽事件,每次點(diǎn)擊按鈕后調(diào)用createCaptcha()方法,該方法用于重新生成驗(yàn)證碼(即旋轉(zhuǎn)塊的角度隨機(jī)設(shè)置)。
button = (Button) findComponentById(ResourceTable.Id_btn_change);//綁定Buttonbutton.setClickedListener(new Component.ClickedListener() {//設(shè)置監(jiān)聽 @Override public void onClick(Component component) { rotateCaptchaView.createCaptcha();//隨機(jī)生成旋轉(zhuǎn)塊的旋轉(zhuǎn)角度 ... }});
Library解析
Library解析部分將要重點(diǎn)圍繞RotateCaptchaView類,對其內(nèi)部邏輯按步驟展開講解,主要包括初始化準(zhǔn)備工作、初始化旋轉(zhuǎn)驗(yàn)證區(qū)域、繪制旋轉(zhuǎn)塊邊框的路徑并計(jì)算比例、以及繪制旋轉(zhuǎn)塊邊框和驗(yàn)證區(qū)域。
(1)初始化準(zhǔn)備工作
此部分是在RotateCaptchaView類構(gòu)造函數(shù)中實(shí)現(xiàn)的,具體由初始化方法init()執(zhí)行,此部分主要實(shí)現(xiàn)了下述4個(gè)功能。
1)設(shè)置參數(shù)
獲取xml文件中添加的SwipeCaptcha_ohos3.0組件的參數(shù)即寬、高,并獲取系統(tǒng)屏幕寬度,用于后續(xù)為背景圖片設(shè)置尺寸;設(shè)置半徑mCaptchaRadius用于確定旋轉(zhuǎn)塊尺寸,設(shè)置旋轉(zhuǎn)驗(yàn)證閾值mMatchDeviation用于判斷旋轉(zhuǎn)驗(yàn)證是否成功。
private void init(Context context, AttrSet attrSet, String defStyleAttr) { mHeight = getHeight();//獲取xml中旋轉(zhuǎn)驗(yàn)證控件的高 mWidth = getWidth();//獲取xml中旋轉(zhuǎn)驗(yàn)證控件的寬 if (mWidth == 0) { //match_parent //獲取系統(tǒng)屏幕寬度 mWidth = DisplayManager.getInstance().getDefaultDisplay(context).get().getAttributes().width; } mCaptchaRadius = (mHeight - SLIDER_HEIGHT - AttrHelper.vp2px(20, context)) / 2;//旋轉(zhuǎn)塊的半徑 mMatchDeviation = AttrHelper.vp2px(3, context);//旋轉(zhuǎn)驗(yàn)證成功與否的閾值...}
2)初始化背景圖片
此步驟主要是完成背景圖片的初始化操作,包括圖片尺寸、縮放模式、圖像源等屬性的設(shè)定,原理可參考圖2。
具體實(shí)現(xiàn)步驟為:
- 實(shí)例化Image類得到背景圖片的對象;
- 設(shè)置Image對象的寬為之前獲取的屏幕寬度mWidth,高為mHeight-SLIDER_HEIGHT,即組件高減去拖動條高的差值;
- 設(shè)置圖片縮放模式為中心縮放;
- 設(shè)置圖像源為圖2中的灰色圖片,表示“暫無背景圖片,”可提示用戶暫未設(shè)置真正的背景圖片。
::: hljs-center
::: hljs-center
:::
圖2 初始化并設(shè)置背景圖片效果
:::
mImage = new Image(context);LayoutConfig imageConfig = new LayoutConfig(mWidth, mHeight - SLIDER_HEIGHT);//寬為屏幕寬度,高為組件高減去拖動條高的差值mImage.setLayoutConfig(imageConfig);mImage.setScaleMode(Image.ScaleMode.CLIP_CENTER);//縮放模式為中心縮放mImage.setPixelMap(ResourceTable.Media_no_resource);//設(shè)置圖片
3)設(shè)置拖動條
先實(shí)例化Slider類得到拖動條對象,為其設(shè)置寬、高、上邊距等屬性;再設(shè)置拖動條的進(jìn)度值,其中最小值設(shè)置為0,最大值設(shè)置為10000(此處將最大值設(shè)置較大的原因是為了得到更加絲滑流暢的拖動效果),并將初始進(jìn)度值設(shè)置為0,將已拖動的進(jìn)度條顏色設(shè)置為黑色用來強(qiáng)調(diào)拖動進(jìn)度。
mSlider = new Slider(context);mSlider = new Slider(mLayout.getContext());mSlider.setWidth(mWidth); //寬度mSlider.setHeight(SLIDER_HEIGHT); //高度mSlider.setMarginTop(mHeight - SLIDER_HEIGHT);mSlider.setMinValue(0); //進(jìn)度最小值mSlider.setMaxValue(10000); //進(jìn)度最大值mSlider.setProgressValue(0); //當(dāng)前進(jìn)度值mSlider.setProgressColor(Color.BLACK); //進(jìn)度條已拖動的顏色setSlideListener(); //監(jiān)聽器
4)設(shè)置拖動條監(jiān)聽事件
拖動條的監(jiān)聽事件是通過調(diào)用setSliderListener()方法具體實(shí)現(xiàn)的,在該方法中,首先重寫onTouchEnd()方法,判斷旋轉(zhuǎn)結(jié)束后旋轉(zhuǎn)塊當(dāng)前角度和旋轉(zhuǎn)至無縫拼接的角度的差值是否小于已經(jīng)規(guī)定好的驗(yàn)證閾值mMatchDeviation。
若小于驗(yàn)證閾值,則驗(yàn)證成功:先取消旋轉(zhuǎn)塊邊緣的陰影,這是為了完整地呈現(xiàn)旋轉(zhuǎn)拼接后的背景圖片,再設(shè)置回調(diào)函數(shù),實(shí)現(xiàn)彈出提示框提示用戶驗(yàn)證成功的效果;
若大于驗(yàn)證閾值,則驗(yàn)證失敗:直接設(shè)置回調(diào)函數(shù),提示用戶驗(yàn)證失敗的效果,同時(shí)旋轉(zhuǎn)塊恢復(fù)驗(yàn)證前的角度。
private void setSlideListener() { mSlider.setValueChangedListener(new Slider.ValueChangedListener() { @Override public void onTouchEnd(Slider slider) { if (onCaptchaMatchCallback != null) { if (Math.abs(mSlider.getProgress() * 360 / 10000 - randomDegree) < mMatchDeviation) {//判斷旋轉(zhuǎn)驗(yàn)證誤差是否小于閾值 mPaint.setMaskFilter(null); //取消旋轉(zhuǎn)塊的陰影 ... onCaptchaMatchCallback.matchSuccess(RotateCaptchaView.this); } else { slider.setProgressValue(0); onCaptchaMatchCallback.matchFailed(RotateCaptchaView.this); }}} });}
(2)初始化旋轉(zhuǎn)驗(yàn)證區(qū)域
在通過Image類對象調(diào)用setPixelMap()方法設(shè)置好驗(yàn)證背景圖片后,由initCaptcha()方法完成旋轉(zhuǎn)驗(yàn)證區(qū)域的初始化,主要實(shí)現(xiàn)的是對畫筆的初始化。
先實(shí)例化一個(gè)隨機(jī)數(shù)對象mRandom,用于后續(xù)計(jì)算隨機(jī)生成的旋轉(zhuǎn)驗(yàn)證塊角度值。再實(shí)例化Paint類得到畫筆對象mPaint用于具體繪制旋轉(zhuǎn)塊,為其設(shè)置畫筆抗鋸齒、位掩碼標(biāo)志、填充樣式和陰影等屬性,其中設(shè)置抗鋸齒屬性是為了實(shí)現(xiàn)邊緣弧線流暢無明顯鋸齒狀過度痕跡的效果,設(shè)置位掩碼標(biāo)志用于防止抖動,實(shí)現(xiàn)柔和的顏色過度效果防止出現(xiàn)階梯狀痕跡的現(xiàn)象。
再實(shí)例化一個(gè)Path類對象用于繪制旋轉(zhuǎn)塊邊緣的圓形路徑。 接著就可以調(diào)用createCaptcha()方法開始繪制了。
private void initCaptcha() { mRandom = new Random(System.nanoTime()); //設(shè)置畫筆 mPaint = new Paint(); mPaint.setAntiAlias(true); //抗鋸齒效果 mPaint.setDither(true); //防止抖動 mPaint.setStyle(Paint.Style.FILL_STYLE);//填充樣式 mPaint.setMaskFilter(new MaskFilter(10, MaskFilter.Blur.SOLID)); //陰影 mPath = new Path();//用于繪制旋轉(zhuǎn)快邊緣圓形路徑 createCaptcha();}
(3)設(shè)置旋轉(zhuǎn)塊邊框的路徑并計(jì)算比例
這部分和接下來的步驟(4)都是由initCaptcha()方法實(shí)現(xiàn)的。在此部分中,
首先:
- 判斷是否設(shè)置好了背景圖片,如果設(shè)置好了則使用Random類對象調(diào)用帶參的nextInt()方法隨機(jī)生成一個(gè)范圍0至240的整數(shù),以此隨機(jī)整數(shù)與60相加的和作為重新生成驗(yàn)證碼后旋轉(zhuǎn)塊的隨機(jī)角度;
- 設(shè)置拖動條初始進(jìn)度為0,同時(shí)激活按鈕,將其狀態(tài)設(shè)置為可點(diǎn)擊并觸發(fā)點(diǎn)擊事件;
- 為繪制的畫筆添加陰影效果,傳入的參數(shù)分別表示度數(shù)和樣式,實(shí)現(xiàn)在旋轉(zhuǎn)塊圓形邊緣處有一圈陰影的效果;
- 設(shè)置好上述屬性后,需要調(diào)用invalidate()方法對圖片和布局視圖進(jìn)行刷新。
其次: - 設(shè)置繪制路徑即旋轉(zhuǎn)塊邊框的圓形軌跡,為嚴(yán)謹(jǐn)起見先使用reset()方法清空之前已經(jīng)繪制的Path路徑至原始狀態(tài)。
- 調(diào)用addCircle()方法設(shè)置圓的繪制路徑,該方法前兩個(gè)參數(shù)表示旋轉(zhuǎn)塊的圓點(diǎn)坐標(biāo)X、Y值,第三個(gè)參數(shù)表示圓的半徑,第四個(gè)參數(shù)表示繪制方向。
此處我們設(shè)置的繪制軌跡是一個(gè)以圖片中點(diǎn)(mWidth/2f,(mHeight-SLIDER_HEIGHT)/2f)為圓心、mCaptchaRadius為半徑、順時(shí)針方向繪制的圓形,計(jì)算原理和坐標(biāo)軸方向可參考圖3,其中圓心即圖片中點(diǎn)坐標(biāo)的計(jì)算方法是橫坐標(biāo)X為圖片寬度的一半,縱坐標(biāo)Y為圖片高度的一半即旋轉(zhuǎn)驗(yàn)證組件的高減去拖動條高的差值的一半。
::: hljs-center
:::
::: hljs-center
圖3 旋轉(zhuǎn)塊邊框圓形路徑設(shè)置原理
:::
public void createCaptcha() { if (mImage.getPixelMap() != null) {//已設(shè)置背景圖片 randomDegree = mRandom.nextInt(240) + 60;//生成隨機(jī)角度 mSlider.setProgressValue(0);//初始進(jìn)度為0 mSlider.setEnabled(true);//狀態(tài)為可點(diǎn)擊并觸發(fā)監(jiān)聽 mPaint.setMaskFilter(new MaskFilter(10, MaskFilter.Blur.SOLID));//添加陰影 mImage.invalidate();//刷新視圖 mLayout.invalidate(); } //繪制遮罩路徑 mPath.reset(); mPath.addCircle(mWidth / 2f, (mHeight - SLIDER_HEIGHT) / 2f, mCaptchaRadius, Path.Direction.CLOCK_WISE); mPath.close();...}
最后,根據(jù)圖片的原寬度和控件寬度算出縮放比例,這部分原理與SwipeCaptcha_ohos2.0組件同理,簡單回顧一下,此處計(jì)算得到的較大的ratio代表圖片真實(shí)的縮放比例,這是由于上文介紹的Image控件將圖片縮放模式設(shè)為了CLIP_CENTER中心縮放模式,該模式會將圖片的短邊縮放至合適的大小并對長邊進(jìn)行裁剪,因此較小的縮放比例代表被裁剪的邊,較大的則代表在填充進(jìn)旋轉(zhuǎn)驗(yàn)證組件時(shí)的真實(shí)縮放比例。
public void createCaptcha() {...//根據(jù)圖片的原寬度 和 控件寬度 算出縮放比例 PixelMap pixelMap = mImage.getPixelMap(); int originWidth = pixelMap.getImageInfo().size.width; int originHeight = pixelMap.getImageInfo().size.height; float ratioWidth = (float) mWidth / originWidth; float ratioHeight = (float) (mHeight - SLIDER_HEIGHT) / originHeight; float ratio = Math.max(ratioWidth, ratioHeight);//更大的ratio...}
(4)繪制旋轉(zhuǎn)塊邊框和驗(yàn)證區(qū)域
本步驟首先在Canvas畫布上,根據(jù)上一步設(shè)置好的繪制軌跡mPath和畫筆mPaint繪制旋轉(zhuǎn)塊的圓形邊框,這部分不會隨著旋轉(zhuǎn)而更新。
接著根據(jù)拖動條變化的數(shù)值調(diào)整旋轉(zhuǎn)塊的旋轉(zhuǎn)角度。具體旋轉(zhuǎn)的角度值是mSlider.getProgress() * 360 / 10000 - randomDegree,即算出拖動條當(dāng)前進(jìn)度值占最大值的比例,然后乘以360得到按比例變換后的角度值,再減去前面步驟已生成的隨機(jī)角度,就可以以(mWidth/2f,mHeight-SLIDER_HEIGHT)即背景圖片中點(diǎn)為旋轉(zhuǎn)中心進(jìn)行旋轉(zhuǎn);并獲取圖片的 PixelMapHolder,根據(jù)路徑裁剪canvas畫布,將其縮放至跟圖片縮放程度一致。
最后判斷若圖片寬的縮放比例和圖片真實(shí)縮放比例一樣時(shí),說明寬沒變是在垂直方向上進(jìn)行了裁剪,則根據(jù)比例計(jì)算出被裁剪掉的圖片高度,并在畫布上繪制內(nèi)容;若二者比例不相等,說明寬有變化是在水平方向上進(jìn)行了裁剪,則根據(jù)比例計(jì)算出被裁剪掉的圖片寬度,并在畫布上繪制內(nèi)容。
public void createCaptcha() {... mImage.addDrawTask((component, canvas) -> {//繪制邊框 canvas.drawPath(mPath, mPaint); }); mLayout.addDrawTask((component, canvas) -> {//繪制驗(yàn)證碼 canvas.rotate(mSlider.getProgress() * 360 / 10000 - randomDegree, mWidth / 2f, (mHeight - SLIDER_HEIGHT) / 2f); //根據(jù)拖動條的數(shù)值調(diào)整旋轉(zhuǎn)角度 canvas.translate(0, 0); PixelMapHolder pixelMapHolder = new PixelMapHolder(pixelMap);//獲取圖片的 PixelMapHolder canvas.clipPath(mPath, Canvas.ClipOp.INTERSECT);//根據(jù)路徑裁剪canvas canvas.scale(ratio, ratio);//畫布縮放至跟圖片縮放程度一致 if (ratio == ratioWidth) { float heightErr = (originHeight * ratio - (mHeight - SLIDER_HEIGHT)) / 2;//根據(jù)比例計(jì)算出垂直方向上由于 CLIP_CENTER 裁剪掉的圖片的高度 canvas.drawPixelMapHolder(pixelMapHolder, 0, -heightErr / ratio, mPaint);//繪制內(nèi)容 } else { float widthErr = (originWidth * ratio - mWidth) / 2;//根據(jù)比例計(jì)算出水平方向上由于 CLIP_CENTER 裁剪掉的圖片的寬度 canvas.drawPixelMapHolder(pixelMapHolder, -widthErr / ratio, 0, mPaint);//繪制內(nèi)容 } });}
項(xiàng)目貢獻(xiàn)人
王時(shí)予 李珂 朱偉 鄭森文 陳美汝 張馨心 劉雨琦
想了解更多關(guān)于鴻蒙的內(nèi)容,請?jiān)L問:
51CTO和華為官方戰(zhàn)略合作共建的鴻蒙技術(shù)社區(qū)
https://harmonyos.51cto.com/#bkwz
::: hljs-center
:::