鴻蒙開源第三方組件—SwipeCaptcha_ohos3.0旋轉驗證組件
前言
基于安卓平臺的滑動拼## 二級標題圖驗證組件SwipeCaptcha(https://github.com/mcxtzhang/SwipeCaptcha ),實現了鴻蒙化遷移和重構,代碼已經開源到(https://gitee.com/isrc_ohos/swipe-captcha_ohos ),目前已經獲得了很多人的Star和Fork ,歡迎各位下載使用并提出寶貴意見!
背景
前一期SwipeCaptcha_ohos2.0文章(https://harmonyos.51cto.com/posts/8787 )中介紹過,系統為了確保在注冊或登錄頁面時不是機器人在操作,通常需要用戶進行手動驗證,本期的SwipeCaptcha_ohos3.0是對前兩版本驗證方式進行功能升級,得到的一種新的驗證方式——旋轉驗證。
此驗證方式將圖片作為背景,通過把旋轉塊旋轉至能夠與背景圖片無縫拼接來完成驗證,操作簡單,安全性強,可被應用于各種網站的登錄、注冊、找回密碼或投票等場景中。
組件效果展示
成功運行組件后,可以通過將旋轉塊旋轉至能夠與背景圖片拼接成一張完整圖片,從而完成驗證。對應圖1所示的運行效果圖,本組件主要提供的功能是:
- 拖動圖片下方的滑動條,可以改變旋轉塊角度;
- 若旋轉塊旋轉后與原背景圖片的誤差值小于既定的閾值,則驗證成功,反之則失敗;
- 在驗證完成后,可以點擊滑動條下方的條狀按鈕重新生成驗證碼(即旋轉塊的角度隨機設置)。

圖1 旋轉驗證運行效果
Sample解析
通過上文相信大家已經了解SwipeCaptcha_ohos3.0組件的使用效果,下面將具體講解其使用方法。其使用方法和SwipeCaptcha_ohos2.0組件類似,在此我們簡單回顧一下,共分為5個步驟:
步驟1. 在xml文件中添加RotateCaptchaView控件。
步驟2. 導入RotateCaptchaView類并聲明類對象。
步驟3. 綁定RotateCaptchaView控件并設置組件背景圖片。
步驟4. 設置回調處理函數。
步驟5. 設置Button控件監聽事件,重新生成驗證區域
(1)在xml文件中添加RotateCaptchaView控件
在xml文件中添加RotateCaptchaView控件,用于顯示旋轉驗證的動態效果。先設置該控件的高和寬,此處將寬度定為跟隨父控件的大小(match_parent),高度設置為220vp;再設置組件左右邊距分別為12vp,旋轉塊的半徑為80vp。
- <com.huawei.swipecaptchaview.lib.RotateCaptchaView
- ohos:id="$+id:rotateCaptchaView"//控件id
- ohos:height="220vp"//控件的高
- ohos:width="match_parent"//控件的寬
- ohos:left_margin="12vp"//左邊距
- ohos:right_margin="12vp"//右邊距
- app:captchaRadius="80vp"/>//旋轉塊半徑
(2)導入SwipeCaptchaView類并聲明類對象
在MainAbilitySlice.java文件中,通過import關鍵字導入RotateCaptchaView類,該類用于在后續為驗證結果設置回調并重新生成旋轉塊角度。
- //導入SwipeCaptchaView類
- import com.huawei.swipecaptchaview.lib.RotateCaptchaView;public class MainAbilitySlice extends AbilitySlice {
- //聲明SwipeCaptchaView類對象
- SwipeCaptchaView swipeCaptchaView;
- ......
- }
(3)綁定SwipeCaptchaView控件并設置組件背景圖片
在MainAbilitySlice.java的onStart()方法中,使用findComponentById()方法將xml文件中RotateCaptchaView控件與RotateCaptchaView類對象綁定;再調用setImageId()方法設置組件的背景圖片。
- //根據id綁定相應的控件
- rotateCaptchaView = (RotateCaptchaView) findComponentById(ResourceTable.Id_rotateCaptchaView);
- ...
- //設置背景圖片
- rotateCaptchaView.setImageId(ResourceTable.Media_pic02);
(4)設置回調處理函數
設置SwipeCaptchaView組件的回調處理函數,來提示用戶旋轉驗證結果。以提示用戶“驗證成功”這個功能為例:需要重寫matchSuccess()方法,設置驗證成功后的提示信息。在上述方法中實例化一個ToastDialog提示框對象,使用該對象的setText()方法設置顯示文字為“驗證成功!”;setAlignment()方法設置提示框的布局位置在整體布局的中央;show()方法用于顯示提示框。
設置驗證失敗的情況和驗證成功同理,只需重寫matchFailed()方法,同時將文字信息設置為“驗證失敗!”即可。
- //每次旋轉結束后會根據相應的回調提示用戶驗證結果
- rotateCaptchaView.setOnCaptchaMatchCallback(new RotateCaptchaView.OnCaptchaMatchCallback() {
- @Override
- public void matchSuccess(RotateCaptchaView rotateCaptchaView) {
- new ToastDialog(getContext())
- .setText(" 驗證成功!")
- .setAlignment(LayoutAlignment.CENTER)
- .show();
- }
- ...
- }
(5)設置Button控件監聽事件,重新生成驗證區域
綁定button對象和xml文件中“重新生成驗證碼”Button控件;為button設置監聽事件,每次點擊按鈕后調用createCaptcha()方法,該方法用于重新生成驗證碼(即旋轉塊的角度隨機設置)。
- button = (Button) findComponentById(ResourceTable.Id_btn_change);//綁定Button
- button.setClickedListener(new Component.ClickedListener() {//設置監聽
- @Override
- public void onClick(Component component) {
- rotateCaptchaView.createCaptcha();//隨機生成旋轉塊的旋轉角度
- ...
- }
- });
Library解析
Library解析部分將要重點圍繞RotateCaptchaView類,對其內部邏輯按步驟展開講解,主要包括初始化準備工作、初始化旋轉驗證區域、繪制旋轉塊邊框的路徑并計算比例、以及繪制旋轉塊邊框和驗證區域。
(1)初始化準備工作
此部分是在RotateCaptchaView類構造函數中實現的,具體由初始化方法init()執行,此部分主要實現了下述4個功能。
1)設置參數
獲取xml文件中添加的SwipeCaptcha_ohos3.0組件的參數即寬、高,并獲取系統屏幕寬度,用于后續為背景圖片設置尺寸;設置半徑mCaptchaRadius用于確定旋轉塊尺寸,設置旋轉驗證閾值mMatchDeviation用于判斷旋轉驗證是否成功。
- private void init(Context context, AttrSet attrSet, String defStyleAttr) {
- mHeight = getHeight();//獲取xml中旋轉驗證控件的高
- mWidth = getWidth();//獲取xml中旋轉驗證控件的寬
- if (mWidth == 0) { //match_parent
- //獲取系統屏幕寬度
- mWidth = DisplayManager.getInstance().getDefaultDisplay(context).get().getAttributes().width;
- }
- mCaptchaRadius = (mHeight - SLIDER_HEIGHT - AttrHelper.vp2px(20, context)) / 2;//旋轉塊的半徑
- mMatchDeviation = AttrHelper.vp2px(3, context);//旋轉驗證成功與否的閾值
- ...
- }
2)初始化背景圖片
此步驟主要是完成背景圖片的初始化操作,包括圖片尺寸、縮放模式、圖像源等屬性的設定,原理可參考圖2。
具體實現步驟為:
- 實例化Image類得到背景圖片的對象;
- 設置Image對象的寬為之前獲取的屏幕寬度mWidth,高為mHeight-SLIDER_HEIGHT,即組件高減去拖動條高的差值;
- 設置圖片縮放模式為中心縮放;
- 設置圖像源為圖2中的灰色圖片,表示“暫無背景圖片,”可提示用戶暫未設置真正的背景圖片。

圖2 初始化并設置背景圖片效果
- 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);//設置圖片
3)設置拖動條
先實例化Slider類得到拖動條對象,為其設置寬、高、上邊距等屬性;再設置拖動條的進度值,其中最小值設置為0,最大值設置為10000(此處將最大值設置較大的原因是為了得到更加絲滑流暢的拖動效果),并將初始進度值設置為0,將已拖動的進度條顏色設置為黑色用來強調拖動進度。
- mSlider = new Slider(context);
- mSlider = new Slider(mLayout.getContext());
- mSlider.setWidth(mWidth); //寬度
- mSlider.setHeight(SLIDER_HEIGHT); //高度
- mSlider.setMarginTop(mHeight - SLIDER_HEIGHT);
- mSlider.setMinValue(0); //進度最小值
- mSlider.setMaxValue(10000); //進度最大值
- mSlider.setProgressValue(0); //當前進度值
- mSlider.setProgressColor(Color.BLACK); //進度條已拖動的顏色
- setSlideListener(); //監聽器
4)設置拖動條監聽事件
拖動條的監聽事件是通過調用setSliderListener()方法具體實現的,在該方法中,首先重寫onTouchEnd()方法,判斷旋轉結束后旋轉塊當前角度和旋轉至無縫拼接的角度的差值是否小于已經規定好的驗證閾值mMatchDeviation。
若小于驗證閾值,則驗證成功:先取消旋轉塊邊緣的陰影,這是為了完整地呈現旋轉拼接后的背景圖片,再設置回調函數,實現彈出提示框提示用戶驗證成功的效果;
若大于驗證閾值,則驗證失敗:直接設置回調函數,提示用戶驗證失敗的效果,同時旋轉塊恢復驗證前的角度。
- 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) {//判斷旋轉驗證誤差是否小于閾值
- mPaint.setMaskFilter(null); //取消旋轉塊的陰影
- ...
- onCaptchaMatchCallback.matchSuccess(RotateCaptchaView.this);
- } else {
- slider.setProgressValue(0);
- onCaptchaMatchCallback.matchFailed(RotateCaptchaView.this);
- }}}
- });
- }
(2)初始化旋轉驗證區域
在通過Image類對象調用setPixelMap()方法設置好驗證背景圖片后,由initCaptcha()方法完成旋轉驗證區域的初始化,主要實現的是對畫筆的初始化。
先實例化一個隨機數對象mRandom,用于后續計算隨機生成的旋轉驗證塊角度值。再實例化Paint類得到畫筆對象mPaint用于具體繪制旋轉塊,為其設置畫筆抗鋸齒、位掩碼標志、填充樣式和陰影等屬性,其中設置抗鋸齒屬性是為了實現邊緣弧線流暢無明顯鋸齒狀過度痕跡的效果,設置位掩碼標志用于防止抖動,實現柔和的顏色過度效果防止出現階梯狀痕跡的現象。
再實例化一個Path類對象用于繪制旋轉塊邊緣的圓形路徑。 接著就可以調用createCaptcha()方法開始繪制了。
- private void initCaptcha() {
- mRandom = new Random(System.nanoTime());
- //設置畫筆
- 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();//用于繪制旋轉快邊緣圓形路徑
- createCaptcha();
- }
(3)設置旋轉塊邊框的路徑并計算比例
這部分和接下來的步驟(4)都是由initCaptcha()方法實現的。在此部分中,
首先:
- 判斷是否設置好了背景圖片,如果設置好了則使用Random類對象調用帶參的nextInt()方法隨機生成一個范圍0至240的整數,以此隨機整數與60相加的和作為重新生成驗證碼后旋轉塊的隨機角度;
- 設置拖動條初始進度為0,同時激活按鈕,將其狀態設置為可點擊并觸發點擊事件;
- 為繪制的畫筆添加陰影效果,傳入的參數分別表示度數和樣式,實現在旋轉塊圓形邊緣處有一圈陰影的效果;
- 設置好上述屬性后,需要調用invalidate()方法對圖片和布局視圖進行刷新。
- 其次:
- 設置繪制路徑即旋轉塊邊框的圓形軌跡,為嚴謹起見先使用reset()方法清空之前已經繪制的Path路徑至原始狀態。
- 調用addCircle()方法設置圓的繪制路徑,該方法前兩個參數表示旋轉塊的圓點坐標X、Y值,第三個參數表示圓的半徑,第四個參數表示繪制方向。
- 此處我們設置的繪制軌跡是一個以圖片中點(mWidth/2f,(mHeight-SLIDER_HEIGHT)/2f)為圓心、mCaptchaRadius為半徑、順時針方向繪制的圓形,計算原理和坐標軸方向可參考圖3,其中圓心即圖片中點坐標的計算方法是橫坐標X為圖片寬度的一半,縱坐標Y為圖片高度的一半即旋轉驗證組件的高減去拖動條高的差值的一半。

圖3 旋轉塊邊框圓形路徑設置原理
- public void createCaptcha() {
- if (mImage.getPixelMap() != null) {//已設置背景圖片
- randomDegree = mRandom.nextInt(240) + 60;//生成隨機角度
- mSlider.setProgressValue(0);//初始進度為0
- mSlider.setEnabled(true);//狀態為可點擊并觸發監聽
- 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();
- ...
- }
最后,根據圖片的原寬度和控件寬度算出縮放比例,這部分原理與SwipeCaptcha_ohos2.0組件同理,簡單回顧一下,此處計算得到的較大的ratio代表圖片真實的縮放比例,這是由于上文介紹的Image控件將圖片縮放模式設為了CLIP_CENTER中心縮放模式,該模式會將圖片的短邊縮放至合適的大小并對長邊進行裁剪,因此較小的縮放比例代表被裁剪的邊,較大的則代表在填充進旋轉驗證組件時的真實縮放比例。
- public void createCaptcha() {
- ...
- //根據圖片的原寬度 和 控件寬度 算出縮放比例
- 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)繪制旋轉塊邊框和驗證區域
本步驟首先在Canvas畫布上,根據上一步設置好的繪制軌跡mPath和畫筆mPaint繪制旋轉塊的圓形邊框,這部分不會隨著旋轉而更新。
接著根據拖動條變化的數值調整旋轉塊的旋轉角度。具體旋轉的角度值是mSlider.getProgress() * 360 / 10000 - randomDegree,即算出拖動條當前進度值占最大值的比例,然后乘以360得到按比例變換后的角度值,再減去前面步驟已生成的隨機角度,就可以以(mWidth/2f,mHeight-SLIDER_HEIGHT)即背景圖片中點為旋轉中心進行旋轉;并獲取圖片的 PixelMapHolder,根據路徑裁剪canvas畫布,將其縮放至跟圖片縮放程度一致。
最后判斷若圖片寬的縮放比例和圖片真實縮放比例一樣時,說明寬沒變是在垂直方向上進行了裁剪,則根據比例計算出被裁剪掉的圖片高度,并在畫布上繪制內容;若二者比例不相等,說明寬有變化是在水平方向上進行了裁剪,則根據比例計算出被裁剪掉的圖片寬度,并在畫布上繪制內容。
- public void createCaptcha() {
- ...
- mImage.addDrawTask((component, canvas) -> {//繪制邊框
- canvas.drawPath(mPath, mPaint);
- });
- mLayout.addDrawTask((component, canvas) -> {//繪制驗證碼
- canvas.rotate(mSlider.getProgress() * 360 / 10000 - randomDegree, mWidth / 2f, (mHeight - SLIDER_HEIGHT) / 2f); //根據拖動條的數值調整旋轉角度
- canvas.translate(0, 0);
- PixelMapHolder pixelMapHolder = new PixelMapHolder(pixelMap);//獲取圖片的 PixelMapHolder
- canvas.clipPath(mPath, Canvas.ClipOp.INTERSECT);//根據路徑裁剪canvas
- canvas.scale(ratio, ratio);//畫布縮放至跟圖片縮放程度一致
- if (ratio == ratioWidth) {
- float heightErr = (originHeight * ratio - (mHeight - SLIDER_HEIGHT)) / 2;//根據比例計算出垂直方向上由于 CLIP_CENTER 裁剪掉的圖片的高度
- canvas.drawPixelMapHolder(pixelMapHolder, 0, -heightErr / ratio, mPaint);//繪制內容
- } else {
- float widthErr = (originWidth * ratio - mWidth) / 2;//根據比例計算出水平方向上由于 CLIP_CENTER 裁剪掉的圖片的寬度
- canvas.drawPixelMapHolder(pixelMapHolder, -widthErr / ratio, 0, mPaint);//繪制內容
- }
- });
- }