在HarmonyOS NEXT开发中月视图模式的日历组件参考?

在HarmonyOS NEXT开发中月视图模式的日历组件参考?
可以切换上一月(的一号)/下一月/切换到本月的今天 停留在某月时,可以切换选择日期。

阅读 1.2k
avatarAI BotBETA

在HarmonyOS NEXT中实现月视图日历组件可以通过ArkUI自定义组件实现,以下是核心实现步骤(基于API 9+):

  1. 状态管理

    @State currentDate: Date = new Date() // 当前展示月份
    @State selectedDate: Date = null // 选中的日期
  2. 月份切换逻辑

    // 切换月份
    changeMonth(offset: number) {
      const newDate = new Date(this.currentDate)
      newDate.setMonth(this.currentDate.getMonth() + offset)
      this.currentDate = newDate
    }
    
    // 回到今日
    backToToday() {
      this.currentDate = new Date()
      this.selectedDate = new Date()
    }
  3. 日期渲染核心方法

    // 生成月份日期数组
    generateMonthDays(): Date[][] {
      const weeks = []
      const year = this.currentDate.getFullYear()
      const month = this.currentDate.getMonth()
      
      // 计算当月第一天是周几
      const firstDay = new Date(year, month, 1)
      const startDay = firstDay.getDay()
    
      // 生成日期网格
      let day = 1 - startDay
      for (let i = 0; i < 6; i++) {
     const week = []
     for (let j = 0; j < 7; j++) {
       const date = new Date(year, month, day++)
       week.push(date)
     }
     weeks.push(week)
      }
      return weeks
    }
  4. UI布局示例

    Column() {
      // 月份切换工具栏
      Row() {
     Button('←').onClick(() => this.changeMonth(-1))
     Text(formatMonth(this.currentDate))
     Button('→').onClick(() => this.changeMonth(1))
     Button('Today').onClick(() => this.backToToday())
      }
    
      // 周标题
      Grid() {
     ForEach(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], (day) => {
       Text(day)
     })
      }
    
      // 日期网格
      Grid() {
     ForEach(this.generateMonthDays(), (week) => {
       ForEach(week, (date) => {
         CalendarDayCell({
           date: date,
           currentMonth: date.getMonth() === this.currentDate.getMonth(),
           isToday: isSameDay(date, new Date()),
           isSelected: isSameDay(date, this.selectedDate)
         })
       })
     })
      }
    }
  5. 日期单元格组件

    @Component
    struct CalendarDayCell {
      private date: Date
      private currentMonth: boolean
      private isToday: boolean
      private isSelected: boolean
    
      build() {
     Text(this.date.getDate().toString())
       .backgroundColor(this.isSelected ? Color.Blue : Color.Transparent)
       .textColor(this.currentMonth ? Color.Black : Color.Gray)
       .border(this.isToday ? { width: 1, color: Color.Red } : null)
       .onClick(() => {
         // 触发日期选择事件
         this.selectedDate = this.date
       })
      }
    }

关键点说明

  1. 使用Grid布局构建6x7的日期矩阵
  2. 通过Date对象处理日期计算逻辑
  3. 样式区分:

    • 灰色显示非当月日期
    • 红色边框标记今日
    • 蓝色背景标记选中日期
  4. 日期切换时自动重新生成日期矩阵

扩展功能建议

  • 添加@Watch监听日期变化触发事件
  • 集成多语言支持的周标题
  • 添加日期范围选择功能
  • 实现滑动切换月份的手势支持

如果需要现成组件,建议查看官方文档中是否更新了Calendar组件(当前HarmonyOS 4.0尚未提供标准日历组件,需要自行实现)。

2 个回答

解决方案如下:

// import { Lunar } from 'lunar-javascript' 
import dateClass from '../beans/DateClass' 
import dayjs from 'dayjs' 
 
@Entry 
@Component 
struct DatePickerDialogExample01 { 
  weekList: Array<string> = ['一', '二', '三', '四', '五', '六', '日'] 
  @State show: boolean = false 
  @State selectDate: dayjs.Dayjs = dayjs() 
  @State selectMonth: string = dayjs().format('YYYY年MM月') 
  @State dataList: Array<dateClass> = [] 
  @State monthParam: number = 0 //切换日历的参数 
 
  build() { 
    Column() { 
      Button('弹出或关闭日历') 
        .height(36) 
        .fontSize(20) 
        .fontColor(Color.White) 
        .onClick(() => { 
          this.show = !this.show 
          this.selectDate = dayjs() 
          this.monthParam = 0 
          this.getDataList(this.monthParam) 
        }) 
 
      if (this.show) { 
        Row() { 
          Text(this.selectMonth) 
            .fontSize(16) 
            .fontColor(Color.White) 
          Row() { 
            Row() { 
              Image($r('app.media.startIcon')) 
                .width(24) 
                .height(12) 
                .backgroundColor(Color.Black) 
            } 
            .height('100%') 
            .width(48) 
            .alignItems(VerticalAlign.Center) 
            .justifyContent(FlexAlign.Center) 
            .onClick(() => { 
              this.monthParam-- 
              this.getDataList(this.monthParam) 
            }) 
 
            Row() { 
              Image($r('app.media.startIcon')) 
                .width(24) 
                .height(12) 
                .backgroundColor(Color.Black) 
            } 
            .height('100%') 
            .width(48) 
            .alignItems(VerticalAlign.Center) 
            .justifyContent(FlexAlign.Center) 
            .onClick(() => { 
              this.monthParam++ 
              this.getDataList(this.monthParam) 
            }) 
          } 
        } 
        .height(48) 
        .width('100%') 
        .justifyContent(FlexAlign.SpaceBetween) 
 
        Row() { 
          ForEach(this.weekList, (item: string) => { 
            Text(item) 
              .fontSize(16) 
            Row() { 
              ForEach(this.weekList, (item: string) => { 
                Text(item) 
                  .fontSize(16) 
                  .fontColor(Color.White) 
                  .width(48) 
                  .textAlign(TextAlign.Center) 
              }) 
            } 
            .width('100%') 
 
            Grid() { 
              ForEach(this.dataList, (item: dateClass) => { 
                GridItem() { 
                  Column() { 
                    Text(item.day.toString()) 
                      .fontSize(16) 
                      .fontColor(Color.White) 
                      .width(48) 
                      .textAlign(TextAlign.Center) 
                      .opacity(item.isToMonth ? 1 : 0.4) 
                    Text(item.lunarDay) 
                      .fontSize(12) 
                      .fontColor(Color.White) 
                      .width(48) 
                      .textAlign(TextAlign.Center) 
                      .opacity(item.isToMonth ? 1 : 0.4) 
                  } 
                  .width(48) 
                  .height(48) 
                  .alignItems(HorizontalAlign.Center) 
                  .justifyContent(FlexAlign.Center) 
                  .backgroundColor(item.dayjsObj.format('YYYY-MM-DD') == 
                  dayjs().format('YYYY-MM-DD') ? 
                    '#007DFF' : Color.Black) 
                  .borderWidth(1) 
                  .borderColor(item.dayjsObj.format('YYYY-MM-DD') == 
                  this.selectDate.format('YYYY-MM-DD') ? 
                    '#007DFF' : Color.Black) 
                  .onClick(() => { 
                    this.selectDate = item.dayjsObj 
                  }) 
                } 
              }) 
            } 
            .height(288) 
            .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') 
            .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr') 
            .columnsGap(0) 
            .rowsGap(0) 
          } 
        } 
        .width('100%') 
        .height('100%') 
        .backgroundColor(Color.Black) 
        .justifyContent(FlexAlign.Center) 
        .padding({ left: 12, right: 12 }) 
      } 
      getDataList(param: number) { 
        this.dataList = [] 
        let firstDate = dayjs().add(param, 'month').startOf('month') // 当月的第一天 
        let afterDate = dayjs().add(param, 'month').endOf('month') // 当月的最后一天 
        this.selectMonth = firstDate.format('YYYY年MM月') 
        let frontDay = 0 // 显示日历的第一天需要向前的天数 
        if (firstDate.day() == 0) { 
          frontDay = 6 
        } else { 
          frontDay = firstDate.day() - 1 
        } 
        let showFirstDay = firstDate.subtract(frontDay, 'day') // 显示日历的第一天 
        for (let i = 0;i < 42; i++) { 
          let dayjsObj = showFirstDay.add(i, 'day') 
          let day = dayjsObj.date() 
          let lunarDay = '' 
          // if (Lunar.fromDate(dayjsObj.toDate()).getFestivals().length !== 0) { // 显示节日 
          // lunarDay = Lunar.fromDate(dayjsObj.toDate()).getFestivals()[0] 
          // } else if (Lunar.fromDate(dayjsObj.toDate()).getJieQi() !== '') { // 显示节气 
          // lunarDay = Lunar.fromDate(dayjsObj.toDate()).getJieQi() 
          // } else { // 显示农历日期 
          // lunarDay = Lunar.fromDate(dayjsObj.toDate()).getDayInChinese() 
          // } 
          let isToMonth = true 
          if (dayjsObj.isBefore(firstDate) || dayjsObj.isAfter(afterDate)) { 
            isToMonth = false 
          } 
          this.dataList.push(new dateClass(dayjsObj, day, lunarDay, isToMonth)) 
        } 
        // console.log('aboutToAppear',JSON.stringify(this.dataList)) 
      } 
 
      aboutToAppear() { 
        this.getDataList(this.monthParam) 
      } 
    } 
    // @ts-ignore 
    import dayjs from 'dayjs' 
 
    export default class DateClass { 
      dayjsObj: dayjs.Dayjs // dayjs时间 
      day: number // 天 
      lunarDay: string // 农历 
      isToMonth: boolean // 是否当前月 
 
      constructor(dayjsObj: dayjs.Dayjs, day: number, lunarDay: string, isToMonth: boolean) { 
        this.dayjsObj = dayjsObj 
        this.day = day 
        this.lunarDay = lunarDay 
        this.isToMonth = isToMonth 
      } 
    }

自定义日历组件CustomCalendar。这里参考日历三方库@xsqd/calendar的部分源码使用两个ForEach循环实现日历的月视图和周视图的日期布局效果。通过CalendarViewType条件渲染对应的月视图或周视图。年视图使用Canvas绘制显示年视图中每个月。使用OffscreenCanvasRenderingContext2D在Canvas上进行离屏绘制(主要使用fillText绘制月份,星期,日期等文本数据),它会将需要绘制的内容先绘制在缓存区,然后使用transferToImageBitmap将其转换成图片,一次性绘制到canvas上,以加快绘制速度。
案例完整代码,可以参考链接:https://gitee.com/harmonyos-cases/cases/tree/master/CommonApp...