鸿蒙(Harmony)实现滑块验证码

在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!!)

至此导致流程已结束,当然还有一些细节需要自己根据业务实现。最后完成效果如下:

上一篇:使用C语言连接MySQL


下一篇:小土堆学习笔记17:优化器