鸿蒙Harmony-Next 徒手撸一个日历控件

news2024/11/14 14:54:00

本文将介绍如何使用鸿蒙Harmony-Next框架实现一个自定义的日历控件。我们将创建一个名为CalendarView的组件(注意,这里不能叫 Calendar因为系统的日历叫这个),它具有以下功能:

  • 显示当前月份的日历
  • 支持选择日期
  • 显示农历日期
  • 可以切换上一月和下一月

组件结构

我们的CalendarView组件主要由以下部分组成:

  1. 月份导航栏
  2. 星期标题
  3. 日期网格

实现代码

@Component
export struct CalendarView {
  // 组件状态
  @State selectedDate: Date = new Date()
  @State isDateSelected: boolean = false
  @State currentMonth: number = new Date().getMonth()
  @State currentYear: number = new Date().getFullYear()

  build() {
    Column() {
      // 月份导航栏
      Row() {
        // ... 月份切换和显示逻辑 ...
      }
      
      // 星期标题
      Row() {
        // ... 星期标题显示逻辑 ...
      }
      
      // 日期网格
      Grid() {
        // ... 日期显示和选择逻辑 ...
      }
    }
  }

  // ... 其他辅助方法 ...
}

关键功能实现

1. 月份切换

通过onMonthChange方法实现月份的切换:

private onMonthChange(increment: number) {
  // ... 月份切换逻辑 ...
}

2. 日期选择

使用onDateSelected方法处理日期选择:

private onDateSelected(day: number) {
  // ... 日期选择逻辑 ...
}

3. 农历日期显示

利用LunarDate类来计算和显示农历日期:

private getLunarDate(day: number): string {
  return LunarDate.solarToLunar(this.currentYear, this.currentMonth + 1, day);
}

4. 日期颜色处理

根据日期状态(过去、当前、选中)设置不同的颜色:

private getDateColor(day: number): string {
  // ... 日期颜色逻辑 ...
}

农历日期转换(LunarDate农历算法实现)

LunarDate类来实现公历到农历的转换,本算法实现主要依赖于预先编码的农历数据和巧妙的位运算 , 优点是计算速度快,代码相对简洁。但缺点是依赖于预先编码的数据,如果需要扩展到1900年之前或2100年之后,就需要额外的数据,及算法上的调整。这个类包含了大量的位运算, 主要方法包括:

  • solarToLunar: 将公历日期转换为农历日期
  • getLunarYearDays: 计算农历年的总天数
  • getLeapMonth: 获取闰月
  • getLeapDays: 获取闰月的天数
  • getLunarMonthDays: 获取农历月的天数

1. 农历数据编码

private static lunarInfo: number[] = [
  0x04bd8, 0x04ae0, 0x0a570, /* ... 更多数据 ... */
];

这个数组包含了从1900年到2100年的农历数据编码。每个数字都是一个16位的二进制数,包含了该年的闰月、大小月等信息。

  • 最后4位: 表示闰月的月份,为0则表示没有闰月。
  • 中间12位: 分别代表12个月,为1表示大月(30天),为0表示小月(29天)。
  • 最高位: 闰月是大月还是小月,仅当存在闰月时有意义。

2. 公历转农历的核心算法

static solarToLunar(year: number, month: number, day: number): string {
  // ... 前置检查代码 ...

  let offset = Math.floor((objDate.getTime() - baseDate.getTime()) / 86400000);

  // 1. 计算农历年
  for (i = 1900; i < 2101 && offset > 0; i++) {
    temp = LunarDate.getLunarYearDays(i);
    offset -= temp;
  }
  const lunarYear = i - 1;

  // 2. 计算闰月
  leap = LunarDate.getLeapMonth(lunarYear);
  isLeap = false;

  // 3. 计算农历月和日
  for (i = 1; i < 13 && offset > 0; i++) {
    // ... 月份计算逻辑 ...
  }
  const lunarMonth = i;
  const lunarDay = offset + 1;

  // 4. 转换为农历文字表示
  return dayStr === '初一' ? monthStr + "月" : dayStr;
}

主要步骤是:

  1. 计算从1900年1月31日(农历1900年正月初一)到目标日期的总天数。
  2. 逐年递减这个天数,确定农历年份。
  3. 确定该年是否有闰月,以及闰月的位置。
  4. 逐月递减剩余天数,确定农历月份和日期。
  5. 将数字转换为对应的农历文字表示。

3. 辅助方法

获取农历年的总天数

private static getLunarYearDays(year: number): number {
  let i = 0, sum = 348;
  for (i = 0x8000; i > 0x8; i >>= 1) {
    sum += (LunarDate.lunarInfo[year - 1900] & i) ? 1 : 0;
  }
  return sum + LunarDate.getLeapDays(year);
}

这个方法通过位运算来计算一年中每个月的天数,再加上闰月的天数(如果有的话)。

获取闰月信息

private static getLeapMonth(year: number): number {
  return LunarDate.lunarInfo[year - 1900] & 0xf;
}

private static getLeapDays(year: number): number {
  if (LunarDate.getLeapMonth(year)) {
    return (LunarDate.lunarInfo[year - 1900] & 0x10000) ? 30 : 29;
  }
  return 0;
}

这些方法用于确定某一年是否有闰月,以及闰月的具体月份和天数。

获取农历月的天数

private static getLunarMonthDays(year: number, month: number): number {
  return (LunarDate.lunarInfo[year - 1900] & (0x10000 >> month)) ? 30 : 29;
}

这个方法通过位运算来确定某个农历月是大月(30天)还是小月(29天)。

完整的代码如下:

@Component
export struct CalendarView {

  @State selectedDate: Date = new Date()
  @State isDateSelected: boolean = false
  @State currentMonth: number = new Date().getMonth()
  @State currentYear: number = new Date().getFullYear()

  build() {
    Column() {
      Row() {
        Text('上一月')
          .fontColor('#165dff')
          .decoration({
            type: TextDecorationType.Underline,
            color: '#165dff'
          })
          .onClick(() => this.onMonthChange(-1))
        Text(`${this.currentYear}${this.currentMonth + 1}`)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ left: 15, right: 15 })
        Text('下一月')
          .fontColor('#165dff')
          .decoration({
            type: TextDecorationType.Underline,
            color: '#165dff'
          })
          .onClick(() => this.onMonthChange(1))
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({ top: 20, bottom: 30 })

      // 星期标题
      Row() {
        ForEach(['日', '一', '二', '三', '四', '五', '六'], (day: string) => {
          Text(day)
            .width('14%')
            .textAlign(TextAlign.Center)
            .fontSize(18)
            .fontColor('#999999')
        }, (day: string) => day)
      }
      .margin({ bottom: 10 })

      Grid() {
        ForEach(this.getDaysInMonth(), (day: number) => {
          GridItem() {
            Column() {
              Text(day.toString())
                .fontSize(18)
                .fontWeight(this.isSelectedDate(day) ? FontWeight.Bold : FontWeight.Normal)
                .fontColor(this.getDateColor(day))
              Text(this.getLunarDate(day))
                .fontSize(12)
                .fontColor(this.getDateColor(day))
            }
            .width('100%')
            .height('100%')
            .borderRadius(25)
            .backgroundColor(this.isSelectedDate(day) ? '#007DFF' : Color.Transparent)
            .justifyContent(FlexAlign.Center)
          }
          .aspectRatio(1)
          .onClick(() => this.onDateSelected(day))
        }, (day: number) => day.toString())
      }
      .width('100%')
      .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
      .rowsGap(8)
      .columnsGap(8)
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 16, bottom: 16 })
    .backgroundColor('#F5F5F5')
  }

  private onMonthChange(increment: number) {
    let newMonth = this.currentMonth + increment
    let newYear = this.currentYear

    if (newMonth > 11) {
      newMonth = 0
      newYear++
    } else if (newMonth < 0) {
      newMonth = 11
      newYear--
    }

    this.currentMonth = newMonth
    this.currentYear = newYear
  }

  private onDateSelected(day: number) {
    const newSelectedDate = new Date(this.currentYear, this.currentMonth, day)
    if (this.isDateSelected &&
      this.selectedDate.getDate() === day &&
      this.selectedDate.getMonth() === this.currentMonth &&
      this.selectedDate.getFullYear() === this.currentYear) {
      // 如果点击的是已选中的日期,取消选中
      this.isDateSelected = false
    } else {
      // 否则,选中新的日期
      this.selectedDate = newSelectedDate
      this.isDateSelected = true
    }
  }

  private isSelectedDate(day: number): boolean {
    return this.isDateSelected &&
      this.selectedDate.getDate() === day &&
      this.selectedDate.getMonth() === this.currentMonth &&
      this.selectedDate.getFullYear() === this.currentYear
  }

  private getDaysInMonth(): number[] {
    const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate()
    return Array.from<number, number>({ length: daysInMonth }, (_, i) => i + 1)
  }

  private getDateColor(day: number): string {
    const currentDate = new Date(this.currentYear, this.currentMonth, day)
    const today = new Date()
    today.setHours(0, 0, 0, 0)

    if (currentDate < today) {
      return '#CCCCCC' // 灰色显示过去的日期
    } else if (this.isSelectedDate(day)) {
      return '#ffffff' // 选中日期的文字颜色
    } else {
      return '#000000' // 未选中日期的文字颜色
    }
  }

  private getLunarDate(day: number): string {
    return LunarDate.solarToLunar(this.currentYear, this.currentMonth + 1, day);
  }
}

class LunarDate {
  private static lunarInfo: number[] = [
    0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
    0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
    0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
    0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
    0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
    0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5d0, 0x14573, 0x052d0, 0x0a9a8, 0x0e950, 0x06aa0,
    0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
    0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b5a0, 0x195a6,
    0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
    0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,
    0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
    0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
    0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
    0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
    0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0
  ];
  private static Gan = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
  private static Zhi = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
  private static Animals = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"];
  private static lunarMonths = ["正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"];
  private static lunarDays = ["初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
    "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
    "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"];

  static solarToLunar(year: number, month: number, day: number): string {
    if (year < 1900 || year > 2100) {
      return "无效年份";
    }

    const baseDate = new Date(1900, 0, 31);
    const objDate = new Date(year, month - 1, day);
    let offset = Math.floor((objDate.getTime() - baseDate.getTime()) / 86400000);

    let i: number, leap = 0, temp = 0;
    for (i = 1900; i < 2101 && offset > 0; i++) {
      temp = LunarDate.getLunarYearDays(i);
      offset -= temp;
    }

    if (offset < 0) {
      offset += temp;
      i--;
    }

    const lunarYear = i;
    leap = LunarDate.getLeapMonth(i);
    let isLeap = false;

    for (i = 1; i < 13 && offset > 0; i++) {
      if (leap > 0 && i === (leap + 1) && isLeap === false) {
        --i;
        isLeap = true;
        temp = LunarDate.getLeapDays(lunarYear);
      } else {
        temp = LunarDate.getLunarMonthDays(lunarYear, i);
      }

      if (isLeap === true && i === (leap + 1)) {
        isLeap = false;
      }
      offset -= temp;
    }

    if (offset === 0 && leap > 0 && i === leap + 1) {
      if (isLeap) {
        isLeap = false;
      } else {
        isLeap = true;
        --i;
      }
    }

    if (offset < 0) {
      offset += temp;
      --i;
    }
    const lunarMonth = i;
    const lunarDay = offset + 1;
    const monthStr = (isLeap ? "闰" : "") + LunarDate.lunarMonths[lunarMonth - 1];
    const dayStr = LunarDate.lunarDays[lunarDay - 1];
    return dayStr === '初一' ? monthStr + "月" : dayStr;
  }

  private static getLunarYearDays(year: number): number {
    let i = 0, sum = 348;
    for (i = 0x8000; i > 0x8; i >>= 1) {
      sum += (LunarDate.lunarInfo[year - 1900] & i) ? 1 : 0;
    }
    return sum + LunarDate.getLeapDays(year);
  }

  private static getLeapMonth(year: number): number {
    return LunarDate.lunarInfo[year - 1900] & 0xf;
  }

  private static getLeapDays(year: number): number {
    if (LunarDate.getLeapMonth(year)) {
      return (LunarDate.lunarInfo[year - 1900] & 0x10000) ? 30 : 29;
    }
    return 0;
  }

  private static getLunarMonthDays(year: number, month: number): number {
    return (LunarDate.lunarInfo[year - 1900] & (0x10000 >> month)) ? 30 : 29;
  }
}

使用

 Column() {
      CalendarView()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')

最终的效果如下:

至此我们就徒手撸了一个日历控件的实现了, 各位可以基于这个基础实现,进一步扩展相关的功能,如添加事件标记、自定义主题等,以满足不同应用场景的需求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2151020.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

9月18日国家网络安全通报中心发布的100个高危漏洞(下)

9月18日国家网络安全通报中心发布&#xff0c;公安机关网安部门从危害程度、广泛性、漏洞利用形式、利用难度、检测难度等维度&#xff0c;梳理出了100个突出的高危漏洞&#xff0c;目前这些漏洞是各个网络安全公司检测的重点&#xff0c;广大网络运营者应尽快对照排查自己的网…

火车站高铁站站点时刻查询网站计算机毕设/动车站点时刻查询

创建一个关于火车站高铁站站点时刻查询的毕业设计项目&#xff0c;是一个非常实际且具有挑战性的任务。这样的项目不仅能帮助学生综合运用所学知识&#xff0c;还能够为用户提供便捷的服务。下面将详细说明项目的各个方面&#xff1a; 1. 需求分析 用户需求&am…

代码随想录冲冲冲 Day51 图论Part3

101. 孤岛的总面积 dfs 首先dfs的作用就是在遇到陆地的时候找到所有的周围陆地 对于这道题的dfs 会把所有的链接边缘的陆地变成海洋 这样在全部调整之后 剩下的就是孤岛了 这道题中的dfs的结束条件就是遇到海洋时 遇到每一个陆地就会把面积1&#xff0c;在每一次重新找到…

(2)leetcode 234.回文链表 141.环形链表

234.回文链表 题目链接 234.回文链表 解题思路与代码 获取链表的中间段。 我们将mid这个节点记录下来&#xff0c;然后将这段链表反转&#xff0c;以下是反转的逻辑&#xff0c;最后我们将pre返回就是结果&#xff0c;就是通过中间变量tem记录位置从而实现链表的反转 最后结果…

LLM - 理解 多模态大语言模型(MLLM) 的 对齐微调(Alignment) 与相关技术 (五)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/142354652 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 完备(F…

vue scoped解析

不加scoped 加上scoped 从上面的图可以看出&#xff0c;给style加上scoped之后&#xff0c;会给这个模块的所有元素都加上一个自定义属性data-v-xxxx&#xff0c;这个xxxx就是这个文件的相对路径加上文件名生成的hash值&#xff0c;这样就能保证自定义属性独一无二 给所有元…

windows打开可选功能窗口的方式(呜呜设置里面找不到可选功能只能这样找了)

打开方式 winR打开运行窗口&#xff0c;输入fodhelper&#xff0c;按下回车键 即可快速打开可选功能窗口

手动部署并测试内网穿透

文章目录 手动部署并测试内网穿透1、原理2、下载 frp 文件3、配置对应的配置文件4、启动 frp 服务5、效果 手动部署并测试内网穿透 1、原理 原理就是让你需要访问的内网可以被其他内网访问到。 其实就是让内网经过一个公网服务器的转发&#xff0c;使得能够被访问。 这里我们需…

算法课习题汇总(2)

整数划分问题 将正整数n表示成一系列正整数之和&#xff0c;nn1n2…nk(n1>n2>…>nk,k>1)。正整数n的这种表示称为正整数n的划分。 思路&#xff1a; n表示待划分数&#xff0c;m表示最大减数。 #include<iostream> using namespace std;int q(int n, int…

MySQL:库表的基本操作

库操作 查看 查看存在哪些数据库&#xff1a; show databases;查看自己当前处于哪一个数据库&#xff1a; select database(); 由于我不处于任何一个数据库中&#xff0c;此处值为NULL 查看当前有哪些用户连接到了MySQL&#xff1a; show processlist; 创建 创建一个数据库 语…

【JAVA入门】Day48 - 线程池

【JAVA入门】Day48 - 线程池 文章目录 【JAVA入门】Day48 - 线程池一、线程池的主要核心原理二、自定义线程池三、线程池的大小 我们之前写的代码都是&#xff0c;用到线程的时候再创建&#xff0c;用完之后线程也就消失了&#xff0c;实际上这是不对的&#xff0c;它会浪费计算…

【源码+文档+调试讲解】健身房管理平台小程序

摘 要 随着健康意识的增强和移动互联网技术的普及&#xff0c;健身房管理平台小程序应运而生&#xff0c;为健身爱好者提供便捷的健身服务和管理。本设计针对传统健身房会员管理混乱、课程预约不便利、用户互动缺乏等问题&#xff0c;开发了一款集教练、预约教练、会员、健身…

正也科技-辖区与指标管理系统 强化决策支持

正也科技的“辖区与指标管理系统”设计理念先进&#xff0c;旨在通过科学合理的组织架构和精细化的指标管理&#xff0c;帮助企业实现更高效的市场布局、人员配置及业绩监控。以下是对该系统核心功能的进一步阐述及其对企业运营带来的优势&#xff1a; 正也科技辖区管理 1. 组…

基于SpringBoot+Vue+MySQL的社区医院管理系统

系统展示 系统背景 在当前医疗体系日益完善的背景下&#xff0c;社区医院作为基层医疗服务的重要一环&#xff0c;其管理效率和服务质量直接关系到居民的健康福祉。为了提升社区医院的管理水平&#xff0c;优化患者就医体验&#xff0c;我们设计了一套基于SpringBoot、Vue.js与…

深兰科技荣获“2024年度人工智能最具商业合作价值企业”奖

9月19日&#xff0c;以“释放AI应用价值&#xff0c;发展新质生产力”为主题的“AIAC2024人工智能应用大会”在北京隆重举行。大会揭晓了“AI卓智奖”年度人工智能创新评选的获奖榜单&#xff0c;深兰科技荣获“2024年度人工智能最具商业合作价值企业”称号&#xff0c;同时&am…

【高分系列卫星简介——高分一号(GF-1)】

高分一号卫星&#xff08;GF-1&#xff09; 高分一号&#xff08;GF-1&#xff09;是中国高分辨率对地观测系统&#xff08;简称“高分专项”&#xff09;的第一颗卫星&#xff0c;具有里程碑式的意义。以下是对高分一号卫星的详细介绍&#xff1a; 一、基本信息 发射时间&…

标签云效果

产品要求&#xff0c;词云要实现动态滚动。查资料&#xff0c;改写效果。 echarts词云效果 传统的echarts-wordCloud不能满足需求。 标签云 换了标签云&#xff0c;以下是代码 <template><div class"mx-auto" :style"{ width: width px }"&g…

平凉锅盔,真的绝绝子

平凉&#xff0c;这座古老的城市&#xff0c;孕育出了一种令人赞叹的美食 —— 平凉锅盔。平凉锅盔&#xff0c;那是一种能瞬间勾起人们食欲的存在。远远望去&#xff0c;它如同一座金色的小山&#xff0c;散发着诱人的光泽。其外形圆润饱满&#xff0c;厚实的面饼给人一种踏实…

时代变了,MySQL 早已不是最流行的数据库了

以下文章来源于古时的风筝 &#xff0c;作者风筝 在StackOverflow 上看到2024年技术趋势&#xff0c;关于数据库的部分&#xff0c;PostgreSQL 是开发人员使用最多的数据库&#xff0c;超过 MySQL 了。虽然在国内好像不是这样。 PostgreSQL 在 2018 年的开发者调查中首次亮相…

极越联手百度这你受得了吗!SU7还能稳坐“7字辈”头把交椅?

文/王俣祺 导语&#xff1a;自从今年上半年小米SU7标榜为“年轻人的第一台纯电轿车”&#xff0c;各家车企全都坐不住了。尤其是与小米“颇有渊源”的吉利&#xff0c;从极氪再到领克&#xff0c;目标已经可以说是路人皆知了。现在极越07也来了&#xff0c;可以看出吉利也是下了…