在Android和ios两端已经使用的滑块验证码框架还未适配鸿蒙版,于是需要自己去实现类似如下的滑块验证码:
那么实现这样的验证码主要涉及到几个内容:
1、自定义弹窗
2、base64图片转换
3、滑动组件与滑块的联动,以及横移距离转换等
自定义弹窗:
自定义一个可导出的弹窗组件CustomDialog,最主要是使用 @CustomDialog 修饰符。
@CustomDialog
export struct BlockPuzzleDialog {phoneNum: number | string = ''controller: CustomDialogController = new CustomDialogController({builder: BlockPuzzleDialog({}),})build() {Column(){}}// 验证码校验回调给使用页面blockCheckCallback: (token: string) => void = (token: string) => {}
}
在使用页面创建构造器与弹窗绑定
@Entry@Componentstruct LoginPage {dialogController: CustomDialogController = new CustomDialogController({builder: BlockPuzzleDialog({phoneNum: this.phoneNum, blockCheckCallback: (token: string) => {this.blockPuzzleSuccessCallback(token)}}),autoCancel: false,//弹窗是否自动取消alignment: DialogAlignment.Center,// 弹窗位置cornerRadius: 8,width: '90%'// 弹窗宽度})build(){...}}
弹窗UI组件的实现:核心组件就一个预先挖孔的底图上面叠加滑块图片再加上一个slider组件
build(){......Stack() {Image(this.coverUri).width('100%').margin({ top: 10 }).objectFit(ImageFit.Auto).onComplete((event) => {this.scaleRatio = event!!.componentWidth / event?.width!!})Image(this.blockUri).width(this.blockW + "px").height(this.blockH + "px").margin({ top: 10 }).objectFit(ImageFit.Auto).onComplete((event) => {this.blockW = event?.width!! * this.scaleRatiothis.blockH = event?.height!! * this.scaleRatiothis.slideMax = Const.mWidth * 0.9 - 24 - px2vp(this.blockW)}).translate({ x: this.bolckTranslateX + "px" })this.loading()}.width('100%').alignContent(Alignment.Start)RelativeContainer() {Text('向右拖动滑动填充拼图').fontSize(18).fontColor($r('app.color.C_BEBEC6')).id('blockTip').alignRules({"top": {"anchor": "slider","align": VerticalAlign.Top},"bottom": {"anchor": "slider","align": VerticalAlign.Bottom},"left": {"anchor": "slider","align": HorizontalAlign.Start},"right": {"anchor": "slider","align": HorizontalAlign.End},}).textAlign(TextAlign.Center)Slider({style: SliderStyle.InSet,value: $$this.sliderValue,step: 1,max: vp2px(this.slideMax)}).trackColor(this.sliderConfig.trackColor).selectedColor(this.sliderConfig.selectedColor).blockSize({ height: 40, width: 44 }).blockStyle({type: SliderBlockType.IMAGE,image: this.sliderConfig.blockImg})// .sliderInteractionMode(SliderInteraction.SLIDE_ONLY).trackBorderRadius(Const.BORDER_RADIUS_4).trackThickness(40).width('100%').onChange((value: number, mode: SliderChangeMode) => {// this.bolckTranslateX = this.slideMax * (value / this.slideMax)this.bolckTranslateX = valueconsole.info('滑块滑动:滑块滑动数值==' + value + " 图片位移==" + this.bolckTranslateX)if (mode == SliderChangeMode.End) {// this.sliderValue = valuelet point = new Point()point.x = parseFloat((this.bolckTranslateX / this.scaleRatio).toFixed(0))console.info('滑动结束:滑动数值 this.sliderValue==' + this.sliderValue + " this.bolckTranslateX==" +this.bolckTranslateX + " 转像素==" + point.x)this.checkCaptcha(point)}}).id('slider')}.width('100%').height(40).margin({ top: 10 })......}
滑块图片translate的值就是Slider组件的滑动值。使用
this.dialogController.open() 弹窗
Base64图片的下载与转换
aboutToAppear(): void {this.getSlideImage()
}......// 获取底图和滑块图片的base64数据并保存到本地,同时获取到滑块校验相关信息。
getSlideImage() {this.sliderConfig.showLoading = trueHttpUtil.getData<BlockResult>(Const.URL_BLOCK_IMG).then((result) => {if (result !== undefined && result !== null) {this.blockResult = resultthis.coverBase64 = this.blockResult.repData?.originalImageBase64!!this.blockBase64 = this.blockResult.repData?.jigsawImageBase64!!console.info("滑块:获取到base64 ==" + this.coverBase64)let coverName = "coverBase64_" + Date.now().toString() + ".png"let blockName = "blockBase64_" + Date.now().toString() + ".png"this.coverPath = this.context.filesDir + "/temp/" + coverName;this.blockPath = this.context.filesDir + "/temp/" + blockName;this.coverUri =Utils.saveBase64Image(this.coverBase64, this.context, coverName)this.blockUri =Utils.saveBase64Image(this.blockBase64, this.context, blockName)this.sliderConfig.showLoading = falsethis.reset()}})}
可以参考官网示例 通过buffer.from的方法,将base64编码格式的字符串创建为新的Buffer对象,接着用fileIo.writeSync方法将转换好的Buffer对象写入文件。
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir; // data为需要转换的base64字符串,返回沙箱路径uri
export async function writeFile(data: string): Promise<string> { let uri = '' try { let filePath = filesDir + "/1.png"; uri = fileUri.getUriFromPath(filePath); let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); console.info("file fd: " + file.fd); const reg = new RegExp("data:image/\\w+;base64,") const base64 = data.replace(reg, ""); console.log("base64flag", base64) const dataBuffer = buffer.from(base64, 'base64') let writeLen = fileIo.writeSync(file.fd, dataBuffer.buffer); hilog.info(0xA0c0d0,'uri',uri) fileIo.closeSync(file); } catch (Error) { hilog.error(0xA0c0d0,'Error',Error.code) } return uri;
}
当然你还可以直接将Base64转换成PiexlMap.先将base64字符串解析成arraybuffer,然后利用这个arraybuffer构建新PixelMap,需要注意的是,使用decodeSync对base64字符串解码时,传入的base64字符串不能有'data:image/jpeg;base64,'这样的前缀。
import CommonConstants from '../common/constants/CommonContants';
import { util } from '@kit.ArkTS';
import { image } from '@kit.ImageKit';@Entry
@Component
struct Index {@State message: string = 'Base64ToPixelMap';private base64: string = CommonConstants.Image_Base64_String; // 该变量为图片的base64格式字符串 @State private pixelMap: PixelMap | null = null;build() {Row() {Column() {Text(this.message).fontSize(50).fontWeight(FontWeight.Bold).onClick(async () => {let helper = new util.Base64Helper();let buffer: ArrayBuffer = helper.decodeSync(this.base64, util.Type.MIME).buffer as ArrayBuffer;let imageSource = image.createImageSource(buffer);let opts: image.DecodingOptions = { editable: true };this.pixelMap = await imageSource.createPixelMap(opts);})Image(this.pixelMap).width(200).height(200).margin(15)}.width('100%')}.height('100%')}
}
将得到的图片本地保存地址uri或者转换成的piexlMap设置给底图和滑动图片。
滑动值校验
上面已经说过,滑块的移动值就是Slider滑动值。其中slider 步长设置为1,滑动的最大值slideMax=底图的宽度-滑块图片的宽度。这样滑动值转换更方便,联动效果也更好。这里注意下 底图在填满控件的时候有一定的缩放,滑动图片组件也需要按照这个缩放比例设置宽高。
step: 1, max: vp2px(this.slideMax)
最后在slider的onchange回调中校验滑动值是不是正确,注意滑动值要除以上面的底图缩放比例。
将滑动值加上校验token传给校验接口获取校验结果。
.onChange((value: number, mode: SliderChangeMode) => {this.bolckTranslateX = valueconsole.info('滑块滑动:滑块滑动数值==' + value + " 图片位移==" + this.bolckTranslateX)if (mode == SliderChangeMode.End) {// this.sliderValue = valuelet point = new Point()point.x = parseFloat((this.bolckTranslateX / this.scaleRatio).toFixed(0))console.info('滑动结束:滑动数值 this.sliderValue==' + this.sliderValue + " this.bolckTranslateX==" +this.bolckTranslateX + " 转像素==" + point.x)this.checkCaptcha(point)} })
checkFail() {this.sliderConfig.showLoading = falsethis.sliderConfig.trackColor = $r('app.color.C_0DF32222')this.sliderConfig.selectedColor = $r('app.color.C_F32222')this.sliderConfig.blockImg = $r('app.media.drag_btn_error')this.sliderValue = 0this.bolckTranslateX = 0setTimeout(() => {// 删掉滑块图片FileUtil.delFile(this.coverPath)FileUtil.delFile(this.blockPath)this.getSlideImage()}, 300)}checkSuccess() {this.sliderConfig.showLoading = falsethis.sliderConfig.trackColor = $r('app.color.C_0D1264E0')this.sliderConfig.selectedColor = $r('app.color.C_1264E0')this.sliderConfig.blockImg = $r('app.media.drag_btn_success')setTimeout(() => {this.controller.close()// 删掉滑块图片FileUtil.delFile(this.coverPath)FileUtil.delFile(this.blockPath)if (this.blockCheckCallback !== undefined) {this.blockCheckCallback(this.blockResult?.token!!)}}, 300)}
调用刚刚定义的回调方法将校验结果回调给登录页面this.blockCheckCallback(this.blockResult?.token!!)
至此导致流程已结束,当然还有一些细节需要自己根据业务实现。最后完成效果如下: