基于OpenHarmony的自定义日历组件实现----详细的自定义日历组件-Bond! 原创
引言
在移动应用开发中,日历组件是一个常见的需求,用户可以通过查看日历来安排自己的行程、任务等。今天,我们将详细介绍一个基于HarmonyOS框架的自定义日历组件的实现细节。该日历组件不仅展示了当前月份的日期,还能根据用户安排的计划在特定日期上进行标记。下面,我们逐步分析这个组件的实现过程。
项目概述
项目主要功能是展示一个自定义的日历,并且可以根据用户输入的计划数据,在日历中标记出计划开始和进行中的日期。日历支持滑动手势来切换到前一个月或后一个月。
项目运行结果
代码结构解析
1. 接口定义
interface Riqi{
startYear:number;
startMonth:number;
startDay:number;
endYear:number;
endMonth:number;
endDay:number;
jihuaName:string;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
首先,我们定义了一个名为Riqi的接口,用来描述每个计划的开始年、开始月、开始日、结束年、结束月、结束日以及计划名称。
2. 组件定义
@Entry
@Component
struct DataPage {
@State jihua:Array<Riqi>=[
{startYear:2025,startMonth:2,startDay:2,endYear:2025,endMonth:2,endDay:5,jihuaName:"洛阳之旅"},
{startYear:2025,startMonth:2,startDay:11,endYear:2025,endMonth:2,endDay:25,jihuaName:"南京之旅"}
]
@State selectedDate: Date = now;
@State week:Array<string>=["日","一","二","三","四","五","六"]
@State year:number=now.getFullYear();
@State month:number=now.getMonth()+1;
@State day:number=now.getDay();
@State riqi:Array<string>=tian(this.year,this.month, findday(this.year,this.month,this.day))
@State riqi_qian:Array<string>=tian(this.year,this.month-1, findday(this.year,this.month-1,this.day))
@State riqi_hou:Array<string>=tian(this.year,this.month+1, findday(this.year,this.month+1,this.day))
// ... methods ...
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
我们使用@Entry和@Component装饰器定义了一个名为DataPage的组件。组件内部使用@State装饰器定义了一系列状态变量,包括计划列表jihua、选中的日期selectedDate、一周的天数week、当前年份year、当前月份month、当前星期几day以及当前月份的日历数据riqi。
3. 判断日期是否在计划中的方法
private isDateInPlan(date: number): boolean {
return this.jihua.some(plan =>
this.year === plan.startYear &&
this.month === plan.startMonth &&
(plan.startDay === date || plan.endDay === date)
);
}
private isPlanStartDate(date: number): boolean {
return this.jihua.some(plan =>
this.year === plan.startYear &&
this.month === plan.startMonth &&
plan.startDay === date
);
}
private isDateInPlanRange(date: number): boolean {
return this.jihua.some(plan =>
this.year === plan.startYear &&
this.month === plan.startMonth &&
plan.startDay < date &&
plan.endDay > date
);
}
private getPlanName(date: number): string {
const plan = this.jihua.find(p =>
this.year === p.startYear &&
this.month === p.startMonth &&
p.startDay === date
);
return plan ? plan.jihuaName : '';
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
这些私有方法用于判断某个日期是否在用户定义的计划中,以及该日期是计划的开始日期还是在计划的进行期间。这些方法为后续的日历渲染提供了必要的逻辑条件。
4. 日历标题与日期选择器
Text(`${this.year}年${this.month}月`)
.fontSize(24)
.fontWeight(500)
.onClick(()=>{
DatePickerDialog.show({
start: new Date("2000-1-1"),
end: new Date("2100-12-31"),
selected: this.selectedDate,
showTime:false,
useMilitaryTime:false,
disappearTextStyle: {color: Color.Pink, font: {size: '22fp', weight: FontWeight.Bold}},
textStyle: {color: '#ff00ff00', font: {size: '18fp', weight: FontWeight.Normal}},
selectedTextStyle: {color: '#ff182431', font: {size: '14fp', weight: FontWeight.Regular}},
onDateAccept: (value: Date) => {
this.selectedDate = value
this.year=value.getFullYear();
this.month=value.getMonth()+1;
this.day=value.getDay();
this.riqi=tian(this.selectedDate.getFullYear(),this.selectedDate.getMonth()+1, findday(this.selectedDate.getFullYear(),this.selectedDate.getMonth()+1,this.selectedDate.getDay()))
},
onCancel: () => {},
onDateChange: () => {},
onDidAppear: () => {},
onDidDisappear: () => {},
onWillAppear: () => {},
onWillDisappear: () => {}
})
})
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
这段代码定义了一个文本按钮,用于显示当前日期的年份和月份,并且当用户点击该按钮时,会弹出一个日期选择器对话框。用户可以在对话框中选择日期,选择完成后,该组件会更新当前选中的日期并刷新日历显示。
5. 星期导航栏
ForEach(this.week,(item:string)=>{
GridItem() {
Button({ type: ButtonType.Normal }) {
Text(`${item}`)
.fontSize(16)
.fontColor("#000000")
}.width('90%')
.height('90%')
.borderRadius(0)
.backgroundColor("rgba(245, 245, 245, 1)")
}
})//循环渲染星期导航栏
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
这段代码用于渲染星期导航栏,通过ForEach循环遍历week数组,为每个星期几创建一个按钮,并设置按钮的样式。
6. 日期渲染
ForEach(this.riqi, (item:string, index:number) => {
GridItem() {
Column(){
// 判断是否为当前月份的日期
if (index >= findday(this.year, this.month, this.day) &&
index < findday(this.year, this.month, this.day) + getDaysInMonth(this.year, this.month)) {
if(this.isDateInPlan(Number(item))) {
// 标记计划开始和进行中的日期
} else {
// 渲染普通日期
}
} else {
// 渲染非当前月份的日期
}
}
}
})
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
这段代码通过ForEach循环遍历riqi数组,渲染出当前月份的日历。每个日期都会被包裹在一个按钮中,并根据不同的条件设置不同的样式。如果日期在用户的计划中,会用特定的颜色标记;如果不是当前月份的日期,会以浅灰色显示。
7. 滑动手势支持
.gesture(
SwipeGesture({
direction: SwipeDirection.Horizontal,
fingers: 1,
speed: 50
})
.onAction(event => {
if(event.angle > 0){
// 从右往左滑
this.month++;
if(this.month > 12){
this.month=1;
this.year++;
}
this.riqi=tian(this.year,this.month, findday(this.year,this.month,this.day))
}else{
// 从左往右滑
this.month--;
if(this.month < 1){
this.month=12;
this.year--;
}
this.riqi=tian(this.year,this.month, findday(this.year,this.month,this.day))
}
})
)
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
为了支持用户滑动切换月份的功能,我们给整个日历组件添加了一个滑动手势。当检测到左滑(即event.angle < 0)时,月份减一,如果月份小于1,则将月份设置为12并年份减一;当检测到右滑(即event.angle > 0)时,月份加一,如果月份大于12,则将月份设置为1并年份加一。每次月份切换后,都会重新计算并渲染该月份的日历。
辅助函数解析
findday函数
function findday(y:number,m:number,d:number) {
// 计算指定日期是该月第几天
}
- 1.
- 2.
- 3.
- 4.
findday函数用于计算指定的日期是该月的第几天。它首先计算出从2000年1月1日到指定日期之前的总天数,然后通过计算得出该日期在当月的星期几,从而确定日历显示时前面需要空出几个格子。
tian函数
function tian(y: number, m: number, kong: number): Array<string> {
// 根据年份、月份和前面空出的格子数,生成一个包含42个格子的日历数组
}
- 1.
- 2.
- 3.
- 4.
tian函数根据年份、月份以及前面需要空出的格子数来生成一个包含42个元素的数组,这个数组代表了当前月份的日历布局。数组中的元素可能是上个月的日期、本月的日期或者下个月的日期。
getDaysInMonth函数
function getDaysInMonth(year: number, month: number): number {
// 返回指定月份的天数
}
- 1.
- 2.
- 3.
- 4.
这个简单的函数用来返回指定年份和月份的天数。它根据年份是否为闰年来判断二月的天数是28天还是29天。
总结
通过以上详细的代码解析,我们可以看到这个自定义日历组件的实现采用了多种HarmonyOS提供的布局和交互组件,包括Grid、GridItem、ForEach、DatePickerDialog等。日历不仅能够展示当前月份的日期,还能根据用户定义的计划来标记特定日期,提供了一个不错的用户交互体验。此外,组件还支持通过滑动手势来切换月份,使得用户操作更加便捷。
详细代码展示
const now = new Date();
interface Riqi{
startYear:number;
startMonth:number;
startDay:number;
endYear:number;
endMonth:number;
endDay:number;
jihuaName:string;
}
@Entry
@Component
struct DataPage {
@State jihua:Array<Riqi>=[
{startYear:2025,startMonth:2,startDay:2,endYear:2025,endMonth:2,endDay:5,jihuaName:"洛阳之旅"},
{startYear:2025,startMonth:2,startDay:11,endYear:2025,endMonth:2,endDay:25,jihuaName:"南京之旅"}
]
@State selectedDate: Date = now;
@State week:Array<string>=["日","一","二","三","四","五","六"]
@State year:number=now.getFullYear();
@State month:number=now.getMonth()+1;
@State day:number=now.getDay();
@State riqi:Array<string>=tian(this.year,this.month, findday(this.year,this.month,this.day))
@State riqi_qian:Array<string>=tian(this.year,this.month-1, findday(this.year,this.month-1,this.day))
@State riqi_hou:Array<string>=tian(this.year,this.month+1, findday(this.year,this.month+1,this.day))
private isDateInPlan(date: number): boolean {
return this.jihua.some(plan =>
this.year === plan.startYear &&
this.month === plan.startMonth &&
(plan.startDay === date || plan.endDay === date)
);
}
private isPlanStartDate(date: number): boolean {
return this.jihua.some(plan =>
this.year === plan.startYear &&
this.month === plan.startMonth &&
plan.startDay === date
);
}
private isDateInPlanRange(date: number): boolean {
return this.jihua.some(plan =>
this.year === plan.startYear &&
this.month === plan.startMonth &&
plan.startDay < date &&
plan.endDay > date
);
}
private getPlanName(date: number): string {
const plan = this.jihua.find(p =>
this.year === p.startYear &&
this.month === p.startMonth &&
p.startDay === date
);
return plan ? plan.jihuaName : '';
}
//以2024.12的日历作为初始页面
//对各个变量进行初始化
build() {
Grid() {
GridItem(){
Row(){
Text(`${this.year}年${this.month}月`)
.fontSize(24)
.fontWeight(500)
.onClick(()=>{
DatePickerDialog.show({ // 建议使用 this.getUIContext().showDatePickerDialog()接口
start: new Date("2000-1-1"),
end: new Date("2100-12-31"),
selected: this.selectedDate,
showTime:false,
useMilitaryTime:false,
disappearTextStyle: {color: Color.Pink, font: {size: '22fp', weight: FontWeight.Bold}},
textStyle: {color: '#ff00ff00', font: {size: '18fp', weight: FontWeight.Normal}},
selectedTextStyle: {color: '#ff182431', font: {size: '14fp', weight: FontWeight.Regular}},
onDateAccept: (value: Date) => {
// 通过Date的setFullYear方法设置按下确定按钮时的日期,这样当弹窗再次弹出时显示选中的是上一次确定的日期
this.selectedDate = value
console.info("DatePickerDialog:onDateAccept()" + value.toString())
this.year=value.getFullYear();
this.month=value.getMonth()+1;
this.day=value.getDay();
this.riqi=tian(this.selectedDate.getFullYear(),this.selectedDate.getMonth()+1, findday(this.selectedDate.getFullYear(),this.selectedDate.getMonth()+1,this.selectedDate.getDay()))
},
onCancel: () => {
console.info("DatePickerDialog:onCancel()")
},
onDateChange: (value: Date) => {
console.info("DatePickerDialog:onDateChange()" + value.toString())
},
onDidAppear: () => {
console.info("DatePickerDialog:onDidAppear()")
},
onDidDisappear: () => {
console.info("DatePickerDialog:onDidDisappear()")
},
onWillAppear: () => {
console.info("DatePickerDialog:onWillAppear()")
},
onWillDisappear: () => {
console.info("DatePickerDialog:onWillDisappear()")
}
})
})
Image($r('app.media.sanjiao')).width(13.56).height(9.81).margin({left:5})
}
.width('90%')
.height('100%')
//根据用户输入的年月更改页面标题
}.columnStart(1)
.columnEnd(3)
GridItem() {
Column() {
Blank()
}
}.columnStart(4)
.columnEnd(7)
//跳转前一个或下一个月
ForEach(this.week,(item:string)=>{
GridItem() {
Button({ type: ButtonType.Normal }) {
Text(`${item}`)
.fontSize(16)
.fontColor("#000000")
}.width('90%')
.height('90%')
.borderRadius(0)
.backgroundColor("rgba(245, 245, 245, 1)")
}
})//循环渲染星期导航栏
// ForEach(this.riqi_qian, (item:string) => {})
ForEach(this.riqi, (item:string, index:number) => {
GridItem() {
Column(){
// 判断是否为当前月份的日期
if (index >= findday(this.year, this.month, this.day) &&
index < findday(this.year, this.month, this.day) + getDaysInMonth(this.year, this.month)) {
if(this.isDateInPlan(Number(item))) {
// if(this.isPlanStartDate(Number(item)))
Row() {
Button({type: ButtonType.Normal}) {
Text(`${item}`)
.fontSize(20)
}.width(32)
.height(32)
.borderRadius(5)
.backgroundColor("rgba(137, 186, 32, 1)")
// .margin({left:this.isPlanStartDate(Number(item))?0:'26%',right:this.isPlanStartDate(Number(item))?'26%':0})
}.width('84%')
.height(32)
.justifyContent(this.isPlanStartDate(Number(item))?FlexAlign.Start:FlexAlign.End)
.backgroundColor("rgba(137, 186, 32, 0.2)")
.margin({left:this.isPlanStartDate(Number(item))?'16%':0,right:this.isPlanStartDate(Number(item))?0:'16%'})
.borderRadius({topLeft:this.isPlanStartDate(Number(item))?5:0,topRight:this.isPlanStartDate(Number(item))?0:5,bottomLeft:this.isPlanStartDate(Number(item))?5:0,bottomRight:this.isPlanStartDate(Number(item))?0:5})
if(this.isPlanStartDate(Number(item))) {
Column() {
Button({type: ButtonType.Normal}) {
// Image($r('app.media.startIcon'))
// Text(this.getPlanName(Number(item)))
// .fontSize(12)
// .fontColor("#ff034980")
Row(){
Image($r('app.media.qizi'))
.height(13)
.width(14.06)
Text(this.getPlanName(Number(item)))
.fontSize(16)
.fontColor("rgba(133, 133, 133, 1)")
}.justifyContent(FlexAlign.SpaceAround).width('80%')
}.width(104).height(29).margin({left:70})
.backgroundColor("rgba(237, 237, 237, 1)")
.borderRadius(5)
}
.height('60%')
.backgroundColor("rgba(245, 245, 245, 1)")
.margin({top:'20%'})
// .justifyContent(FlexAlign.Center)
}
} else if(this.isDateInPlanRange(Number(item))) {
Button({type: ButtonType.Normal}) {
Text(`${item}`)
.fontSize(20)
}.width(index%7==0||(index+1)%7==0?'84%':'100%')
.height(32)
.borderRadius({
topLeft:index%7==0?5:0,
topRight:(index+1)%7==0?5:0,
bottomLeft:index%7==0?5:0,
bottomRight:(index+1)%7==0?5:0
})
.backgroundColor("rgba(137, 186, 32, 0.2)")
.margin({left:(index+1)%7==0?0:'16%',right:index%7==0?0:'16%'})
} else {
Button({type: ButtonType.Normal}) {
Text(`${item}`)
.fontSize(20)
.fontColor('#000000')
}.width('100%')
.height(32)
.borderRadius(0)
.backgroundColor("rgba(245, 245, 245, 1)")
.margin({left:(index+1)%7==0?0:'16%',right:index%7==0?0:'16%'})
}
} else {
Button({type: ButtonType.Normal}) {
Text(`${item}`)
.fontSize(20)
.fontColor('#999999')
}.width('100%')
.height(32)
.borderRadius(0)
.backgroundColor("rgba(245, 245, 245, 1)")
.margin({left:(index+1)%7==0?0:'16%',right:index%7==0?0:'16%'})
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
})//渲染日期
}
.backgroundColor("rgba(245, 245, 245, 1)")
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.rowsTemplate(this.riqi.length>35?'1fr 1fr 2fr 2fr 2fr 2fr 2fr 2fr 2fr 1fr':'1fr 1fr 2fr 2fr 2fr 2fr 2fr 2fr 1fr' )
.gesture(
SwipeGesture({
//触发滑动手势的滑动方向为水平方向。
direction: SwipeDirection.Horizontal,
fingers: 1,
speed: 50
})
//滑动手势识别成功回调。
.onAction(event => {
//angle是负的就是从左往右滑
if(event.angle > 0){
//从右往左滑
if(this.month==12){
this.month=1;
this.year++;
}else{
this.month++;
}
this.riqi=tian(this.year,this.month, findday(this.year,this.month,this.day))
}else{
//从左往右滑
if(this.month==1){
this.month=12;
this.year--;
}else{
this.month--;
}
this.riqi=tian(this.year,this.month, findday(this.year,this.month,this.day))
}
})
)
//根据月份天数判断需要几行
}
}
function findday(y:number,m:number,d:number) {
let run: Array<number> = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let feirun: Array<number> = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let sum: number = 0;
let nian: number = 2000;
let yue: number = 1;
let ri: number = 1;
let jujin: number = 0;
for (let j = nian; j < y; j++) {
//计算前几年有多少天,需区分是否为闰年
sum = 0;
if ((j % 400 == 0) || (j % 4 == 0 && j % 100 != 0)) {
sum += 366;
} else {
sum += 365;
}
jujin += sum;
}
sum = 0; //计算当年有多少天
if ((y % 400 == 0) || (y % 4 == 0 && y % 100 != 0)) {
for (let i = 0; i < m-1; i++) {
sum += run[i];
}
} else {
for (let i = 0; i < m-1; i++) {
sum += feirun[i];
}
}
jujin += sum;
jujin = (jujin+6) % 7;
//计算出要显示的日历前面需要空几格
return jujin;
}
function tian(y: number, m: number, kong: number): Array<string> {
let day: Array<string> = new Array(42); // 总共42个格子
let d: number;
let run: Array<number> = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let feirun: Array<number> = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// 确定本月天数
if ((y % 400 == 0) || (y % 4 == 0 && y % 100 != 0)) {
d = run[m - 1];
} else {
d = feirun[m - 1];
}
// 计算上个月的天数
let lastMonth: number = (m - 2 + 12) % 12;
let lastMonthDays: number = (y % 400 == 0 || (y % 4 == 0 && y % 100 != 0)) ? run[lastMonth] : feirun[lastMonth];
// 填充前面的空格为上个月的最后几天
for (let i = 0; i < kong; i++) {
day[i] = (lastMonthDays - kong + i + 1).toString(); // 依次填充
}
// 填充本月的日期
for (let i = kong; i < d + kong; i++) {
day[i] = (i - kong + 1).toString();
}
// 填充后面的空格为下个月的前几天
for (let i = d + kong; i < 42; i++) {
day[i] = (i - d - kong + 1).toString();
}
return day;
}
function padZero(arg0: number): number {
throw new Error('Function not implemented.');
}
// 添加一个新的辅助函数来获取指定月份的天数
function getDaysInMonth(year: number, month: number): number {
let run: Array<number> = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let feirun: Array<number> = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)) {
return run[month - 1];
} else {
return feirun[month - 1];
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
- 342.
- 343.
- 344.
- 345.
- 346.
- 347.
- 348.
- 349.
- 350.
- 351.
- 352.
- 353.
- 354.
- 355.
- 356.
- 357.
- 358.
微信扫码分享
写的太好了,日历开发再也不难了
有点意思,可以的
代码封装简洁清晰