鸿蒙HarmonyOS开发Demo之代办小工具(保姆级教程&含项目代码zip)

项目搭建指南

相关链接

效果演示

鸿蒙移动端开发demo-代办小工具视频演示

项目搭建

从空项目开始

在这里插入图片描述

创建页面

  • 保留原有页面 Index.ets 作为App入口页面。
  • 新建两个页面:
    • AddTodo: 新增代办页面。
    • ToDoDetail: 代办详情页。

建议右键创建page页面,IDE会自动修改路由,新增页面。
在这里插入图片描述

系统会自动在这里新增路由。
在这里插入图片描述

创建数据

创建数据模型

src/main/ets 下新建文件夹 model,然后在下新建文件 ToDoModel.ets

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,插入默认数据。

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装饰器修饰结构体,表示页面入口,页面入口只能有一个

@Entry
@Component
struct Index {
  @State todoList: ToDoModel[] = CommonConstants.TODO_DATA

  build() {
    Column() {
      ToDoList({ todoList: $todoList })
    }
  }
}

新建 ToDoList.ets

@Component
export default struct ToDoList {
  @Link todoList: ToDoModel[]

  build() {
    Stack() {
      // ...
    }
    .width(CommonConstants.FULL_LENGTH)
    .height(CommonConstants.FULL_LENGTH)
  }
}

创建标题组件

@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 去读取资源文件。如 $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’

{
  "float": [
        {
          "name": "checkbox_width",
          "value": "28vp"
        }
    ]
  }

使用封装的组件

TitleView({ title: $r('app.string.page_title')} )
  .margin({ left: '5%' })

在这里插入图片描述

创建新增按钮组件

@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的组件

@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, 传参进去

export struct ScrollView {
    @Builder itemEnd(item: ToDoModel) { /// 这里传参进来是为了之后能执行删除逻辑
        // 侧滑后尾端出现的组件
        Button() {
          Text("删除")
            .fontColor('white')
        }
        .width('50vp')
        .height('40vp')
        .margin({ left: '10vp' })
        .onClick(() => {
          /// 这里以后要补删除逻辑
        })
     }
  ...../// 这里是原来的代码
 }

绑定删除按钮和ListItem

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
在这里插入图片描述

@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 })  
    }
}

最后拼接一下就好了

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())
 }

完成首页的拼装

@Component
export default struct ToDoList {
  @Link todoList: ToDoModel[]

  build() {
    Stack() {
      // ...
    }
    .width(CommonConstants.FULL_LENGTH)
    .height(CommonConstants.FULL_LENGTH)
  }
}

创建新增代办页面

了解大概布局方法后,这里不介绍怎么布局了,介绍一些组件的使用,直接看demo中的完整代码即可。

日期选择组件

/// 初始化一个日期字符串对象用于接受选择后的值
@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))
    }
 })

文本选择组件

 /// 新建一个可选的颜色列表
  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))
    }
 })

创建代办详情页

@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%')
  }
}

路由

首页 -> 新增代办

import router from '@ohos.router';

export default struct ToDoList {
  @Link todoList: ToDoModel[]

  build() {
    Stack() {
      // ...
      AddButton()
        .onClick(() => {
          router.pushUrl({
            url: "pages/AddToDo",
            params: {
              xxx:xxx
            }
          })
        })
    }
  }
}

新增代办 -> 首页

router.back({
    url: "",
    params: {
      "model": new ToDoModel(
        +new Date(),
        this.text,
        this.selectedDate,
        this.colors[this.selectedColor].get('value'),
        this.imageUrl
      )
    }
 })

返回之前挽留一下

router.showAlertBeforeBackPage({
    message: "返回后将不保存当前内容。"
})
router.back()

在这里插入图片描述

router.showAlertBeforeBackPage({
    message: "返回后将不保存当前内容。"
})
router.back()

首页接收从新增代办页回调的参数

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 页面,使用路由跳转的时候,带上参数。

@Component
export struct ScrollView {
    build() {
        List() {
            ForEach(this.totalTasks, (item: ToDoModel, index: number) => {
                ListItem() {
                    ToDoItem({...})
                    .onClick(() => {
                        router.pushUrl({
                          url: "pages/ToDoDetail",
                          params: {
                            task: item
                          }
                        })
                      })
               }
            }
        }
    }
}

代办详情页使用路由获取参数

@Entry
@Component
export default struct ToDoDetail {
  @State todoTask?: ToDoModel = router.getParams()?.['task']
}

样式

全局组件样式

@Styles commonStyle() {
    .backgroundColor('#FFFFFF')
    .borderRadius('10vp')
    .height('8%')
    .width('100%')
 }

使用:

TextInput({ text: this.text ,placeholder: "输入TODO内容" })
          .onChange((text: string) => {
            this.text = text
          })
          .fontSize($r('app.float.item_font_size'))
          .commonStyle()

给某个组件拓展样式

@Extend(Row) function rowStyle() {
  .padding({ left: '4%', right: '4%' })
  .justifyContent(FlexAlign.SpaceBetween)
}

@Extend(Text) function detailTextStyle() {
    .fontColor('#333333')
    .fontWeight(500)
}

使用方式如下:

Text(xxxx).detailTextStyle()

其他

ForEachLazyForEach

ForEach 的使用比较简单,上面的示例已经有了。

使用 LazyForEach 可以提高性能,但是数据必须封装在一个对象里面,遵循接口 IDataSource

实现如下几个方法:

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)
    }
  }
}

使用:

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)
  }
  • 61
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鸿蒙开发助手

赏钱一扔,代码超神,事业飞腾

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值