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

本文将介绍如何使用鸿蒙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 + incrementlet newYear = this.currentYearif (newMonth > 11) {newMonth = 0newYear++} else if (newMonth < 0) {newMonth = 11newYear--}this.currentMonth = newMonththis.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 = newSelectedDatethis.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.xdnf.cn/news/145220.html

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

相关文章

[译] Go语言的源起,发展和未来

本篇内容是根据2019年9月份Creating the Go programming language音频录制内容的整理与翻译, 两位主持人与Go 的创始人 Rob Pike 和 Robert Griesemer谈论了 Go 的起源、发展、影响和未来。这是一个史诗般的剧集&#xff0c;深入探讨了 Go 的历史和详细信息&#xff0c;以及他们…

web基础—dvwa靶场(十一)CSP Bypass

CSP Bypass(CSP 绕过) 内容安全策略&#xff08;CSP&#xff09;用于定义脚本和其他资源可以从何处加载或执行&#xff0c;本模块将指导您根据开发人员犯下的常见错误来绕过该策略。 这些漏洞都不是 CSP 中的实际漏洞&#xff0c;它们都是实现 CSP 的方式中的漏洞。 绕过内容安…

JAVA——IO_缓冲流

目录 一、字节缓冲流 二、字符缓冲流 字符缓冲输入流( BufferedReader ) 字符缓冲输出流&#xff08; BufferedWriter ) 缓冲流作用&#xff1a;对原始流进行包装&#xff0c;以提高原始流读写数据的性能 一、字节缓冲流 1. 作用&#xff1a;提高字节流读写数据的性能 2…

使用scp命令从本地往服务器传输文件失败

解决办法&#xff1a; 找到这个文件&#xff0c;打开&#xff0c;将里面的服务器ip对应的一行数据删掉即可。

【Python报错已解决】To update, run: python.exe -m pip install --upgrade pip

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

麒麟操作系统快捷键设置

这些是银河麒麟操作系统常用的快捷键&#xff0c;和Windows系统有点儿相似。 但也有一些快捷键为未列出来&#xff0c;如CtrlALTT打开终端&#xff0c;Ctrld关闭终端&#xff0c;F2&#xff1a;重命名&#xff1b; CtrlshiftN&#xff1a;新建文件夹。

虚拟机ens33网卡不显示inet地址(已设置NOBOOT为yes)

在虚拟机中输入ifconfig或ip addr时&#xff0c;出现如下情况&#xff1a; sudo dhclient ens33sudo ifconfig ens33依次执行上面两行&#xff0c;之后发现ens33中可以显示inet了 本虚拟机的地址就是192.168.244.131

ABAP 一步一步教你添加ALV界面菜单功能按钮

ABAP 一步一步教你添加菜单功能按钮。 程序里面找到这个组件小按钮 就可以看到GUI状态了。 在修改GUI STATUS 是如果要添加一个功能按钮&#xff0c;必须先创建一个功能键&#xff08;具体参照下方&#xff09;&#xff0c;之后再在应用程序工具栏输入该功能键的功能码否则报…

Windows上创建批处理.bat文件并且注册为开机自启(Python-web微服务)

1. winodws桌面点击创建文本文件 &#xff08;文件名称.txt&#xff09; 2. 将如下代码写入txt文件中 echo off if "%1""h" goto begin start mshta vbscript:createobject("wscript.shell").run("""%~nx0"" h"…

百元学生党头戴式耳机选哪个?四款热门天花板机型推荐

当前市场上&#xff0c;耳机产品的竞争愈发激烈&#xff0c;降噪技术也日益精进。回想过去&#xff0c;要想享受到优质的降噪体验&#xff0c;动辄需要花费数千元&#xff0c;但现在&#xff0c;高品质的降噪耳机已经降至百元级别。在众多降噪耳机中&#xff0c;头戴式耳机尤为…

网站SEO,该如何规范目标网站URL配置!

随着互联网技术的飞速发展&#xff0c;搜索引擎优化&#xff08;SEO&#xff09;在网站建设和运营中的重要性日益凸显。优化目标网站的URL配置&#xff0c;作为SEO策略中的关键环节&#xff0c;对于提升网站在搜索引擎中的排名和曝光度具有至关重要的作用。大连蝙蝠侠科技将从U…

掌握IT资产发现的三个步骤

IT 资产生态系统非常复杂&#xff0c;因为资产不断变化&#xff0c;包括新增资产、移除过时资产或修改现有资产。在这种动态环境中&#xff0c;IT 资产管理者很难全面查看所有拥有的资产。 根据Gartner的预测&#xff0c;到 2025 年&#xff0c;大约 30% 的关键基础设施组织将…

Hutool树结构工具-TreeUtil构建树形结构

1 pom.xml <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.26</version> </dependency> 2 核心代码 import cn.beijing.satoken.domain.ZhiweiCityArea; import cn.beijing.sa…

机器人上的DPDK使用思考

引言 项目背景 人形机器人作为智能技术的集大成者&#xff0c;正逐步从科幻电影走进现实生活&#xff0c;广泛应用于工业制造、医疗健康、家庭服务等多个领域。在这一发展过程中&#xff0c;传感器技术的飞速发展和物联网技术的广泛应用&#xff0c;极大地提升了人形机器人对…

【AI视频】Runway:Gen-2 运镜详解

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AI视频 | Runway 文章目录 &#x1f4af;前言&#x1f4af;Camera Control&#xff08;运镜&#xff09;&#x1f4af;Camera Control功能测试Horizonta&#xff08;左右平移&#xff09;Vertical&#xff08;上下平移&#xff0…

双token无感刷新

文章目录 &#x1f7e2;双token无感刷新1、token过期续期的五种方案对比2、双token的基本概念3、双token无感刷新的原理4、双token无感刷新的实现方式5.前端实现 ✒️总结 &#x1f7e2;双token无感刷新 对于token无感刷新这个东西有复杂度的话&#xff0c;它主要在后端&#x…

【使用Hey对vllm接口压测】模型并发能力

使用Hey对vllm进行模型并发压测 docker run --rm --networkknowledge_network \registry.cn-shanghai.aliyuncs.com/zhph-server/hey:latest \-n 200 -c 200 -m POST -H "Content-Type: application/json" \-H "Authorization: xxx" \-d {"model"…

如何查询论文的SCI检索号?

一、登录Web of Science 不要自己登录&#xff0c;需要选择机构为CHINA CERNET Federation&#xff0c;否则无法查询文章。 然后转到机构&#xff0c;选择对应的大学。 更具对应文章名查询文献。 二、查询文献名

CUDA并行架构

一、CUDA简介 CUDA(Compute Unified Device Architecture)是一种由NVIDIA推出的通用并行计算架构&#xff0c;该架构使GPU(Graphics Processing Unit)能够对复杂的计算问题做性能速度优化。 二、串并行模式 高性能计算的关键是利用多核处理器进行并行计算。 串行模式&#…

使用Anaconda安装pyTorch

1.Anaconda简介 Anaconda 是一个流行的 Python 数据科学和机器学习平台&#xff0c;它简化了包管理和部署&#xff0c;使得安装、运行和升级包及其依赖变得非常容易。Anaconda 通过其内置的 Conda 包和环境管理器&#xff0c;提供了一个强大的环境&#xff0c;用于科学计算&…