项目搭建指南
相关链接
- ArtTs语法: developer.harmonyos.com/cn/docs/doc...
- ArkUI文档: developer.huawei.com/consumer/cn...
- ArkUIAPI: developer.huawei.com/consumer/cn...
效果演示
[video(video-OmLMX9Uh-1715421962184)(type-csdn)(url-live.csdn.net/v/embed/386...(image-https%3A%2F%2Fvideo-community.csdnimg.cn%2Fvod-84deb4%2Fe069ecca0f7b71efb5eb6733a68f0102%2Fsnapshots%2F9e9cb0593da6437dab50363ba0f0ab17-00003.jpg%3Fauth_key%3D4869020995-0-0-869677ee5b17dde8018e460df30aa1fd)(title-%25E9%25B8%25BF%25E8%2592%2599%25E7%25A7%25BB%25E5%258A%25A8%25E7%25AB%25AF%25E5%25BC%2580%25E5%258F%2591demo-%25E4%25BB%25A3%25E5%258A%259E%25E5%25B0%258F%25E5%25B7%25A5%25E5%2585%25B7%25E8%25A7%2586%25E9%25A2%2591%25E6%25BC%2594%25E7%25A4%25BA "https://live.csdn.net/v/embed/386398)(image-https://video-community.csdnimg.cn/vod-84deb4/e069ecca0f7b71efb5eb6733a68f0102/snapshots/9e9cb0593da6437dab50363ba0f0ab17-00003.jpg?auth_key=4869020995-0-0-869677ee5b17dde8018e460df30aa1fd)(title-%E9%B8%BF%E8%92%99%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%BC%80%E5%8F%91demo-%E4%BB%A3%E5%8A%9E%E5%B0%8F%E5%B7%A5%E5%85%B7%E8%A7%86%E9%A2%91%E6%BC%94%E7%A4%BA"))]
项目搭建
从空项目开始
创建页面
- 保留原有页面
Index.ets
作为App入口页面。 - 新建两个页面:
AddTodo
: 新增代办页面。ToDoDetail
: 代办详情页。
建议右键创建page页面,IDE会自动修改路由,新增页面。
系统会自动在这里新增路由。
创建数据
创建数据模型
在 src/main/ets
下新建文件夹 model
,然后在下新建文件 ToDoModel.ets
。
typescript
export class ToDoModel {
id: number
name? : string
date? : string
color? : string
imageUrl? : string
constructor(id: number, name: string, date: string, color: string, imageUrl: string) {
this.id = id
this.name = name
this.date = date
this.color = color
this.imageUrl = imageUrl
}
}
创建默认数据
在 src/main/ets
下新建文件夹 common/constant
,新建文件 CommonConstant.est
,插入默认数据。
typescript
export default class CommonConstants {
static readonly TODO_DATA: Array<ToDoModel> = [
new ToDoModel(1, "早起晨练","2020-01-01","#FDD6AC","https://cdn.dribbble.com/users/1753960/screenshots/4965971/media/93c049fbbd538ce5ebe9230cfbd7f211.jpg"),
// ...
];
// ...
}
创建组件
组件的封装使用装饰器 @Component
创建首页组件
我们用默认的 Index.ets
。 每个页面都得用@Entry装饰器修饰结构体,表示页面入口,页面入口只能有一个
typescript
@Entry
@Component
struct Index {
@State todoList: ToDoModel[] = CommonConstants.TODO_DATA
build() {
Column() {
ToDoList({ todoList: $todoList })
}
}
}
新建 ToDoList.ets
typescript
@Component
export default struct ToDoList {
@Link todoList: ToDoModel[]
build() {
Stack() {
// ...
}
.width(CommonConstants.FULL_LENGTH)
.height(CommonConstants.FULL_LENGTH)
}
}
创建标题组件
typescript
@Component
export struct TitleView {
private title?: Resource
private titleStr?: string
build() {
Text(this.title || this.titleStr)
.fontSize($r('app.float.title_font_size'))
.fontWeight(FontWeight.Bold)
.lineHeight($r('app.float.title_font_height'))
.width(CommonConstants.TITLE_WIDTH)
.margin({ top: $r('app.float.title_margin_top'), bottom: $r('app.float.title_margin_bottom') })
.textAlign(TextAlign.Start)
}
}
关于 Resource
Resource
表示资源文件类型,我们可以通过 $r
去读取资源文件。如 <math xmlns="http://www.w3.org/1998/Math/MathML"> r ( ′ a p p . m e d i a . i c o n ′ ) ,其中 a p p 是固定值, m e d i a 是 s r c / m a i n / r e s o u r c e / b a s e 下的文件夹名, i c o n 是 m e d i a 文件夹下的具体资源名 r('app.media.icon'),其中app是固定值,media是src/main/resource/base下的文件夹名,icon是media文件夹下的具体资源名 </math>r(′app.media.icon′),其中app是固定值,media是src/main/resource/base下的文件夹名,icon是media文件夹下的具体资源名r还可以读取json文件中的字符串 比如在src/main/resource/base/element下新建文件float.json,按照如下格式填写,即可通过 $r('app.float.checkbox_width') 读取到值 '28vp'
typescript
{
"float": [
{
"name": "checkbox_width",
"value": "28vp"
}
]
}
使用封装的组件
typescript
TitleView({ title: $r('app.string.page_title')} )
.margin({ left: '5%' })
创建新增按钮组件
typescript
@Component
export struct AddButton {
build() {
Button($r("app.string.add_page_title"))
.fontSize($r('app.float.item_font_size'))
.padding({ top:'5vp', bottom: '5vp'})
.width('80%')
.margin({ bottom: '44pv' })
}
}
创建列表组件
有两种方式,一种是用Scroller组件,一种是用List组件,两种都用了之后,发现List组件相对更好用,所以这里介绍List组件 新建一个名为ScrollView的组件
typescript
@Component
export struct ScrollView {
@Link totalTasks: ToDoModel[]
build() {
List() {
ForEach(this.totalTasks, (item: ToDoModel, index: number) => {
ListItem() {
// ...
}
.swipeAction({ end: this.itemEnd.bind(this, item)})
}, (item: ToDoModel) => item.id.toString())
}
}
}
给每个ListItem新增侧滑删除功能
因为这个侧滑删除的按钮,只会在ListItem中使用,所以我们可以抽出来,放在ScrollView结构体里面 用@Builder装饰器,然后新增一个Button, 传参进去
typescript
export struct ScrollView {
@Builder itemEnd(item: ToDoModel) { /// 这里传参进来是为了之后能执行删除逻辑
// 侧滑后尾端出现的组件
Button() {
Text("删除")
.fontColor('white')
}
.width('50vp')
.height('40vp')
.margin({ left: '10vp' })
.onClick(() => {
/// 这里以后要补删除逻辑
})
}
...../// 这里是原来的代码
}
绑定删除按钮和ListItem
typescript
build() {
List() {
ForEach(this.totalTasks, (item: ToDoModel, index: number) => {
ListItem() {
//// 这里填入具体的Item组件布局,待会再补,先写个Text
Text(item.name)
}
.swipeAction({ end: this.itemEnd.bind(this, item)}) /// 使用bind方法,this传入当前对象,item传入参数,如果有多个参数,就加逗号继续往后面补
}, (item: ToDoModel) => item.id.toString())
}
创建列表Item项
新建ToDoItem.ets
typescript
@Component
export struct ToDoItem {
/// 这里一堆准备由父组件传来的属性,其实也可以改为只传递模型,这里写这么多个为了演示传多个参数的情况
private identifier? : number
private content?: string
private image?: string
private date?: string
private color?: string
@State isComplete: boolean = false;
/// 这里封装一下左边的icon
@Builder labelIcon(icon: Resource) {
Image(icon)
.objectFit(ImageFit.Contain)
.width($r("app.float.checkbox_width"))
.height($r('app.float.checkbox_width'))
.margin($r('app.float.checkbox_margin'))
.onClick(() => { /// 加上点击事件,点击的时候更新状态
this.isComplete = !this.isComplete
})
}
build() {
Row() {
/// 由于isComplete被@State修饰了,所以当isComplete 属性发生改变,这里会被重新渲染
if (this.isComplete) {
this.labelIcon($r('app.media.ic_ok'))
} else {
this.labelIcon($r('app.media.ic_default'))
}
Column({ space: '10vp' }) {
Text(this.content)
.fontSize($r('app.float.item_font_size'))
.fontWeight(CommonConstants.FONT_WEIGHT)
.opacity(this.isComplete ? CommonConstants.OPACITY_COMPLETED : CommonConstants.OPACITY_DEFAULT)
.decoration({
type: this.isComplete ? TextDecorationType.LineThrough : TextDecorationType.None
})
if ((this.date?.length ?? 0) > 0) {
Text(this.date)
.fontSize('15fp')
.fontColor(0x888888)
}
}
.alignItems(HorizontalAlign.Start)
Blank() /// 空白占位,用于撑开布局
Image(this.image || $r('app.media.ic_icon'))
.width('30%')
.margin({ top: '5%', bottom: '5%', right: '5%' })
.borderRadius(10)
}
.justifyContent(FlexAlign.SpaceBetween)
.borderRadius(CommonConstants.BORDER_RADIUS)
.backgroundColor($r('app.color.start_window_background'))
.width(CommonConstants.LIST_DEFAULT_WIDTH)
.backgroundColor(this.color?.replace("#", "#80"))
.offset({ x: this.offsetX })
}
}
最后拼接一下就好了
typescript
build() {
List() {
ForEach(this.totalTasks, (item: ToDoModel, index: number) => {
ListItem() {
ToDoItem({
identifier: item.id,
content: item.name,
image: item.imageUrl,
date: item.date,
color: item.color
})
.margin({ bottom: CommonConstants.COLUMN_SPACE })
.animation({})
.onClick(() => {
/// 这里后续补充路由
})
}
.swipeAction({ end: this.itemEnd.bind(this, item)}) /// 使用bind方法,this传入当前对象,item传入参数,如果有多个参数,就加逗号继续往后面补
}, (item: ToDoModel) => item.id.toString())
}
完成首页的拼装
typescript
@Component
export default struct ToDoList {
@Link todoList: ToDoModel[]
build() {
Stack() {
// ...
}
.width(CommonConstants.FULL_LENGTH)
.height(CommonConstants.FULL_LENGTH)
}
}
创建新增代办页面
了解大概布局方法后,这里不介绍怎么布局了,介绍一些组件的使用,直接看demo中的完整代码即可。
日期选择组件
typescript
/// 初始化一个日期字符串对象用于接受选择后的值
@State private selectedDate: string = this.formatDate(new Date())
/// 新建一个方法用于日期转字符串
formatDate(date: Date): string {
console.info(JSON.stringify(date))
return date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2,'0') + "-" + date.getDate().toString().padStart(2,'0')
}
DatePickerDialog.show({
start: new Date(), /// 起始日期
end: new Date("2100-12-31"), /// 结束日期
selected: new Date(this.selectedDate), /// 初始化日期
onAccept: (value: DatePickerResult) => {
/// 点击了确认按钮
this.selectedDate = `${value.year}-${(value.month + 1).toString().padStart(2,'0')}-${value.day.toString().padStart(2,'0')}`
},
onCancel: () => {
/// 点击了取消按钮
console.info("DatePickerDialog: onCancel")
},
onChange: (value: DatePickerResult) => {
/// 选择内容发生变化
console.info("DatePickerDialog: onChange" + JSON.stringify(value))
}
})
文本选择组件
typescript
/// 新建一个可选的颜色列表
private colors: Array<Map<string, string>> = [
new Map<string, string>([['name',"红色"], ['value', '#FF0000']]),
new Map<string, string>([['name',"蓝色"], ['value', '#0000FF']]),
new Map<string, string>([['name',"橙色"], ['value', '#F68009']]),
new Map<string, string>([['name',"黄色"], ['value', '#00FFFF']]),
new Map<string, string>([['name',"绿色"], ['value', '#00FF00']]),
]
/// 用于接收选择的颜色下标
@State private selectedColor: number = 0
TextPickerDialog.show({
/// 可选范围
range: this.colors.map((item): string => {
return item.get("name")
}),
/// 接收选择的内容
selected: this.selectedColor,
onAccept: (value: TextPickerResult) => {
/// 点击确定
this.selectedColor = Number(value.index)
console.info("TextPickerDialog: onAccept()" + JSON.stringify(value))
},
onCancel: () => {
/// 点击取消
console.info("TextPickerDialog: onCancel()")
},
onChange: (value: TextPickerResult) => {
/// 选择数据变化
console.info("TextPickerDialog: onChange()" + JSON.stringify(value))
}
})
创建代办详情页
typescript
@Entry
@Component
export default struct ToDoDetail {
/// 用路由接受从首页传递过来的模型
@State todoTask?: ToDoModel = router.getParams()?.['task']
build() {
Row() {
Column() {
Image(this.todoTask.imageUrl || $r('app.media.ic_icon'))
.width('85%')
.borderRadius('15vp')
TitleView({
titleStr: this.todoTask.name
})
.margin({ bottom: '20vp' })
Text(this.todoTask.date)
.fontSize('25vp')
.width(CommonConstants.TITLE_WIDTH)
.margin({ bottom: '20vp' })
Button("完成")
.fontSize($r('app.float.item_font_size'))
.padding({ top: '5vp', bottom: '5vp' })
.width('80%')
.margin({ bottom: '44pv' })
.onClick(() => {
/// 后续补上返回上一页的路由
})
}
.width('100%')
}
.backgroundColor(this.todoTask.color.replace("#","#80"))
.height('100%')
}
}
路由
首页 -> 新增代办
typescript
import router from '@ohos.router';
export default struct ToDoList {
@Link todoList: ToDoModel[]
build() {
Stack() {
// ...
AddButton()
.onClick(() => {
router.pushUrl({
url: "pages/AddToDo",
params: {
xxx:xxx
}
})
})
}
}
}
新增代办 -> 首页
typescript
router.back({
url: "",
params: {
"model": new ToDoModel(
+new Date(),
this.text,
this.selectedDate,
this.colors[this.selectedColor].get('value'),
this.imageUrl
)
}
})
返回之前挽留一下
typescript
router.showAlertBeforeBackPage({
message: "返回后将不保存当前内容。"
})
router.back()
typescript
router.showAlertBeforeBackPage({
message: "返回后将不保存当前内容。"
})
router.back()
首页接收从新增代办页回调的参数
typescript
struct Index {
onPageShow() {
const params = router.getParams()
console.info("生命周期方法,页面展示,",JSON.stringify(params))
if (params === undefined) {
return
}
if (params["model"] === undefined) {
return
}
let model = params["model"] as ToDoModel
this.todoList.push(model)
}
}
首页 -> 代办详情页
在 ScrollView
页面,使用路由跳转的时候,带上参数。
typescript
@Component
export struct ScrollView {
build() {
List() {
ForEach(this.totalTasks, (item: ToDoModel, index: number) => {
ListItem() {
ToDoItem({...})
.onClick(() => {
router.pushUrl({
url: "pages/ToDoDetail",
params: {
task: item
}
})
})
}
}
}
}
}
代办详情页使用路由获取参数
typescript
@Entry
@Component
export default struct ToDoDetail {
@State todoTask?: ToDoModel = router.getParams()?.['task']
}
样式
全局组件样式
typescript
@Styles commonStyle() {
.backgroundColor('#FFFFFF')
.borderRadius('10vp')
.height('8%')
.width('100%')
}
使用:
typescript
TextInput({ text: this.text ,placeholder: "输入TODO内容" })
.onChange((text: string) => {
this.text = text
})
.fontSize($r('app.float.item_font_size'))
.commonStyle()
给某个组件拓展样式
typescript
@Extend(Row) function rowStyle() {
.padding({ left: '4%', right: '4%' })
.justifyContent(FlexAlign.SpaceBetween)
}
@Extend(Text) function detailTextStyle() {
.fontColor('#333333')
.fontWeight(500)
}
使用方式如下:
typescript
Text(xxxx).detailTextStyle()
其他
ForEach
和 LazyForEach
ForEach
的使用比较简单,上面的示例已经有了。
使用 LazyForEach
可以提高性能,但是数据必须封装在一个对象里面,遵循接口 IDataSource
。
实现如下几个方法:
typescript
import CommonConstants from '../common/constant/CommonConstant'
import { ToDoModel } from '../Model/ToDoModel';
export class DataModel implements IDataSource {
private listeners: DataChangeListener[] = [];
tasks: Array<ToDoModel> = []
constructor(tasks: ToDoModel[]) {
this.tasks = tasks
}
getData(index: number): ToDoModel {
return this.tasks[index]
}
totalCount(): number {
return this.tasks.length
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener)
if (pos >= 0) {
console.info("remove listener")
this.listeners.splice(pos, 1)
}
}
}
使用:
typescript
private data: DataModel = new DataModel();
build() {
List({ space: 3 }) {
LazyForEach(this.data, (item: ToDoModel) => {
ListItem() {
Row() {
Text(item).fontSize(50)
.onAppear(() => {
console.info("appear:" + item.name)
})
}.margin({ left: 10, right: 10 })
}
.onClick(() => {
this.data.pushData(`Hello ${this.data.totalCount()}`);
})
}, item => item)
}.cachedCount(5)
}