鸿蒙开发进阶:实现一个属于自己的日期选择器

46 篇文章 3 订阅
12 篇文章 0 订阅

前言

hello 大家好 我是无言。最近在开发一个鸿蒙项目,刚好遇到了一个自定义比较强,样式还挺不错的日期选择器需求,经过不断调试打磨,最终效果还不错,所以打算分享出来,给大家一些创意灵感。

主要实现的功能

  • 选中日期回传给父组件。
  • 根据父组件默认选中时间打开子组件弹窗回显日期。
  • 动态加载日期边滚动边加载后续数据提升性能。

实现效果先一睹为快。

实现过程

一、准备工作
  • 安装好最新DevEco Studio 开发工具,创建一个新的空项目。
  • 为了避免重复造轮子,所以时间处理工具我这里用了[dayjs]。

 

二、先把整个弹窗逻辑实现

  • 创建弹窗子组件文件 ets/components/TimeDateDialog.ets 下面代码建议着重看下添加注释的地方,是容易踩坑的地方,所以我都特别标注出来了。
//弹窗具体内容
@CustomDialog
struct CustomDialogDate {
  controller?: CustomDialogController
  @State dateList:string[]=['2024-01-11','2024-01-12','2024-01-13','2024-01-14','2024-01-15'] //只存储天
  @State hourList:string[]=['01','02','03','04','05'] //只存储小时
  @State minList:string[]=['05','10','15','20','25'] //只存分钟
  cancel: () => void = () => {
  }
  confirm: (data:string) => void = () => {
  }

  aboutToAppear(): void {

  }

  build() {
    Column() {
      Row(){
        Text('显示时间').fontSize(18)
      }.padding({ bottom:25 }).width('100%').justifyContent(FlexAlign.Center)
      // 滚动选中区域
      Flex(){
        // 时间
        Flex(){
          // 时间
          TextPicker({ range: this.dateList})
            .width('50%')
            .canLoop(false)//不循环
            .divider({
              strokeWidth: 1,
              startMargin: 0,
              endMargin: 0,
              color:'#ececec'
            })
            .textStyle({color:'#777777', font: {size: 15, weight: 400}})
            .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
         TextPicker({ range: this.hourList }).width('25%')
            .divider({
              strokeWidth: 1,
              startMargin: 0,
              endMargin: 0,
              color:'#ececec'
            })
            .textStyle({color:'#777777', font: {size: 15, weight: 400}})
            .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})

          //分钟
          TextPicker({ range: this.minList}).width('25%')
            .divider({
              strokeWidth: 1,
              startMargin: 0,
              endMargin: 0,
              color:'#ececec'
            })
            .textStyle({color:'#777777', font: {size:15, weight: 400}})
            .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})

        }
      }
      // 按钮
      Row(){
        Button('取消', { type: ButtonType.Normal, stateEffect: true })
          .margin({right:6})
          .borderRadius(6)
          .backgroundColor('#fff')
          .fontColor('#ffd96868')
          .borderWidth(1)
          .borderColor('#ffd96868')
          .fontSize(15)
          .width(156)
          .onClick(()=>{
            this.controller?.close()
          })
        Button('确定', { type: ButtonType.Normal, stateEffect: true })
          .margin({left:6})
          .borderRadius(6)
          .fontSize(15)
          .backgroundColor('#ffd96868')
          .width(156)
          .onClick(()=>{
             this.confirm('回传信息给父组件')
            this.controller?.close()
          })
      }.justifyContent(FlexAlign.Center)
      .padding({top:15})
      .margin({top:25})
    }.width('100%')
    .padding(16)
    .backgroundColor('#fff')
    .borderRadius({topLeft:15,topRight:15})
  }
}

//定义controller对象 主要作用父子通信 父组件调用子组件方法 唤醒弹窗
export  class DialogDateController {
  ShowDialog = (value?: string) => {
  }
}

//弹窗控制逻辑
@Component
export  struct TimeDateDialog { //修改命名 注意前面加了 export 需要暴露组件
  private controller: DialogDateController = new DialogDateController();
  CustomDialogController: CustomDialogController | null =null ;
  @Prop defaultTime: string;
  cancel?: () => void
  confirm?: (data:string) => void = () => {
  }
  // 打开显示弹窗
  private async  ShowDialog(value?: string){
    this.CustomDialogController?.open()
  }
  aboutToAppear(): void {
    if (this.controller) {
      //给controller对应的方法赋值
      this.controller.ShowDialog = this.ShowDialog.bind(this); //这里需要注意 用了  bind改变 this 指向
    }
    this.CustomDialogController=  new CustomDialogController({
      builder: CustomDialogDate({
        cancel: this.cancel,
        confirm: this.confirm,
      }),
      autoCancel: true,
      onWillDismiss:(dismissDialogAction: DismissDialogAction)=> {
        if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
          dismissDialogAction.dismiss()
        }
        if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {
          dismissDialogAction.dismiss()
        }
      },
      alignment: DialogAlignment.Bottom,
      offset: { dx: 0, dy:  0},
      customStyle: true,//这里为true 样式才可以完全自定义
    })
  }
  aboutToDisappear() {
    this.CustomDialogController = null // 将dialogController置空
  }


  build() { //因为用的 自定义弹窗功能,所以这下面可以为空

  }
}
  • 在父组件 ets/page/Index.ets 文件中引入子组件弹窗并唤醒弹窗,确定后回传信息给父组件。

import  {TimeDateDialog,DialogDateController} from "../components/TimeDateDialog"

@Entry
@Component
struct Index {
  @State DateDialogRef: DialogDateController = new DialogDateController();//用于调用子组件的方法 唤醒弹窗

  build() {
    RelativeContainer() {
     Column(){

       Button('打开弹窗')
         .onClick(()=>{
           this.DateDialogRef.ShowDialog()
         })
     }.padding(20)

      TimeDateDialog({
        controller:this.DateDialogRef,
        confirm:(data)=>{
          console.log('data',data)
        }
      })
    }
    .height('100%')
    .width('100%')
  }
}

 预览可以看到父组件能够成功唤醒弹窗,并且子组件点击确定后,成功回传信息到了父组件。

三、动态生成日期

这里我们先默认动态生成当前日期前后20天的数据,核心方法用到了 dayjsadd 增加方法 和 subtract减少方法和数组的 push方法 和 unshift方法。

修改TimeDateDialog.ets文件 下面只列出了修改部分其余的省略保持一致。

import dayjs from "dayjs"
import 'dayjs/locale/zh-cn'; // 引入中文本地化
import localeData from 'dayjs/plugin/localeData';
dayjs.extend(localeData);
dayjs.locale('zh-cn'); // 使用中文

@CustomDialog
struct CustomDialogDate {
  controller?: CustomDialogController
  dateMap:Map<string,string>= new Map();//键值对存储日期信息
  @State dateList:string[]=[] //只存储天
  @State hourList:string[]=[] //只存储小时
  @State minList:string[]=[] //只存分钟
  @State dayStr: string=''; //存储日期选中的信息
  
  cancel: () => void = () => {
  }
  confirm: (data:string) => void = () => {
  }

  aboutToAppear(): void {
    this.initDate(); //初始化日期
    this.initHour(); //初始化小时
    this.initMin(); //初始化分钟
  }
  //初始化日期
  initDate(){
    // 获取后续10天的时间
    this.changeDateList('add')
    this.changeDateList('subtract')
  }
  // 处理数据 type: add 新增 subtract减少
  changeDateList(type:string,time?:string){
    for (let i = 0; i < 20; i++) {
      // 这里需要 注意 subtract 减少 是i+1否则数据有一天是重复的
      const futureDay =type=='add'? dayjs(time).add(i, 'day'):dayjs(time).subtract(i+1, 'day');
      const nowDay = dayjs().format("YYYY-MM-DD") //今天
      const day = futureDay.format("YYYY-MM-DD")
      let title = futureDay.format('M月D日 dddd')
      if (nowDay == day) {
        title = '今天'
        this.dayStr = title
      }
      this.dateMap.set(title, day)
      if(type=='add'){ //添加以后的时间
        this.dateList.push(title)
      }else{ //添加 以前的时间
        this.dateList.unshift(title)
      }
    }
  }
  // 时间
  initMin(){
    for (let i = 0; i < 12; i++) {
      const str = this.padZero(i*5)
      this.minList.push(str)
    }
  }
  // 小时
  initHour(){
    for (let i = 0; i < 24; i++) {
      const str = this.padZero(i)
      this.hourList.push(str)
    }
  }
  //补充 0
  padZero(num:number) {
    return num.toString().padStart(2, '0');
  }
  ... 

 重新允许打开弹窗可以看到日期已经是动态生成 而且已经汉化 并且有了星期信息。

四、滚动加载更多数据

核心逻辑当往下滚动到倒数第10条数据的时候利用push从日期数组尾部添加20条以后的数据,当往上滚动到第10条数据的时候 利用 unshift 从日期数组头部添加 20条以前的数据,这里需要注意从头部添加完数据之后,需要 修改日期选中索引selected值+20。因为现在默认从今日开始前后20条数据所以this.DateSelect=20 默认选中当天。

修改TimeDateDialog.ets文件 下面只列出了修改部分其余的省略保持一致。

...
//初始化日期
initDate(){
  // 获取后续10天的时间
  this.changeDateList('add')
  this.changeDateList('subtract')
  this.DateSelect=20//默认选中当天
}
...
getTopName(){
  const date =this.dateMap.get(this.dayStr)
  return  dayjs(date).format('YYYY年M月D日 dddd')
}
build() {
  Column() {
    Row(){
      Text(this.getTopName()).fontSize(18)
    }.padding({ bottom:25 }).width('100%').justifyContent(FlexAlign.Center)
    // 滚动选中区域
    Flex(){
      // 时间
      TextPicker({ range: this.dateList, selected: this.DateSelect})
        .width('50%')
        .canLoop(false)//不循环
        .divider({
          strokeWidth: 1,
          startMargin: 0,
          endMargin: 0,
          color:'#ececec'
        })
        .textStyle({color:'#777777', font: {size: 15, weight: 400}})
        .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
        .onChange((value: string | string[], index: number | number[]) => {
          this.dayStr=value as string
          if(index==(this.dateList.length-10)){ //当滚动到倒数第10个时开始加载后续20天的数据
            this.DateSelect=index as number
            const dayStr = this.dateList[this.dateList.length-1]
            const day= this.dateMap.get(dayStr) as string
            this.changeDateList('add',day) //添加以后的时间
          }
          if(index==10){ //当滚动第10个时开始加载前20天的数据
            const dayStr = this.dateList[0]
            const day= this.dateMap.get(dayStr) as string
            this.changeDateList('subtract',day) //添加以前的时间
            this.DateSelect=index as number +20
          }
        })
      //小时
      TextPicker({ range: this.hourList, selected: this.HourSelect  }).width('25%')
        .divider({
          strokeWidth: 1,
          startMargin: 0,
          endMargin: 0,
          color:'#ececec'
        })
        .textStyle({color:'#777777', font: {size: 15, weight: 400}})
        .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
        .onChange((value: string | string[], index: number | number[]) => {
          this.HourSelect=index as number
        })
      //分钟
      TextPicker({ range: this.minList, selected: this.minSelect  }).width('25%')
        .divider({
          strokeWidth: 1,
          startMargin: 0,
          endMargin: 0,
          color:'#ececec'
        })
        .textStyle({color:'#777777', font: {size:15, weight: 400}})
        .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
        .onChange((value: string | string[], index: number | number[]) => {
          this.minSelect=index as number
        })
    }
    // 按钮
    Row(){
      Button('取消', { type: ButtonType.Normal, stateEffect: true })
        .margin({right:6})
        .borderRadius(6)
        .backgroundColor('#fff')
        .fontColor('#ffd96868')
        .borderWidth(1)
        .borderColor('#ffd96868')
        .fontSize(15)
        .width(156)
        .onClick(()=>{
          this.controller?.close()
        })
      Button('确定', { type: ButtonType.Normal, stateEffect: true })
        .margin({left:6})
        .borderRadius(6)
        .fontSize(15)
        .backgroundColor('#ffd96868')
        .width(156)
        .onClick(()=>{
          this.confirm('回传信息')
          this.controller?.close()
        })
    }.justifyContent(FlexAlign.Center)
    .padding({top:15})
    .margin({top:25})
  }.width('100%')
  .padding(16)
  .backgroundColor('#fff')
  .borderRadius({topLeft:15,topRight:15})
}
...

 看看运行效果,可以看到日期可以无限加载滚动了。

五、回传信息给父元素

点击确定按钮提交信息回传给父元素

...
// 提交数据
onSubmit(){
  const day = this.dateMap.get(this.dayStr)
  const hour = this.hourList[this.HourSelect]
  const min = this.minList[this.minSelect]
  const dateTime  = `${day} ${hour}:${min}:00`
  this.confirm(dateTime) //回传日期到父组件
  this.controller?.close()

}
...

六、回显默认值

最后ets/components/TimeDateDialog.ets 完整代码如下

import dayjs from "dayjs"
import 'dayjs/locale/zh-cn'; // 引入中文本地化
import localeData from 'dayjs/plugin/localeData';
dayjs.extend(localeData);
dayjs.locale('zh-cn'); // 使用中文



@CustomDialog
struct CustomDialogDate {
  controller?: CustomDialogController
  dateMap:Map<string,string>= new Map();//键值对存储日期信息
  @State dateList:string[]=[] //只存储天
  @State hourList:string[]=[] //只存储小时
  @State minList:string[]=[] //只存分钟
  @Prop defaultTime: string; //默认值
  @State dayStr: string=''; //存储日期选中的信息
  @State DateSelect: number=0;
  @State HourSelect: number=0;
  @State minSelect: number=0;

  cancel: () => void = () => {
  }
  confirm: (data:string) => void = () => {
  }
  // 提交数据
  onSubmit(){
    const day = this.dateMap.get(this.dayStr)
    const hour = this.hourList[this.HourSelect]
    const min = this.minList[this.minSelect]
    const dateTime  = `${day} ${hour}:${min}:00`

    this.confirm(dateTime) //回传日期到父组件
    this.controller?.close()

  }

