在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
@Component
struct 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.scaleRatio
this.blockH = event?.height!! * this.scaleRatio
this.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 = value
console.info('滑块滑动:滑块滑动数值==' + value + " 图片位移==" + this.bolckTranslateX)
if (mode == SliderChangeMode.End) {
// this.sliderValue = value
let 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 = true
HttpUtil.getData<BlockResult>(Const.URL_BLOCK_IMG).then((result) => {
if (result !== undefined && result !== null) {
this.blockResult = result
this.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 = false
this.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 = value console.info('滑块滑动:滑块滑动数值==' + value + " 图片位移==" + this.bolckTranslateX) if (mode == SliderChangeMode.End) { // this.sliderValue = value let 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 = false
this.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 = 0
this.bolckTranslateX = 0
setTimeout(() => {
// 删掉滑块图片
FileUtil.delFile(this.coverPath)
FileUtil.delFile(this.blockPath)
this.getSlideImage()
}, 300)
}
checkSuccess() {
this.sliderConfig.showLoading = false
this.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!!)
至此导致流程已结束,当然还有一些细节需要自己根据业务实现。最后完成效果如下: