1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { 17 CustomCalendar, // 自定义日历组件 18 CalendarViewType, // 自定义日历类型:年视图YEAR,月视图MONTH,周视图WEEK。 19 CalendarController, // 自定义日历控制器。用于控制年、月、周视图间切换场景下刷新日期数据。 20 DayInfo // 日期信息类 21} from '../customcalendar/components/CustomCalendar'; // 自定义日历组件 22import { SchedulePoint } from '../customcalendar/components/SchedulePoint'; // 自定义添加日程组件 23import { display } from '@kit.ArkUI'; // 屏幕属性模块 24import CommonData from '../customcalendar/common/CommonData'; 25import { DeviceType } from '../customcalendar/model/CalendarModel'; 26 27// 布局权重 28const LAYOUT_WEIGHT_ONE: number = 1; 29const LAYOUT_WEIGHT_THREE: number = 3; 30// 字体缩放倍数 31const TEXT_SCALING: number = 0.95; 32// 当前年 33const TODAY_YEAR: number = new Date().getFullYear(); 34// 当前月 35const TODAY_MONTH: number = new Date().getMonth() + 1; 36// 内边距。和下面的自定义的分段按钮customSegmentButtonItem有关联 37const PADDING: number = 15; 38// 自定义分段按钮中Column宽度 39const COLUMN_WIDTH: number = 50; 40// 自定义分段按钮白色滑块空隙间距 41const GAP_SPACE: number = 6; 42// 自定义分段按钮'月'Text的宽度 43const CUSTOM_SEGMENT_BUTTON_MONTH_WIDTH: number = 30; 44// 自定义分段按钮选中和未选中的字体粗细 45const FONT_WEIGHT_FOUR_HUNDRED: number = 400; 46const FONT_WEIGHT_FIVE_HUNDRED: number = 500; 47// 月份数组 48const MONTHS: string[] = 49 ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; 50// 年月信息标题字体大小 51const FONT_SIZE: number = 18; 52 53/** 54 * 功能描述:本示例介绍如何使用自定义日历组件CustomCalendar实现日历年视图,月视图,周视图以及视图切换功能。还有如何使用Calendar Kit日历服务实现日程提醒的功能。 55 * 56 * 推荐场景:需要使用左右滑动切换年视图,月视图,周视图以及需要添加日程提醒的应用场景 57 * 58 * 核心组件: 59 * 1.CustomCalendar 60 * 2.SchedulePoint 61 * 62 * 实现步骤: 63 * 日历切换场景: 64 * 1.使用Tabs进行年、月、周视图页面布局。 65 * 2.调用自定义日历组件CustomCalendar组件分别在TabContent中显示对应年、月、周视图。 66 * 3.点击自定义分段按钮customSegmentButton进行年、月、周视图间切换。使用视图控制器的swiperRefresh方法刷新对应视图数据。 67 * 日程提醒场景: 68 * 1.通过getCalendarManager获取管理日历对象,使用getCalendar获取日历对象,然后使用createCalendar创建自己的日历账户,通过配置 69 * CalendarConfig中enableReminder为true启用日程提醒功能。 70 * 2.配置日程参数calendarManager.Event,然后传入addEvent创建日程,Calendar Kit日历服务会根据创建的日程进行相应的日程提醒。同时使用持久 71 * 化preferences存储添加的日程信息,用于月视图和周视图中显示相应的日程点。 72 */ 73@Component 74export struct CustomCalendarSamplePage { 75 // 屏幕宽度 76 @State screenWidth: number = 0; 77 // 当前显示的年份 78 @State currentShowYear: number = TODAY_YEAR; 79 // 当前显示的月份 80 @State currentShowMonth: number = TODAY_MONTH; 81 // 当前月视图或周视图中选中的日期 82 @State currentSelectDay: DayInfo = 83 new DayInfo(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate(), 0); 84 // 是否隐藏年、月、周视图中年月信息标题 85 @State isYearMonthHidden: boolean = true; 86 // 当前选中的自定义分段按钮。0年视图,1月视图,2周视图 87 @State currentIndex: number = 1; 88 // 自定义分段按钮左侧边距 89 @State indicatorLeftMargin: number = 0; 90 // 自定义分段按钮白色滑块宽度 91 @State indicatorWidth: number = 0; 92 // 记录自定义分段按钮切换的index。如果切换到年视图,隐藏年月信息标题中月份 93 @State tabSelectedIndex: number = 1; 94 // 自定义分段按钮滑动动画时长 95 private animationDuration: number = 300; 96 // Tabs控制器 97 private tabController: TabsController = new TabsController(); 98 // 自定义日历年视图控制器 99 private calendarYearController = new CalendarController(); 100 // 自定义日历月视图控制器 101 private calendarMonthController = new CalendarController(); 102 // 自定义日历周视图控制器 103 private calendarWeekController = new CalendarController(); 104 // 检查设备是否可折叠 105 private isFoldable: boolean = false; 106 // 折叠设备屏幕显示模式回调 107 private callback: Callback<display.FoldDisplayMode> = (mode: display.FoldDisplayMode) => { 108 // 可折叠设备的显示模式改变时(如展开或者折叠),重新获取屏幕宽度 109 this.screenWidth = this.getCurrentScreenWidth(); 110 // 重新计算indicatorLeftMargin 111 if (this.currentIndex === CalendarViewType.YEAR) { 112 this.indicatorLeftMargin = (this.screenWidth - PADDING * 2) / 6 * 1 - COLUMN_WIDTH / 2; 113 } else if (this.currentIndex === CalendarViewType.MONTH) { 114 this.indicatorLeftMargin = (this.screenWidth - PADDING * 2) / 2 - COLUMN_WIDTH / 2; 115 } else if (this.currentIndex === CalendarViewType.WEEK) { 116 this.indicatorLeftMargin = (this.screenWidth - PADDING * 2) / 6 * 5 - COLUMN_WIDTH / 2; 117 } 118 if (mode === display.FoldDisplayMode.FOLD_DISPLAY_MODE_FULL) { 119 // 折叠屏展开态显示 120 CommonData.DEVICE_TYPE = DeviceType.EXPAND_FOLD; 121 } else if (mode === display.FoldDisplayMode.FOLD_DISPLAY_MODE_MAIN) { 122 // 折叠屏折叠态显示 123 CommonData.DEVICE_TYPE = DeviceType.PHONE; 124 } 125 }; 126 // 依据cases工程Navigation的mode属性说明,如使用Auto,窗口宽度>=600vp时,采用Split模式显示;窗口宽度<600vp时,采用Stack模式显示。 127 private readonly DEVICESIZE: number = 600; 128 // 不传isPlugin,默认为true。作为插件使用。设置false,适配cases。 129 isPlugin: boolean = true; 130 131 /** 132 * 获取当前屏幕宽度 133 */ 134 getCurrentScreenWidth(): number { 135 let screenWidth: number = px2vp(display.getDefaultDisplaySync().width); 136 // 适配cases中Navigation在不同mode时,计算相对需要使用的屏幕宽度。当屏幕宽度大于600vp时,cases工程Navigation的mode采用Split模式显 137 // 示,需要重新计算实际页面所需的屏幕宽度。 138 if (!this.isPlugin && screenWidth >= this.DEVICESIZE) { 139 return screenWidth / 2; 140 } else { 141 return screenWidth; 142 } 143 } 144 145 aboutToAppear() { 146 // 检查设备是否可折叠。false表示不可折叠,true表示可折叠。 147 this.isFoldable = display.isFoldable(); 148 CommonData.IS_FOLD = this.isFoldable; 149 if (this.isFoldable) { 150 // 如果是可折叠设备,注册折叠设备屏幕显示模式变化监听 151 display.on('foldDisplayModeChange', this.callback); 152 if (display.getFoldDisplayMode() === display.FoldDisplayMode.FOLD_DISPLAY_MODE_FULL) { 153 // 设置折叠屏展开态 154 CommonData.DEVICE_TYPE = DeviceType.EXPAND_FOLD; 155 } 156 } 157 // 获取屏幕宽度 158 this.screenWidth = this.getCurrentScreenWidth(); 159 // 初始化自定义分段按钮白色滑块的位置,本案例默认首次加载显示月视图。由于onAreaChange获取indicatorLeftMargin有延迟,首次加载会出现白色 160 // 滑块跳变,所以这里计算indicatorLeftMargin的初始位置。 161 this.indicatorLeftMargin = (this.screenWidth - COLUMN_WIDTH) / 2 - PADDING; 162 // 判断是否是rk3568。用屏幕宽度480vp来判断 163 if (px2vp(display.getDefaultDisplaySync().width) === 480) { 164 CommonData.DEVICE_TYPE = DeviceType.RK; 165 } 166 } 167 168 aboutToDisappear() { 169 if (this.isFoldable) { 170 // 关闭显示设备变化的监听 171 display.off('foldDisplayModeChange', this.callback); 172 } 173 } 174 175 /** 176 * 年月信息标题。月视图和周视图显示年月信息,年视图只显示年信息。周视图中如果选中了日期,则优先根据选中日期显示年月信息。 177 */ 178 @Builder 179 yearMonthTitle() { 180 Row() { 181 Text(`${this.currentShowYear}年 ${this.tabSelectedIndex === CalendarViewType.YEAR ? '' : 182 MONTHS[this.currentShowMonth-1]}`) 183 .fontSize(FONT_SIZE * TEXT_SCALING) 184 .fontWeight(FONT_WEIGHT_FIVE_HUNDRED) 185 // 自定义添加日程组件 186 // monthViewController: 可选项。传入该控制器,添加日程后,对应日程点会刷新到月视图上 187 // weekViewController: 可选项。传入该控制器,添加日程后,对应日程点会刷新到周视图上 188 SchedulePoint({ 189 monthViewController: this.calendarMonthController, 190 weekViewController: this.calendarWeekController 191 }) 192 } 193 .padding({ left: $r('app.integer.calendar_switch_size_ten'), right: $r('app.integer.calendar_switch_size_ten') }) 194 .justifyContent(FlexAlign.SpaceBetween) 195 .width($r('app.string.calendar_switch_full_size')) 196 .height($r('app.integer.calendar_switch_size_thirty')) 197 } 198 199 /** 200 * 自定义分段按钮 201 */ 202 @Builder 203 customSegmentButton() { 204 Stack({ alignContent: Alignment.TopStart }) { 205 Row() { 206 } 207 .width($r('app.string.calendar_switch_full_size')) 208 .height($r('app.integer.calendar_switch_size_thirty_five')) 209 .backgroundColor($r('app.color.calendar_switch_segment_button_row_bgcolor')) 210 .borderRadius($r('app.integer.calendar_switch_border_radius')) 211 .layoutWeight(LAYOUT_WEIGHT_THREE) 212 213 Column() { 214 Row() { 215 } 216 .borderRadius($r('app.integer.calendar_switch_border_radius')) 217 .height($r('app.string.calendar_switch_full_size')) 218 .width((this.screenWidth - PADDING * 2) / 3 - GAP_SPACE) 219 .backgroundColor(Color.White) 220 } 221 .height($r('app.integer.calendar_switch_size_thirty_five')) 222 .width(COLUMN_WIDTH) 223 .margin({ left: this.indicatorLeftMargin }) 224 .padding({ 225 top: $r('app.integer.calendar_switch_size_three'), 226 bottom: $r('app.integer.calendar_switch_size_three') 227 }) 228 229 Row() { 230 this.customSegmentButtonItem(CalendarViewType.YEAR, '年') 231 this.customSegmentButtonItem(CalendarViewType.MONTH, '月') 232 this.customSegmentButtonItem(CalendarViewType.WEEK, '周') 233 } 234 .width($r('app.string.calendar_switch_full_size')) 235 .height($r('app.integer.calendar_switch_size_thirty_five')) 236 .borderRadius($r('app.integer.calendar_switch_border_radius')) 237 .backgroundColor(Color.Transparent) 238 .layoutWeight(LAYOUT_WEIGHT_THREE) 239 } 240 .width($r('app.string.calendar_switch_full_size')) 241 .height($r('app.integer.calendar_switch_size_thirty_five')) 242 .margin({ 243 top: $r('app.integer.calendar_switch_size_ten'), 244 bottom: $r('app.integer.calendar_switch_margin_size_twelve') 245 }) 246 } 247 248 /** 249 * 自定义分段按钮项 250 * @param index 自定义分段按钮索引。这里对应自定义日历视图类型。0:年视图YEAR,1:月视图MONTH,2:周视图WEEK 251 * @param name 自定义分段按钮名。这里对应'年','月','周' 252 */ 253 @Builder 254 customSegmentButtonItem(index: number, name: string) { 255 Column() { 256 Text(name) 257 .width(CUSTOM_SEGMENT_BUTTON_MONTH_WIDTH) 258 .textAlign(TextAlign.Center) 259 .height($r('app.integer.calendar_switch_size_thirty_five')) 260 .fontSize($r('app.integer.calendar_switch_size_fourteen')) 261 .fontColor(this.currentIndex === index ? Color.Black : 262 $r('app.color.calendar_switch_segment_button_font_color')) 263 .fontWeight(this.currentIndex === index ? FONT_WEIGHT_FIVE_HUNDRED : FONT_WEIGHT_FOUR_HUNDRED) 264 } 265 .width($r('app.string.calendar_switch_full_size')) 266 .height($r('app.integer.calendar_switch_size_thirty_five')) 267 .layoutWeight(LAYOUT_WEIGHT_ONE) 268 .onClick(() => { 269 if (index === this.tabSelectedIndex) { 270 // 点击同一个自定义分段按钮项,不做切换,避免冗余操作 271 return; 272 } 273 this.tabSelectedIndex = index; 274 this.tabController.changeIndex(index); 275 this.currentShowYear = this.currentSelectDay.year; 276 this.currentShowMonth = this.currentSelectDay.month; 277 // TODO 知识点:点击自定义分段按钮customSegmentButton进行年、月、周视图间切换。使用视图控制器的swiperRefresh方法刷新对应视图数据。 278 if (this.tabSelectedIndex === CalendarViewType.MONTH) { 279 // 刷新月视图 280 this.calendarMonthController.swiperRefresh(CalendarViewType.MONTH); 281 } else if (this.tabSelectedIndex === CalendarViewType.WEEK) { 282 // 刷新周视图 283 this.calendarWeekController.swiperRefresh(CalendarViewType.WEEK); 284 } else if (this.tabSelectedIndex === CalendarViewType.YEAR) { 285 // 刷新年视图 286 this.calendarYearController.swiperRefresh(CalendarViewType.YEAR); 287 } 288 }) 289 } 290 291 /** 292 * 自定义分段按钮切换动画 293 * @param duration 动画时长 294 * @param leftMargin 自定义分段按钮左侧边距 295 */ 296 startAnimateTo(duration: number, leftMargin: number) { 297 animateTo({ 298 duration: duration, // 动画时长 299 curve: Curve.Linear, // 动画曲线 300 iterations: 1, // 播放次数 301 playMode: PlayMode.Normal, // 动画模式 302 }, () => { 303 this.indicatorLeftMargin = leftMargin; 304 }) 305 } 306 307 build() { 308 Column() { 309 // 年月信息标题(包含添加日程组件) 310 this.yearMonthTitle() 311 // 自定义分段按钮 312 this.customSegmentButton() 313 314 Tabs({ barPosition: BarPosition.End, index: CalendarViewType.MONTH, controller: this.tabController }) { 315 TabContent() { 316 Column() { 317 // 自定义年视图 318 // calendarViewType: 必选项。自定义日历类型。YEAR年视图 MONTH月视图 WEEK周视图 319 // onMonthClick: 可选项。年视图月份点击回调。返回年视图点击的年月信息。仅用于年视图。 320 // onChangeYearMonth: 可选项。年、月、周视图左右滑动切换回调。返回左右切换年、月、周后的年月信息,其中年视图切换实际只返回切换后年份信息。 321 // calendarSwitch: 可选项。年、月、周视图切换场景的相关设置。 322 // - controller: 可选项。自定义日历控制器,用于视图切换后的数据刷新。 323 // - currentSelectDay: 可选项。记录月、周视图中点击选中的日期信息。 324 // - isYearMonthHidden:可选项。是否隐藏自定义日历年、月、周视图中自带的年月信息标题。 325 CustomCalendar({ 326 calendarViewType: CalendarViewType.YEAR, 327 onMonthClick: (year: number, month: number) => { 328 if (this.tabController) { 329 // 切到月视图 330 this.tabController.changeIndex(1); 331 // 刷新年月信息标题 332 this.currentShowYear = year; 333 this.currentShowMonth = month; 334 // 刷新在年视图上点击月后要跳转的月视图数据 335 this.calendarMonthController.swiperYearToMonthRefresh(year, month); 336 } 337 }, 338 onChangeYearMonth: (year: number, month: number) => { 339 this.currentShowYear = year; 340 }, 341 calendarSwitch: { 342 controller: this.calendarYearController, 343 currentSelectDay: this.currentSelectDay, 344 isYearMonthHidden: this.isYearMonthHidden 345 } 346 }) 347 } 348 .width($r('app.string.calendar_switch_full_size')) 349 .height($r('app.string.calendar_switch_full_size')) 350 } 351 352 TabContent() { 353 Column() { 354 // 自定义月视图 355 // calendarViewType: 必选项。自定义日历类型。YEAR年视图 MONTH月视图 WEEK周视图 356 // calendarStyle: 可选项。自定义日历样式。仅用于月、周视图。 357 // - textScaling: 可选项。月视图和周视图中的公历、农历、星期、年月信息标题文字缩放比例。 358 // - backgroundColor: 可选项。今天选中日期的背景色。 359 // - monthDayColor: 可选项。本月公历日期颜色。 360 // - noMonthDayColor: 可选项。非本月公历日期颜色,仅对月视图有效。 361 // - lunarColor: 可选项。本月农历字体颜色。 362 // onDateClick: 可选项。日期点击回调。返回点击日期的年月日信息。仅用于月、周视图。 363 // onChangeYearMonth: 可选项。年、月、周视图左右滑动切换回调,返回左右切换视图后的年月信息,其中年视图切换实际只返回切换后年份信息。 364 // calendarSwitch: 可选项。用于年、月、周视图切换场景的相关设置。 365 // - controller: 可选项。自定义日历控制器,用于视图切换后的数据刷新。 366 // - currentSelectDay: 可选项。记录月、周视图中点击选中的日期信息。 367 // - isYearMonthHidden:可选项。是否隐藏自定义日历年、月、周视图中自带的年月信息标题。 368 CustomCalendar({ 369 calendarViewType: CalendarViewType.MONTH, 370 calendarStyle: { 371 textScaling: TEXT_SCALING, 372 backgroundColor: Color.Red, 373 monthDayColor: Color.Black, 374 noMonthDayColor: Color.Gray, 375 lunarColor: Color.Gray 376 }, 377 onDateClick: (year: number, month: number, date: number) => { 378 this.currentSelectDay.year = year; 379 this.currentSelectDay.month = month; 380 this.currentSelectDay.date = date; 381 }, 382 onChangeYearMonth: (year: number, month: number) => { 383 this.currentShowYear = year; 384 this.currentShowMonth = month; 385 }, 386 calendarSwitch: { 387 controller: this.calendarMonthController, 388 currentSelectDay: this.currentSelectDay, 389 isYearMonthHidden: this.isYearMonthHidden, 390 } 391 }) 392 } 393 .width($r('app.string.calendar_switch_full_size')) 394 .height($r('app.string.calendar_switch_full_size')) 395 } 396 397 TabContent() { 398 Column() { 399 // 自定义周视图 400 // calendarViewType: 必选项。自定义日历类型。YEAR年视图 MONTH月视图 WEEK周视图 401 // calendarStyle: 可选项。自定义日历样式。仅用于月、周视图。 402 // - textScaling: 可选项。月视图和周视图中的公历、农历、星期、年月信息标题文字缩放比例。 403 // - backgroundColor: 可选项。今天选中日期的背景色。 404 // - monthDayColor: 可选项。本月公历日期颜色。 405 // - noMonthDayColor: 可选项。非本月公历日期颜色,仅对月视图有效。 406 // - lunarColor: 可选项。本月农历字体颜色。 407 // onDateClick: 可选项。日期点击回调。返回点击日期的年月日信息。仅用于月、周视图。 408 // onChangeYearMonth: 可选项。年、月、周视图左右滑动切换回调,返回左右切换视图后的年月信息,其中年视图切换实际只返回切换后年份信息。 409 // calendarSwitch: 可选项。用于年、月、周视图切换场景的相关设置。 410 // - controller: 可选项。自定义日历控制器,用于视图切换后的数据刷新。 411 // - currentSelectDay: 可选项。记录月、周视图中点击选中的日期信息。 412 // - isYearMonthHidden:可选项。是否隐藏自定义日历年、月、周视图中自带的年月信息标题。 413 CustomCalendar({ 414 calendarViewType: CalendarViewType.WEEK, 415 calendarStyle: { 416 textScaling: TEXT_SCALING, 417 backgroundColor: Color.Red, 418 monthDayColor: Color.Black, 419 lunarColor: Color.Gray 420 }, 421 onDateClick: (year: number, month: number, date: number) => { 422 this.currentSelectDay.year = year; 423 this.currentSelectDay.month = month; 424 this.currentSelectDay.date = date; 425 }, 426 onChangeYearMonth: (year: number, month: number) => { 427 this.currentShowYear = year; 428 this.currentShowMonth = month; 429 }, 430 calendarSwitch: { 431 controller: this.calendarWeekController, 432 currentSelectDay: this.currentSelectDay, 433 isYearMonthHidden: this.isYearMonthHidden 434 } 435 }) 436 } 437 .width($r('app.string.calendar_switch_full_size')) 438 .height($r('app.string.calendar_switch_full_size')) 439 } 440 } 441 .animationDuration(this.animationDuration) 442 .onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => { 443 // 在年视图中点击月,切换到月视图时,需要显示'年月信息标题'中的的月份信息 444 if (index === CalendarViewType.YEAR && targetIndex === CalendarViewType.MONTH) { 445 this.tabSelectedIndex = CalendarViewType.MONTH; 446 } 447 // 切换动画开始时触发该回调。白色滑块跟着页面一起滑动。 448 this.currentIndex = targetIndex; 449 if (targetIndex === CalendarViewType.YEAR) { 450 // 传入自定义分段按钮左侧边距 点击“年”的分段按钮时,分段按钮白色滑块移动到屏幕1/6位置 451 this.startAnimateTo(this.animationDuration, (this.screenWidth - PADDING * 2) / 6 * 1 - COLUMN_WIDTH / 2); 452 } else if (targetIndex === CalendarViewType.MONTH) { 453 // 传入自定义分段按钮左侧边距 点击“月”的分段按钮时,分段按钮白色滑块移动到屏幕3/6位置 454 this.startAnimateTo(this.animationDuration, (this.screenWidth - PADDING * 2) / 2 - COLUMN_WIDTH / 2); 455 } else if (targetIndex === CalendarViewType.WEEK) { 456 // 传入自定义分段按钮左侧边距 点击“周”的分段按钮时,分段按钮白色滑块移动到屏幕5/6位置 457 this.startAnimateTo(this.animationDuration, (this.screenWidth - PADDING * 2) / 6 * 5 - COLUMN_WIDTH / 2); 458 } 459 }) 460 .onAppear(() => { 461 // TODO: 高性能知识点: 组件挂载显示后触发此回调,预加载年视图数据,避免首次切换到年视图时出现卡顿问题 462 // 针对月视图切换周视图场景,需要预加载周视图(索引2),不然在月视图切换月份,选择日期后,再切换到周视图,周视图不会刷新 463 this.tabController.preloadItems([0, 2]); // 索引0对应年视图,索引2对应周视图 464 }) 465 .layoutWeight(LAYOUT_WEIGHT_ONE) 466 .scrollable(false) 467 .barHeight($r('app.integer.calendar_switch_zero')) 468 } 469 .width($r('app.string.calendar_switch_full_size')) 470 .height($r('app.string.calendar_switch_full_size')) 471 .padding(PADDING) 472 } 473}