OpenHarmony - 基于ArkUI框架實(shí)現(xiàn)日歷應(yīng)用
前言
對于剛剛接觸OpenHarmony應(yīng)用開發(fā)的開發(fā)者,最快的入門方式就是開發(fā)一個簡單的應(yīng)用,下面記錄了一個日歷應(yīng)用的開發(fā)過程,通過日歷應(yīng)用的開發(fā),來熟悉基本圖形的繪制,ArkUI的組件的使用,UI組件生命周期,加深對OpenHarmony應(yīng)用開發(fā)的理解。
效果展示
開發(fā)環(huán)境
- 開發(fā)工具:DevEco Studio 3.1 Release
- 開發(fā)環(huán)境:OpenHarmony API 9
- 開發(fā)語言:eTS
關(guān)于eTS
eTS語言:基于TypeScript(簡稱TS)拓展的出來的,是OpenHarmony應(yīng)用開發(fā)語言,使用ArkUI框架提供的組件進(jìn)行界面開發(fā)。
什么是TypeScript:
TypeScript 是微軟開發(fā)的一個開源的編程語言,是面向?qū)ο髲?qiáng)類型化的,在 JavaScript 的基礎(chǔ)上引入了靜態(tài)類型、類、接口的概念。
TypeScript 和 JavaScript 的區(qū)別:
- TypeScript 是 JavaScript 的超集,在JavaScript的基礎(chǔ)上拓展了語法,包含了 JavaScript 的所有元素
- 在TypeScript 中的數(shù)據(jù)要求有明確的類型,而JavaScript中沒有
- TypeScript在編譯時可以發(fā)現(xiàn)錯誤,JavaScript只有在運(yùn)行時報錯
布局容器組件
- Column :沿垂直方向布局的容器,可以包含多個子組件
- Row:沿水平方向布局容器,可以包含多個子組件
- Stack:堆疊容器,子組件按照順序依次入棧,后一個子組件覆蓋前一個子組件,可以包含多個子組件
- Flex:彈性布局,元素在容器內(nèi)水平居中,垂直等間隔分散,可以包含多個子組件
- Scroll:可滑動的容器組件,當(dāng)子組件的布局尺寸超過父組件的視口時,內(nèi)容可以滑動,內(nèi)部只支持單個子組件,可支持垂直或者水平滑動
- Tabs:一種可以通過頁簽進(jìn)行內(nèi)容視圖切換的容器組件,每個頁簽對應(yīng)一個內(nèi)容視圖,只能包含子組件TabContent
- List:列表包含一系列相同寬度的列表項(xiàng)。適合連續(xù)、多行呈現(xiàn)同類數(shù)據(jù),例如圖片和文本,只能包含ListItem子組件
- Swiper:滑動容器,提供左右切換子組件顯示的能力,可以包含多個子組件
- Grid:網(wǎng)格容器,由“行”和“列”分割的單元格所組成,通過指定“項(xiàng)目”所在的單元格做出各種各樣的布局,只能包含GridItem子組件
繪制組件
- Circle:圓形繪制組件
- Ellipse:橢圓繪制組件
- Line:直線繪制組件
- Polyline:折線繪制組件
- Polygon:多邊形繪制組件
- Path:路徑繪制組件
- Rect:矩形繪制組件
- Shape:繪制組件的父組件,父組件中會描述所有繪制組件均支持的通用屬性。
自定義組件
自定義組件生命周期函數(shù)
- aboutToAppear:在組件的 build 函數(shù)之前執(zhí)行,可以做數(shù)據(jù)的初始化操作。
- aboutToDisappear:在組件銷毀之前執(zhí)行,不允許改變狀態(tài)變量,會導(dǎo)致應(yīng)用程序行為不穩(wěn)定,可以做資源的釋放操作。
- onPageShow:僅@Entry修飾的自定義組件生效,應(yīng)用進(jìn)入前臺臺,頁面顯示時觸發(fā)。
- onPageHide:僅@Entry修飾的自定義組件生效,應(yīng)用進(jìn)入后臺,頁面消失時觸發(fā)。
自定義組件常用屬性
- @State :變量需要本地初始化,初始化的值可以被構(gòu)造參數(shù)覆蓋;
- @Prop:必須通過構(gòu)造函數(shù)參數(shù)初始化,屬于單向數(shù)據(jù)綁定,使用其父組件提供的@State變量進(jìn)行初始化
- @Link:必須通過構(gòu)造函數(shù)參數(shù)進(jìn)行初始化,屬于雙向數(shù)據(jù)綁定,子組件對@Link變量的更改將同步修改父組件的@State變量;
實(shí)現(xiàn)過程
日歷一頁顯示42天,包括上個月、當(dāng)前月、下個月的天數(shù),上個月和下個月的日期顯示灰色,點(diǎn)擊日期顯示選中效果。
支持選擇年份、月份,指定一個日期,獲取當(dāng)前月的天數(shù),根據(jù)該月1號在一周中的第幾天,獲取上個月顯示的天數(shù),以及下個月顯示的天數(shù)。
獲取上一個月的天數(shù),根據(jù)指定月份的1號在一周的第幾天,上月最大天數(shù),計算出上個月天數(shù),以object的形式添加到數(shù)組,以便區(qū)分,代碼如下:
const prevMonthDays = [];
//獲取上個月最大天數(shù)
let prevLastDay = new Date(year, month-1, 0).getDate();
//獲取某月1號所在一周的第幾天
let startWeek = new Date(year, month, 1).getDay();
// 上個月的最大天數(shù)減去當(dāng)前月1號所在一周的第幾天
for (let i = prevLastDay - startWeek + 1; i <= prevLastDay; i++) {
prevMonthDays.push({
date: new Date(year, month - 1, i),
status: 'prev'
});
}
獲取下一個月的天數(shù),根據(jù)當(dāng)前月份的1號在一周的第幾天,當(dāng)前月份的最大天數(shù),計算出下個月天數(shù),以object的形式添加到數(shù)組,以便區(qū)分,代碼如下:
const nextMonthDays = [];
//獲取下個月最大天數(shù)
let curLastDay = new Date(year, month, 0).getDate();
//獲取當(dāng)前月份1號在一周的第幾天
let startWeek = new Date(year, month, 1).getDay();
//一頁的天數(shù)減去當(dāng)前月份的天數(shù)和上個月的天數(shù)
for (let i = 1; i <= 42 - startWeek - curLastDay + 1; i++) {
nextMonthDays.push({
date: new Date(year, month + 1, i),
status: 'next'
});
}
獲取當(dāng)前月的天數(shù),以object的形式添加到數(shù)組,以便區(qū)分,代碼如下:
let curLastDay = new Date(year, month, 0).getDate();
for (let i = 1; i <= curLastDay; i++) {
curMonthDays.push({
date: new Date(year, month, i),
status: 'current'
});
}
屏幕適配
屏幕適配需要用到媒體查詢的接口,可以根據(jù)設(shè)備參數(shù),例如:屏幕分辨率、橫豎屏切換來修改應(yīng)用的樣式。
首先導(dǎo)入媒體查詢模塊:
import mediaquery from '@ohos.mediaquery'
然后通過matchMediaSync接口設(shè)置媒體查詢條件,并保存返回的條件監(jiān)聽句柄,例如:監(jiān)聽設(shè)備類型,橫豎屏狀態(tài)。
//監(jiān)聽橫豎屏狀態(tài)
private listener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(orientation: landscape)');
//監(jiān)聽當(dāng)前設(shè)備類型
private deviceListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('screen and (device-type: default)');
定義觸發(fā)回調(diào)函數(shù),當(dāng)匹配到媒體查詢條件時會觸發(fā)此回調(diào)函數(shù)。
onOrientationChange = (mediaQueryResult) => {
if (mediaQueryResult.matches) {
this.calendarWidth = "70%"
this.titleBarLeft = 80
} else {
this.calendarWidth = "100%"
this.titleBarLeft = 20
}
}
onDeviceTypeChange = (mediaQueryResult) => {
if(mediaQueryResult.matches){
this.titleBarLeftTop = 10
this.weekHeight = 30
this.pikerDialogHeight = 200
console.log("onDeviceTypeChange device-type: default")
}else{
this.titleBarLeftTop = 40
this.weekHeight = 50
this.pikerDialogHeight = 280
}
}
通過條件監(jiān)聽句柄去注冊回調(diào)函數(shù),在 aboutToAppear 組件初始化的時候執(zhí)行注冊,退出時銷毀監(jiān)聽。
//組件初始化
aboutToAppear() {
this.listener.on('change', this.onOrientationChange);
}
//組件銷毀
aboutToDisappear(){
this.listener.off('change', this.onOrientationChange);
}
數(shù)據(jù)懶加載
當(dāng)列表加載的數(shù)據(jù)過大時,直接采用循環(huán)渲染方式,導(dǎo)致頁面啟動時間過長,可以使用LazyForEach組件進(jìn)行數(shù)據(jù)的懶加載進(jìn)行優(yōu)化,按需加載數(shù)據(jù)并創(chuàng)建相應(yīng)組件。
定義一個類并實(shí)現(xiàn)IDataSource接口:
export class YearData implements IDataSource{
private list: number[] = []
private listener: DataChangeListener
constructor(list: number[]) {
this.list = list
}
totalCount(): number {
return this.list.length
}
getData(index: number): any {
return this.list[index]
}
getDataIndex(data:any){
return this.list.indexOf(data)
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener
}
unregisterDataChangeListener() {
}
}
在頁面中導(dǎo)入并使用。
import { YearData } from '../datasource/YearData'
private data: YearData = new YearData([])
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item).fontSize(20).margin({ left: 10 })
}
}
.onClick(() => {
this.data.pushData('item value: ' + this.data.totalCount())
})
}, item => item)
}
總結(jié)
日歷應(yīng)用實(shí)現(xiàn)在一頁42個格子上顯示上個月、當(dāng)前月、下個月的日期,通過日歷應(yīng)用的開發(fā)了解到了ArkUI組件的一些用法,生命周期和數(shù)據(jù)的加載過程,對之后的應(yīng)用開發(fā)有很大的幫助。