/*
* Copyright (c) 2024 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CustomCalendar, // 自定义日历组件
CalendarViewType, // 自定义日历类型:年视图YEAR,月视图MONTH,周视图WEEK。
CalendarController, // 自定义日历控制器。用于控制年、月、周视图间切换场景下刷新日期数据。
DayInfo // 日期信息类
} from '../customcalendar/components/CustomCalendar'; // 自定义日历组件
import { SchedulePoint } from '../customcalendar/components/SchedulePoint'; // 自定义添加日程组件
import { display } from '@kit.ArkUI'; // 屏幕属性模块
import { emitter } from '@kit.BasicServicesKit';
import { PlatformInfo, PlatformTypeEnum } from 'utils';
// 布局权重
const LAYOUT_WEIGHT_ONE: number = 1;
const LAYOUT_WEIGHT_THREE: number = 3;
// 字体缩放倍数
const TEXT_SCALING: number = 0.95;
// 当前年
const TODAY_YEAR: number = new Date().getFullYear();
// 当前月
const TODAY_MONTH: number = new Date().getMonth() + 1;
// 内边距。和下面的自定义的分段按钮customSegmentButtonItem有关联
const PADDING: number = 15;
// 自定义分段按钮中Column宽度
const COLUMN_WIDTH: number = 50;
// 自定义分段按钮白色滑块空隙间距
const GAP_SPACE: number = 6;
// 自定义分段按钮'月'Text的宽度
const CUSTOM_SEGMENT_Button_MONTH_WIDTH: number = 30;
// 自定义分段按钮选中和未选中的字体粗细
const FONT_WEIGHT_FOUR_HUNDRED: number = 400;
const FONT_WEIGHT_FIVE_HUNDRED: number = 500;
// 月份数组
const MONTHS: string[] =
["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"];
// 年月信息标题字体大小
const FONT_SIZE: number = 18;
/**
* 功能描述:本示例介绍如何使用自定义日历组件CustomCalendar实现日历年视图,月视图,周视图以及视图切换功能。还有如何使用Calendar Kit日历服务实现日程提醒的功能。
*
* 推荐场景:需要使用左右滑动切换年视图,月视图,周视图以及需要添加日程提醒的应用场景
*
* 核心组件:
* 1.CustomCalendar
* 2.SchedulePoint
*
* 实现步骤:
* 日历切换场景:
* 1.使用Tabs进行年、月、周视图页面布局。
* 2.调用自定义日历组件CustomCalendar组件分别在TabContent中显示对应年、月、周视图。
* 3.点击自定义分段按钮customSegmentButton进行年、月、周视图间切换。使用视图控制器的swiperRefresh方法刷新对应视图数据。
* 日程提醒场景:
* 1.通过getCalendarManager获取管理日历对象,使用getCalendar获取日历对象,然后使用createCalendar创建自己的日历账户,通过配置CalendarConfig中enableReminder为true启用日程提醒功能。
* 2.配置日程参数calendarManager.Event,然后传入addEvent创建日程,Calendar Kit日历服务会根据创建的日程进行相应的日程提醒。同时使用持久化preferences存储添加的日程信息,用于月视图和周视图中显示相应的日程点。
*/
@Component
export struct CustomCalendarSamplePage {
// 屏幕宽度
@State screenWidth: number = 0;
// 当前显示的年份
@State currentShowYear: number = TODAY_YEAR;
// 当前显示的月份
@State @Watch('monthChange') currentShowMonth: number = TODAY_MONTH;
// 当前月视图或周视图中选中的日期
@State currentSelectDay: DayInfo =
new DayInfo(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate(), 0);
// 是否隐藏年、月、周视图中年月信息标题
@State isYearMonthHidden: boolean = true;
// 当前选中的自定义分段按钮。0年视图,1月视图,2周视图
@State currentIndex: number = 1;
// 自定义分段按钮左侧边距
@State indicatorLeftMargin: number = 0;
// 自定义分段按钮白色滑块宽度
@State indicatorWidth: number = 0;
// 记录自定义分段按钮切换的index。如果切换到年视图,隐藏年月信息标题中月份
@State tabSelectedIndex: number = 1;
// 自定义分段按钮滑动动画时长
private animationDuration: number = 300;
// Tabs控制器
private tabController: TabsController = new TabsController();
// 自定义日历年视图控制器
private calendarYearController = new CalendarController();
// 自定义日历月视图控制器
private calendarMonthController = new CalendarController();
// 自定义日历周视图控制器
private calendarWeekController = new CalendarController();
// 检查设备是否可折叠
private isFoldable: boolean = false;
// 折叠设备屏幕显示模式回调
private callback: Callback<display.FoldDisplayMode> = (mode: display.FoldDisplayMode) => {
// 可折叠设备的显示模式改变时(如展开或者折叠),重新获取屏幕宽度
this.screenWidth = this.getCurrentScreenWidth();
// 重新计算indicatorLeftMargin
if (this.currentIndex === CalendarViewType.YEAR) {
this.indicatorLeftMargin = (this.screenWidth - PADDING * 2) / 6 * 1 - COLUMN_WIDTH / 2;
} else if (this.currentIndex === CalendarViewType.MONTH) {
this.indicatorLeftMargin = (this.screenWidth - PADDING * 2) / 2 - COLUMN_WIDTH / 2;
} else if (this.currentIndex === CalendarViewType.WEEK) {
this.indicatorLeftMargin = (this.screenWidth - PADDING * 2) / 6 * 5 - COLUMN_WIDTH / 2;
}
};
// 依据cases工程Navigation的mode属性说明,如使用Auto,窗口宽度>=600vp时,采用Split模式显示;窗口宽度<600vp时,采用Stack模式显示。
private readonly DEVICESIZE: number = 600;
// 不传isPlugin,默认为true。作为插件使用。设置false,适配cases。
isPlugin: boolean = true;
/**
* 当前月份信息,用于自动化用例
*/
monthChange() {
// 当前显示的月份
let month = this.currentShowMonth;
emitter.emit({ eventId: 0, priority: 0 }, {
data: {
month: month
}
});
}
/**
* 获取当前屏幕宽度
*/
getCurrentScreenWidth(): number {
let screenWidth: number = px2vp(display.getDefaultDisplaySync().width);
// 适配cases中Navigation在不同mode时,计算相对需要使用的屏幕宽度。当屏幕宽度大于600vp时,cases工程Navigation的mode采用Split模式显示,需要重新计算实际页面所需的屏幕宽度。
if (!this.isPlugin && screenWidth >= this.DEVICESIZE) {
return screenWidth / 2;
} else {
return screenWidth;
}
}
aboutToAppear() {
if (PlatformInfo.getPlatform() === PlatformTypeEnum.HARMONYOS) {
// 检查设备是否可折叠。false表示不可折叠,true表示可折叠。
this.isFoldable = display.isFoldable();
if (this.isFoldable) {
// 如果是可折叠设备,注册折叠设备屏幕显示模式变化监听
display.on('foldDisplayModeChange', this.callback);
}
}
// 获取屏幕宽度
this.screenWidth = this.getCurrentScreenWidth();
// 初始化自定义分段按钮白色滑块的位置,本案例默认首次加载显示月视图。由于onAreaChange获取indicatorLeftMargin有延迟,首次加载会出现白色滑块跳变,所以这里计算indicatorLeftMargin的初始位置。
this.indicatorLeftMargin = (this.screenWidth - COLUMN_WIDTH) / 2 - PADDING;
}
aboutToDisappear() {
if (PlatformInfo.getPlatform() === PlatformTypeEnum.HARMONYOS) {
if (this.isFoldable) {
// 关闭显示设备变化的监听
display.off('foldDisplayModeChange', this.callback);
}
}
}
/**
* 年月信息标题。月视图和周视图显示年月信息,年视图只显示年信息。周视图中如果选中了日期,则优先根据选中日期显示年月信息。
*/
@Builder
yearMonthTitle() {
Row() {
Text(`${this.currentShowYear}年 ${this.tabSelectedIndex === CalendarViewType.YEAR ? '' : MONTHS[this.currentShowMonth-1]}`)
.fontSize(FONT_SIZE * TEXT_SCALING)
.fontWeight(FONT_WEIGHT_FIVE_HUNDRED)
if (PlatformInfo.getPlatform() === PlatformTypeEnum.HARMONYOS) {
// 自定义添加日程组件
// monthViewController: 可选项。传入该控制器,添加日程后,对应日程点会刷新到月视图上
// weekViewController: 可选项。传入该控制器,添加日程后,对应日程点会刷新到周视图上
SchedulePoint({
monthViewController: this.calendarMonthController,
weekViewController: this.calendarWeekController
}).id('schedulePoint')
}
}
.padding({ left: $r('app.integer.calendar_switch_size_ten'), right: $r('app.integer.calendar_switch_size_ten') })
.justifyContent(FlexAlign.SpaceBetween)
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.integer.calendar_switch_size_thirty'))
}
/**
* 自定义分段按钮。这里不使用SegmentButtonOptions分段按钮是因为在年视图中点击月份,想实现从'年'滑动到'月'按钮的动效,但是
* SegmentButtonOptions只支持点击操作才有滑动切换动效。这里自定义分段按钮的切换动画参考自定义TabBar的切换动画实现https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-container-tabs-V5#%E7%A4%BA%E4%BE%8B9
*/
@Builder
customSegmentButton() {
Stack({ alignContent: Alignment.TopStart }) {
Row() {
}
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.integer.calendar_switch_size_thirty_five'))
.backgroundColor($r('app.color.calendar_switch_segment_button_row_bgcolor'))
.borderRadius($r('app.integer.calendar_switch_border_radius'))
.layoutWeight(LAYOUT_WEIGHT_THREE)
Column() {
Row() {
}
.borderRadius($r('app.integer.calendar_switch_border_radius'))
.height($r('app.string.calendar_switch_full_size'))
.width((this.screenWidth - PADDING * 2) / 3 - GAP_SPACE)
.backgroundColor(Color.White)
}
.height($r('app.integer.calendar_switch_size_thirty_five'))
.width(COLUMN_WIDTH)
.margin({ left: this.indicatorLeftMargin })
.padding({
top: $r('app.integer.calendar_switch_size_three'),
bottom: $r('app.integer.calendar_switch_size_three')
})
Row() {
this.customSegmentButtonItem(CalendarViewType.YEAR, '年')
this.customSegmentButtonItem(CalendarViewType.MONTH, '月')
this.customSegmentButtonItem(CalendarViewType.WEEK, '周')
}
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.integer.calendar_switch_size_thirty_five'))
.borderRadius($r('app.integer.calendar_switch_border_radius'))
.backgroundColor(Color.Transparent)
.layoutWeight(LAYOUT_WEIGHT_THREE)
}
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.integer.calendar_switch_size_thirty_five'))
.margin({
top: $r('app.integer.calendar_switch_size_eight'),
bottom: $r('app.integer.calendar_switch_size_eight')
})
}
/**
* 自定义分段按钮项
* @param index 自定义分段按钮索引。这里对应自定义日历视图类型。0:年视图YEAR,1:月视图MONTH,2:周视图WEEK
* @param name 自定义分段按钮名。这里对应'年','月','周'
*/
@Builder
customSegmentButtonItem(index: number, name: string) {
Column() {
Text(name)
.width(CUSTOM_SEGMENT_Button_MONTH_WIDTH)
.textAlign(TextAlign.Center)
.height($r('app.integer.calendar_switch_size_thirty_five'))
.fontSize($r('app.integer.calendar_switch_size_fourteen'))
.fontColor(this.currentIndex === index ? Color.Black :
$r('app.color.calendar_switch_segment_button_font_color'))
.fontWeight(this.currentIndex === index ? FONT_WEIGHT_FIVE_HUNDRED : FONT_WEIGHT_FOUR_HUNDRED)
}
.id(index === 0 ? 'year' : (index === 1 ? 'month' : 'week'))
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.integer.calendar_switch_size_thirty_five'))
.layoutWeight(LAYOUT_WEIGHT_ONE)
.onClick(() => {
if (index === this.tabSelectedIndex) {
// 点击同一个自定义分段按钮项,不做切换,避免冗余操作
return;
}
this.tabSelectedIndex = index;
this.tabController.changeIndex(index);
this.currentShowYear = this.currentSelectDay.year;
this.currentShowMonth = this.currentSelectDay.month;
// TODO 知识点:点击自定义分段按钮customSegmentButton进行年、月、周视图间切换。使用视图控制器的swiperRefresh方法刷新对应视图数据。
if (this.tabSelectedIndex === CalendarViewType.MONTH) {
// 刷新月视图
this.calendarMonthController.swiperRefresh(CalendarViewType.MONTH);
} else if (this.tabSelectedIndex === CalendarViewType.WEEK) {
// 刷新周视图
this.calendarWeekController.swiperRefresh(CalendarViewType.WEEK);
} else if (this.tabSelectedIndex === CalendarViewType.YEAR) {
// 刷新年视图
this.calendarYearController.swiperRefresh(CalendarViewType.YEAR);
}
})
}
/**
* 自定义分段按钮切换动画
* @param duration 动画时长
* @param leftMargin 自定义分段按钮左侧边距
*/
startAnimateTo(duration: number, leftMargin: number) {
animateTo({
duration: duration, // 动画时长
curve: Curve.Linear, // 动画曲线
iterations: 1, // 播放次数
playMode: PlayMode.Normal, // 动画模式
}, () => {
this.indicatorLeftMargin = leftMargin;
})
}
build() {
Column() {
// 年月信息标题(包含添加日程组件)
this.yearMonthTitle()
// 自定义分段按钮
this.customSegmentButton()
Tabs({ barPosition: BarPosition.End, index: CalendarViewType.MONTH, controller: this.tabController }) {
TabContent() {
Column() {
// 自定义年视图
// CalendarViewType: 必选项。自定义日历类型。YEAR年视图 MONTH月视图 WEEK周视图
// onMonthClick: 可选项。年视图月份点击回调。返回年视图点击的年月信息。仅用于年视图。
// onChangeYearMonth: 可选项。年、月、周视图左右滑动切换回调。返回左右切换年、月、周后的年月信息,其中年视图切换实际只返回切换后年份信息。
// CalendarSwitch: 可选项。年、月、周视图切换场景的相关设置。
// - controller: 可选项。自定义日历控制器,用于视图切换后的数据刷新。
// - currentSelectDay: 可选项。记录月、周视图中点击选中的日期信息。
// - isYearMonthHidden:可选项。是否隐藏自定义日历年、月、周视图中自带的年月信息标题。
CustomCalendar({
CalendarViewType: CalendarViewType.YEAR,
onMonthClick: (year: number, month: number) => {
if (this.tabController) {
// 切到月视图
this.tabController.changeIndex(1);
// 刷新年月信息标题
this.currentShowYear = year;
this.currentShowMonth = month;
// 刷新在年视图上点击月后要跳转的月视图数据
this.calendarMonthController.swiperYearToMonthRefresh(year, month);
}
},
onChangeYearMonth: (year: number, month: number) => {
this.currentShowYear = year;
// 当前显示的年,用于自动化用例
let currentYear = year;
emitter.emit({ eventId: 0, priority: 0 }, {
data: {
currentYear: currentYear
}
});
},
CalendarSwitch: {
controller: this.calendarYearController,
currentSelectDay: this.currentSelectDay,
isYearMonthHidden: this.isYearMonthHidden
}
})
}
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.string.calendar_switch_full_size'))
}
TabContent() {
Column() {
// 自定义月视图
// CalendarViewType: 必选项。自定义日历类型。YEAR年视图 MONTH月视图 WEEK周视图
// CalendarStyle: 可选项。自定义日历样式。仅用于月、周视图。
// - textScaling: 可选项。月视图和周视图中的公历、农历、星期、年月信息标题文字缩放比例。
// - backgroundColor: 可选项。今天选中日期的背景色。
// - monthDayColor: 可选项。本月公历日期颜色。
// - noMonthDayColor: 可选项。非本月公历日期颜色,仅对月视图有效。
// - lunarColor: 可选项。本月农历字体颜色。
// onDateClick: 可选项。日期点击回调。返回点击日期的年月日信息。仅用于月、周视图。
// onChangeYearMonth: 可选项。年、月、周视图左右滑动切换回调,返回左右切换视图后的年月信息,其中年视图切换实际只返回切换后年份信息。
// CalendarSwitch: 可选项。用于年、月、周视图切换场景的相关设置。
// - controller: 可选项。自定义日历控制器,用于视图切换后的数据刷新。
// - currentSelectDay: 可选项。记录月、周视图中点击选中的日期信息。
// - isYearMonthHidden:可选项。是否隐藏自定义日历年、月、周视图中自带的年月信息标题。
CustomCalendar({
CalendarViewType: CalendarViewType.MONTH,
CalendarStyle: {
textScaling: TEXT_SCALING,
backgroundColor: Color.Red,
monthDayColor: Color.Black,
noMonthDayColor: Color.Gray,
lunarColor: Color.Gray
},
onDateClick: (year: number, month: number, date: number) => {
this.currentSelectDay.year = year;
this.currentSelectDay.month = month;
this.currentSelectDay.date = date;
// 当前选中的日期信息,用于自动化用例
let selectDate = date;
let selectYear = year;
emitter.emit({ eventId: 0, priority: 0 }, {
data: {
selectDate: selectDate,
selectYear: selectYear
}
});
},
onChangeYearMonth: (year: number, month: number) => {
this.currentShowYear = year;
this.currentShowMonth = month;
},
CalendarSwitch: {
controller: this.calendarMonthController,
currentSelectDay: this.currentSelectDay,
isYearMonthHidden: this.isYearMonthHidden,
}
})
}
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.string.calendar_switch_full_size'))
}
TabContent() {
Column() {
// 自定义周视图
// CalendarViewType: 必选项。自定义日历类型。YEAR年视图 MONTH月视图 WEEK周视图
// CalendarStyle: 可选项。自定义日历样式。仅用于月、周视图。
// - textScaling: 可选项。月视图和周视图中的公历、农历、星期、年月信息标题文字缩放比例。
// - backgroundColor: 可选项。今天选中日期的背景色。
// - monthDayColor: 可选项。本月公历日期颜色。
// - noMonthDayColor: 可选项。非本月公历日期颜色,仅对月视图有效。
// - lunarColor: 可选项。本月农历字体颜色。
// onDateClick: 可选项。日期点击回调。返回点击日期的年月日信息。仅用于月、周视图。
// onChangeYearMonth: 可选项。年、月、周视图左右滑动切换回调,返回左右切换视图后的年月信息,其中年视图切换实际只返回切换后年份信息。
// CalendarSwitch: 可选项。用于年、月、周视图切换场景的相关设置。
// - controller: 可选项。自定义日历控制器,用于视图切换后的数据刷新。
// - currentSelectDay: 可选项。记录月、周视图中点击选中的日期信息。
// - isYearMonthHidden:可选项。是否隐藏自定义日历年、月、周视图中自带的年月信息标题。
CustomCalendar({
CalendarViewType: CalendarViewType.WEEK,
CalendarStyle: {
textScaling: TEXT_SCALING,
backgroundColor: Color.Red,
monthDayColor: Color.Black,
lunarColor: Color.Gray
},
onDateClick: (year: number, month: number, date: number) => {
this.currentSelectDay.year = year;
this.currentSelectDay.month = month;
this.currentSelectDay.date = date;
},
onChangeYearMonth: (year: number, month: number) => {
this.currentShowYear = year;
this.currentShowMonth = month;
},
CalendarSwitch: {
controller: this.calendarWeekController,
currentSelectDay: this.currentSelectDay,
isYearMonthHidden: this.isYearMonthHidden
}
})
}
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.string.calendar_switch_full_size'))
}
}
.animationDuration(this.animationDuration)
.onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => {
// 在年视图中点击月,切换到月视图时,需要显示'年月信息标题'中的的月份信息
if (index === CalendarViewType.YEAR && targetIndex === CalendarViewType.MONTH) {
this.tabSelectedIndex = CalendarViewType.MONTH;
}
// 切换动画开始时触发该回调。白色滑块跟着页面一起滑动。
this.currentIndex = targetIndex;
if (targetIndex === CalendarViewType.YEAR) {
// 传入自定义分段按钮左侧边距 点击“年”的分段按钮时,分段按钮白色滑块移动到屏幕1/6位置
this.startAnimateTo(this.animationDuration, (this.screenWidth - PADDING * 2) / 6 * 1 - COLUMN_WIDTH / 2);
} else if (targetIndex === CalendarViewType.MONTH) {
// 传入自定义分段按钮左侧边距 点击“月”的分段按钮时,分段按钮白色滑块移动到屏幕3/6位置
this.startAnimateTo(this.animationDuration, (this.screenWidth - PADDING * 2) / 2 - COLUMN_WIDTH / 2);
} else if (targetIndex === CalendarViewType.WEEK) {
// 传入自定义分段按钮左侧边距 点击“周”的分段按钮时,分段按钮白色滑块移动到屏幕5/6位置
this.startAnimateTo(this.animationDuration, (this.screenWidth - PADDING * 2) / 6 * 5 - COLUMN_WIDTH / 2);
}
})
.onAppear(() => {
// TODO: 高性能知识点: 组件挂载显示后触发此回调,预加载年视图数据,避免首次切换到年视图时出现卡顿问题
// 针对月视图切换周视图场景,需要预加载周视图(索引2),不然在月视图切换月份,选择日期后,再切换到周视图,周视图不会刷新
this.tabController.preloadItems([0, 2]); // 索引0对应年视图,索引2对应周视图
})
.layoutWeight(LAYOUT_WEIGHT_ONE)
.scrollable(false)
.barHeight($r('app.integer.calendar_switch_zero'))
}
.width($r('app.string.calendar_switch_full_size'))
.height($r('app.string.calendar_switch_full_size'))
.padding({ left: PADDING })
}
}