  aboutToAppear(): void {
    this.initDate(); //初始化日期
    this.initHour(); //初始化小时
    this.initMin(); //初始化分钟
    this.initDefaultTime()
  }
  // 初始化默认选中
  initDefaultTime(){
    if(!this.defaultTime) {
      this.DateSelect=20//如果没有默认时间可以默认选中当天
      return
    }
    const nowDay = dayjs().format("YYYY-MM-DD") //今天
    const day = dayjs(this.defaultTime).format("YYYY-MM-DD") //
    const hour = dayjs(this.defaultTime).format("HH") //小时
    const min = dayjs(this.defaultTime).format("mm") //分钟
    let dayStr = dayjs(this.defaultTime).format("M月D日 dddd")
    if(nowDay==day){
      dayStr='今天'
    }
    const dayIndex =this.dateList.indexOf(dayStr)
    if(dayIndex>-1 ){
      this.dayStr=dayStr
      this.DateSelect=dayIndex
    }
    this.HourSelect = this.hourList.indexOf(hour) //选中 小时
    this.minSelect = this.minList.indexOf(min) //选中 小时
  }
  //初始化日期
  initDate(){
    // 获取后续10天的时间
    this.changeDateList('add',this.defaultTime)
    this.changeDateList('subtract',this.defaultTime)
  }
  // 处理数据 type: add 新增 subtract减少
  changeDateList(type:string,time?:string){
    for (let i = 0; i < 20; i++) {
      // 这里需要 注意 subtract 减少 是i+1否则数据有一天是重复的
      const futureDay =type=='add'? dayjs(time).add(i, 'day'):dayjs(time).subtract(i+1, 'day');
      const nowDay = dayjs().format("YYYY-MM-DD") //今天
      const day = futureDay.format("YYYY-MM-DD")
      let title = futureDay.format('M月D日 dddd')
      if (nowDay == day) {
        title = '今天'
        this.dayStr = title
      }
      this.dateMap.set(title, day)
      if(type=='add'){ //添加以后的时间
        this.dateList.push(title)
      }else{ //添加 以前的时间
        this.dateList.unshift(title)
      }
    }
  }
  // 时间
  initMin(){
    for (let i = 0; i < 12; i++) {
      const str = this.padZero(i*5)
      this.minList.push(str)
    }
  }
  // 小时
  initHour(){
    for (let i = 0; i < 24; i++) {
      const str = this.padZero(i)
      this.hourList.push(str)
    }
  }
  //补充 0
  padZero(num:number) {
    return num.toString().padStart(2, '0');
  }
  getTopName(){
    const date =this.dateMap.get(this.dayStr)
    return  dayjs(date).format('YYYY年M月D日 dddd')
  }
  build() {
    Column() {
      Row(){
        Text(this.getTopName()).fontSize(18)
      }.padding({ bottom:25 }).width('100%').justifyContent(FlexAlign.Center)
      // 滚动选中区域
      Flex(){
        // 时间
        TextPicker({ range: this.dateList, selected: this.DateSelect})
          .width('50%')
          .canLoop(false)//不循环
          .divider({
            strokeWidth: 1,
            startMargin: 0,
            endMargin: 0,
            color:'#ececec'
          })
          .textStyle({color:'#777777', font: {size: 15, weight: 400}})
          .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
          .onChange((value: string | string[], index: number | number[]) => {
            this.dayStr=value as string
            if(index==(this.dateList.length-10)){ //当滚动到倒数第10个时开始加载后续20天的数据
              this.DateSelect=index as number
              const dayStr = this.dateList[this.dateList.length-1]
              const day= this.dateMap.get(dayStr) as string
              this.changeDateList('add',day) //添加以后的时间
            }
            if(index==10){ //当滚动第10个时开始加载前20天的数据
              const dayStr = this.dateList[0]
              const day= this.dateMap.get(dayStr) as string
              this.changeDateList('subtract',day) //添加以前的时间
              this.DateSelect=index as number +20
            }
          })
        //小时
        TextPicker({ range: this.hourList, selected: this.HourSelect  }).width('25%')
          .divider({
            strokeWidth: 1,
            startMargin: 0,
            endMargin: 0,
            color:'#ececec'
          })
          .textStyle({color:'#777777', font: {size: 15, weight: 400}})
          .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
          .onChange((value: string | string[], index: number | number[]) => {
            this.HourSelect=index as number
          })
        //分钟
        TextPicker({ range: this.minList, selected: this.minSelect  }).width('25%')
          .divider({
            strokeWidth: 1,
            startMargin: 0,
            endMargin: 0,
            color:'#ececec'
          })
          .textStyle({color:'#777777', font: {size:15, weight: 400}})
          .selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
          .onChange((value: string | string[], index: number | number[]) => {
            this.minSelect=index as number
          })
      }
      // 按钮
      Row(){
        Button('取消', { type: ButtonType.Normal, stateEffect: true })
          .margin({right:6})
          .borderRadius(6)
          .backgroundColor('#fff')
          .fontColor('#ffd96868')
          .borderWidth(1)
          .borderColor('#ffd96868')
          .fontSize(15)
          .width(156)
          .onClick(()=>{
            this.controller?.close()
          })
        Button('确定', { type: ButtonType.Normal, stateEffect: true })
          .margin({left:6})
          .borderRadius(6)
          .fontSize(15)
          .backgroundColor('#ffd96868')
          .width(156)
          .onClick(()=>{
            this.onSubmit()
            this.controller?.close()
          })
      }.justifyContent(FlexAlign.Center)
      .padding({top:15})
      .margin({top:25})
    }.width('100%')
    .padding(16)
    .backgroundColor('#fff')
    .borderRadius({topLeft:15,topRight:15})
  }
}

//定义controller对象 主要作用父子通信 父组件调用子组件方法 唤醒弹窗
export  class DialogDateController {
  ShowDialog = (value?: string) => {
  }
}

// @Entry  去掉入口页面标志
@Component
export  struct TimeDateDialog { //修改命名 注意前面加了 export 需要暴露组件
  private controller: DialogDateController = new DialogDateController();
  CustomDialogController: CustomDialogController | null =null ;
  @Prop defaultTime: string;
  cancel?: () => void
  confirm?: (data:string) => void = () => {
  }
  // 打开显示弹窗
  private async  ShowDialog(value?: string){
    this.CustomDialogController?.open()
  }
  aboutToAppear(): void {
    if (this.controller) {
      //给controller对应的方法赋值
      this.controller.ShowDialog = this.ShowDialog.bind(this); //这里需要注意 用了  bind改变 this 指向
    }
    this.CustomDialogController=  new CustomDialogController({
      builder: CustomDialogDate({
        cancel: this.cancel,
        confirm: this.confirm,
        defaultTime:this.defaultTime
      }),
      autoCancel: true,
      onWillDismiss:(dismissDialogAction: DismissDialogAction)=> {
        if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
          dismissDialogAction.dismiss()
        }
        if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {
          dismissDialogAction.dismiss()
        }
      },
      alignment: DialogAlignment.Bottom,
      offset: { dx: 0, dy:  0},
      customStyle: true,
    })
  }
  aboutToDisappear() {
    this.CustomDialogController = null // 将dialogController置空
  }


  build() { //因为用的 自定义弹窗功能,所以这下面可以为空

  }
}

 重新打开预览可以看到默认值已经成功回显

总结

本文详细介绍了关于在华为鸿蒙系统 去实现一个自定义日期选中弹窗的详细教程,其实关于日期选中的逻辑是相通的,不仅仅是用于鸿蒙中,其实在其他项目中也可以以相同的逻辑实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值