鸿蒙系统应用基础开发

0x01 概要叙述

(1)鸿蒙系统

  • 鸿蒙是华为公司开发的操作系统,在多端使用
    • 以手机为中心,包括手表、平板等
    • “万物互联”思想
  • 各类应用间接为用户带来操作系统的用途
    • “鸿蒙应用千帆起,轻舟已过万重山”

(2)准备工作

a. 语言

  • 鸿蒙系统应用的开发语言:ArkTS
    • 是 TypeScript 的超集
  • 统一了编程体验
    • ArkTS 包括 HTML、CSS、JavaScript

区别:ArkTS 是语言,ArkUI 是框架

b. 工具

开发工具:DevEco Studio

  1. 官网下载安装包并打开
  2. 遇到多选可以全选
  3. 安装完成后不需要重启
  4. Basic Setup 中全部使用 Install
    • 如果本地存在 Node.js、Ohpm,可以选择 Local 并定位到相关目录
    • 建议使用 Install 安装官方推荐版本
    • Ohpm(Open Harmony Package Management)是开放鸿蒙包管理器
  5. SDK Setup 中全部选择 Accept
  6. 选择 Create Project 创建项目
  7. 选择 Empty Ability
  8. 配置项目信息
    1. Project name:项目名称
    2. Bundle name:应用上线的唯一标识
      • 公司域名翻转与应用名称,如 com.example.application
    3. Save location:项目保存路径
    4. Compile SDK:编译开发工具包
    5. Model:选择模型(FA 是鸿蒙开发早期模型)
    6. Device type:选择应用适配设备

c. 项目目录

graph TB app-->AppScope & entry entry-->src-->main & ohosTest main-->ets & resource ets-->entryability & pages entryability-->EntryAbility.ts pages-->Index.ets resource-->en_US & zh_CN & module.json5
  • app:应用模块
  • entry:入口模块
    • 在一个项目中可能会包含多个模块,但 entry 是唯一的主模块
  • AppScope:应用全局配置
    • 各个模块可以共享的配置
  • src:开发的源代码目录
  • main:主目录
  • ohosTest:测试目录
  • EntryAbility.ts:当前模块入口文件
  • pages:页面目录
  • Index.ets:主页面,每个页面的文件的后缀为 ets
  • resources:资源目录,包括文本、图片、音视频等;还包括国际化相关功能子目录,如 en-US、zh-CN
  • module.json5:当前模块配置文件

d. 页面代码结构

  • 一个应用中往往包含多个页面

  • 一个页面就是一个结构描述

    • 关键字 struct 用于描述一个自定义组件,名称与文件名相同,使用驼峰命名法
    • 页面是组件,而组件不一定是页面
    • 一个页面可以拆分成多个组件,提高组件复用率(组件化)
  • 一个组件中必须包含以下内容:

    • build(),用于构建组件 UI 界面,其中:
      • 一般编写 ArkTS 提供的内置组件
      • 只能包含一个根组件
    • @Component/@CustomDialog,组件装饰器/自定义对话框装饰器
  • @Entry,该装饰器可以将组件作为单独页面在 Preview 中进行预览

  • @State,该装饰器作用于组件的内部变量,当变量修改后(数据监视),页面会自动重新渲染;声明时必须初始化

  • 组件可以不断进行嵌套

    build() {
      Row() {
        Column() {
          Row() {
            // ...
          }
        }
      }
    }
    
  • 组件是一个封装好的对象,包括属性(样式)、方法(事件)

    build() {
      Column() {
        Text("Item 1")
        Text("Item 2")
        Text("Item 3")
      }.width(300)
    }
    
    • 300 是虚拟像素,根据屏幕换算
    • 对于列容器,默认宽高由内容决定
  • 举例:

    @Entry
    @Component
    struct Index {
      @State message: string = 'Hello World'
    
      build() {
        Row() {
          Column() {
            Text(this.message)
              .fontSize(50)
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
        }
        .height('100%')
      }
    }
    

e. 调试

  • 工具中的 Preview 提供单页面预览功能
  • 工具中的 Device Manager 中允许连接并使用模拟真机进行调试

(3)常用样式

a. 文本样式

  • fontSize:字号

    Text("Hello, world!").fontSize(60)
    
  • fontColor:文本颜色

    Text("Hello, world!").fontColor(Color.Red)
    // 或
    Text("Hello, world!").fontColor("#f00")
    
  • fontWeight:字重

    Text("Hello, world!").fontWeight(FontWeight.Bold)
    // 或
    Text("Hello, world!").fontWeight(800)
    
  • fontStyle:字样

    Text("Hello, world!").fontStyle(FontStyle.Normal)	// 常规
    Text("Hello, world!").fontStyle(FontStyle.Italic)	// 斜体
    
  • fontFamily:字体

  • textAlign:对齐方向

    Text("Hello, world!")
      .width("100%")
      .textAlign(TextAlign.Start)
    
    • 必须指定宽度后,才可以设置对齐方向
  • lineHeight:行高

    Text("Hello, world!").lineHeight(300)
    
  • decoration:划线

    Text("Hello, world!").decoration({type: TextDecorationType.Underline})		// 下划线
    Text("Hello, world!").decoration({type: TextDecorationType.Overline})		// 上划线
    Text("Hello, world!").decoration({type: TextDecorationType.LineThrough})	// 删除线
    

b. 背景样式

  • backgroundColor:背景颜色
  • backgroundImage:背景图片

c. 盒子模型

  • width:宽度

  • height:高度

  • padding:内边距

    Text("Hello, world!").padding({top:10})
    
  • border:边框

    Text("Hello, world!").border({style: BorderStyle.Solid, color: Color.Red, radius: 50})
    
  • margin:外边距

    Text("Hello, world!").margin(10)
    
  • 列间距(行间距同理)

    Column({space: 16}) {}
    

(4)常用事件

  • 事件三要素:事件源、事件类型、事件处理

    • 事件处理推荐使用箭头函数(参数列表) => 函数体,方便访问组件内其他属性与方法
    Button("Click").onClick(()=>{
      console.log("Log")
    })
    
  • 可以使用 bindthis 绑定到普通函数中

    @Entry
    @Component
    struct Index {
      text: string = "This is a piece of text."
      handle() {
        console.log(this.text);
      }
      build() {
        Column() {
          Button("Click").onClick(this.handle.bind(this))
        }
        .width("100%")
        .height("100%")
      }
    }
    

0x02 页面设计

(1)ArkUI 常用内置组件

a. Text 文本组件

  • 语法:Text(content?: string | Resource)

  • 长文本最大行数与省略显示

    Text("This is a long long sentence.")
      .width(100)
      .maxLines(1)
      .textOverflow({overflow:TextOverflow.Ellipsis})
    
  • 国际化

    • src/main/resources/base/element/string.json

      {
        "string": [
          {
            "name": "username",
            "value": "用户名"
          },
          {
            "name": "password",
            "value": "密码"
          }
        ]
      }
      
    • src/main/resources/en_US/element/string.json

      {
        "string": [
          {
            "name": "username",
            "value": "username"
          },
          {
            "name": "password",
            "value": "password"
          }
        ]
      }
      
    • src/main/resources/zh_CN/element/string.json

      {
        "string": [
          {
            "name": "username",
            "value": "用户名"
          },
          {
            "name": "password",
            "value": "密码"
          }
        ]
      }
      
    • Index.ets

      Column() {
        Row() {
          Text($r('app.string.username'))
            .fontSize(50)
        }
        Row() {
          Text($r('app.string.password'))
            .fontSize(50)
        }
      }
      .width('100%')
      .height('100%')
      

b. TextInput 输入框组件

  • 语法:TextInput(value?:{placeholder?: ResourceStr, text?: ResourceStr, controller?: TextInputController})

  • 登录表单

    Column({space: 20}) {
      Row() {
        Text($r('app.string.username'))
          .fontSize(22)
          .width("20%")
        TextInput({placeholder: "输入账号"})
          .width("70%")
      }
      Row() {
        Text($r('app.string.password'))
          .fontSize(22)
          .width("20%")
        TextInput({placeholder: "输入密码"})
          .width("70%")
          .type(InputType.Password)
      }
    }
    

c. Button 按钮组件

  • 语法:Button(options?: {type?: ButtonType, stateEffect?: boolean})

  • 登录表单按钮组

    Row({ space: 20 }) {
      Button($r('app.string.login'))
        .fontSize(22)
      Button($r('app.string.reset'))
        .fontSize(22)
        .type(ButtonType.Normal)
    }
    
  • 完善登录页面

    @Entry
    @Component
    struct Index {
      @State username: string = ""
      @State password: string = ""
    
      build() {
        Column({space: 20}) {
          Row() {
            Text("登录 Login")
          }
          Row() {
            Text($r('app.string.username'))
            TextInput({placeholder: "输入账号", text: this.username})
              .onChange(content => this.username = content)
          }
          Row() {
            Text($r('app.string.password'))
            TextInput({placeholder: "输入密码", text: this.password})
              .type(InputType.Password)
              .onChange(content => this.password = content)
          }
          Row({ space: 20 }) {
            Button($r('app.string.login'))
              .onClick(() => {
                console.log("username:" + this.username)
                console.log("password:" + this.password)
              })
            Button($r('app.string.reset'))
              .onClick(() => {
                this.username = ""
                this.password = ""
              })
          }
        }
      }
    }
    

d. Blank 空白组件

  • 语法:Blank(min?: number | string)

  • 占据父容器中剩余空间

  • 调整表单对齐

    Column({ space: 20 }) {
      Row() {
        Text("Item1")
        Blank()
        TextInput()
          .width(200)
      }
      .width("80%")
      Row() {
        Text("Item2")
        Blank()
        TextInput()
          .width(200)
      }
      .width("80%")
    }
    .width('100%')
    .height('100%')
    

e. Image 图片组件

  • 语法:Image(src: string | PixelMap | Resource)

  • 可以渲染与展示本地图片和网络图片

    Column({ space: 20 }) {
      Image($r('app.media.logo'))
        .width("50%")
      Image("https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/HW-LOGO.svg")
        .width("50%")
    }
    .width('100%')
    .height('100%')
    

f. Slider 滑块组件

  • 语法:Slider(options?: {value?: number, min?: number, max?: number, step?: number, style?: SliderStyle, direction?: Axis, reverse?: boolean})

  • 举例

    Column({ space: 20 }) {
      Slider({
        min: 0,              // 最小值
        max: 20,             // 最大值
        value: this.value,   // 当前值
        step: 2,             // 步长
        style: SliderStyle.InSet  // 样式
      })
        .trackColor(Color.Red)      // 轨道颜色
        .selectedColor(Color.Pink)  // 选中颜色
        .trackThickness(9)          // 轨道厚度
        .onChange(value => this.value = value)
      Text(this.value.toString())
    }
    .width('100%')
    .height('100%')
    .backgroundColor("#ccc")
    
  • 图片尺寸设置案例:

    @Entry
    @Component
    struct Index {
      @State widthValue:number = 100
      minWidth:number = 50
      maxWidth:number = 340
    
      build() {
        Column({ space: 20 }) {
          Text("图片尺寸设置")
          Row() {
            Image($r("app.media.image"))
              .width(this.widthValue)
          }
          Row() {
            Text("图片宽度  ")
            TextInput({ text: parseInt(this.widthValue.toFixed(0)).toString() })
              .onChange(value => this.widthValue = widthValue)
          }
          Row() {
            Button("缩小").onClick(() => this.widthValue -= 1)
            Button("放大").onClick(() => this.widthValue += 1)
          }
          Slider({
            min: this.minWidth,
            max: this.maxWidth,
            value: this.widthValue,
            step: 1
          })
            .onChange(value => this.widthValue = value)
        }
      }
    }
    

    完整代码:https://gitee.com/srigt/harmony/blob/master/图片宽度自定义

g. List 列表组件

  • 语法:List(value?:{space?: number | string, initialIndex?: number, scroller?: Scroller})

  • 其子组件只能ListItem(value?: string)ListItemGroup(options?: {header?: CustomBuilder, footer?: CustomBuilder, space?: number | string})

    • ListItem 中可以使用其他组件
    • ListItem 组件的 swipeAction() 支持侧滑手势,其中传入组件用于设置侧滑的内容
  • 举例 1:电商平台商品列表

    import router from '@ohos.router'
    
    interface IProduct {
      id: number,
      imageURL: string,
      name: string,
      price: number,
      discounted?: number
    }
    
    @Entry
    @Component
    struct Page {
      titleBgColor: string = "#fafafa"
      contentBgColor: string = "#eee"
    
      products: Array<IProduct> = [
        {
          id: 1,
          imageURL: "",
          name: "Product 1",
          price: 7599,
          discounted: 500
        }
      ]
    
      build() {
        Column() {
          Row() {
            Button() {
              Image($r('app.media.arrow'))
            }
            .onClick(() => {
              router.back()
            })
            Text("商品列表")
            Blank()
            Button() {
              Image($r('app.media.refresh'))
            }
            .onClick(() => {
              console.log("Refresh")
            })
          }
          .backgroundColor(this.titleBgColor)
          List({ space: 20 }) {
            ForEach(this.products, (item) => {
              ListItem() {
                Row() {
                  Image(item.imageURL)
                  Column({ space: 10 }) {
                    Text(item.name)
                    if(item.discounted) {
                      Text("价格:¥" + item.price)
                        .fontColor("#aaa")
                        .decoration({ type: TextDecorationType.LineThrough})
                      Text("折后价:¥" + (item.price - item.discounted))
                      Text("优惠:¥" + item.discounted)
                    } else {
                      Text("价格:¥" + item.price)
                    }
                  }
                  .layoutWeight(1)
                }
              }
              .border({ width: 2, style: BorderStyle.Solid, color: this.contentBgColor, radius: 20 })
            })
          }
        }
        .backgroundColor(this.contentBgColor)
      }
    }
    
  • 举例 2:通讯录

    interface IAddressItem {
      group: string,
      contactList: string[]
    }
    
    @Entry
    @Component
    struct Index {
      addressBook: IAddressItem[] = [
        {
          group: "家人",
          contactList: ["张三", "李四"]
        },
        {
          group: "朋友",
          contactList: ["王五", "赵六"]
        },
        {
          group: "同事",
          contactList: ["田七"]
        }
      ]
    
      @Builder
      groupHeader(group: string) {
        Text(group)
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
      }
    
      build() {
        Column() {
          Text("通讯录")
            .fontSize(50)
            .fontWeight(FontWeight.Bolder)
          List({ space: 20 }) {
            ForEach(this.addressBook, (item:IAddressItem) => {
              ListItemGroup({ header: this.groupHeader(item.group) })
              ForEach(item.contactList, (item:string) => {
                ListItem() {
                  Text(item)
                    .fontSize(20)
                }
              })
            })
          }
        }
        .width("100%")
        .height('100%')
        .padding({ left: 10, right: 10 })
      }
    }
    

h. 自定义对话框

  1. 构建自定义对话框组件

    @CustomDialog
    struct MyDialog {
      controller: CustomDialogController
      build() {
        Column() {
          Text("自定义对话框")
          Button("关闭对话框")
            .onClick(() => {
              this.controller.close()
            })
        }
      }
    }
    
  2. 将对话框组件注册到页面中

    @Entry
    @Component
    struct Index {
      controller: CustomDialogController = new CustomDialogController({
        builder: MyDialog({})
      })
    }
    
  3. 绑定点击事件触发对话框

    build() {
      Column() {
        Button("开启对话框")
          .onClick(() => {
            this.controller.open()
          })
      }
      .width('100%')
      .height('100%')
    }
    

i. 自定义导航

  • 语法:Tabs(value?: {barPosition?: BarPosition, index?: number, controller?: TabsController})

  • 举例:

    @Component
    struct AComponent {
      build() {
        Text("A 组件内容")
      }
    }
    
    @Component
    struct BComponent {
      build() {
        Text("B 组件内容")
      }
    }
    
    @Component
    struct CComponent {
      build() {
        Text("C 组件内容")
      }
    }
    
    @Entry
    @Component
    struct Index {
      @State currentIndex: number = 0
    
      @Builder
      customTabBarContent(icon: Resource, title: string, index: number) {
        Column({ space: 6 }) {
          Image(icon)
            .width(20)
            .fillColor(this.currentIndex == index ? Color.Green : Color.Black)
          Text(title)
            .fontSize(16)
            .fontColor(this.currentIndex == index ? Color.Green : Color.Black)
        }
      }
    
      build() {
        Column() {
          Tabs() {
            TabContent() {
              AComponent()
            }.tabBar(this.customTabBarContent($r("app.media.icon"), "A 组件", 0))
            TabContent() {
              BComponent()
            }.tabBar(this.customTabBarContent($r("app.media.icon"), "B 组件", 1))
            TabContent() {
              CComponent()
            }.tabBar(this.customTabBarContent($r("app.media.icon"), "C 组件", 2))
          }
          .barPosition(BarPosition.End)
          .vertical(false)		// 不使用垂直布局
          .scrollable(false)	// 关闭页面滑动切换
          .onChange((index: number) => {
            this.currentIndex = index
          })
        }
        .width("100%")
        .height("100%")
      }
    }
    

(2)组件化开发

  • 组件化:将整个页面分割为多个部分,并使用单独的组件描述每个部分,使得一个页面由多个组件构成

a. @Builder 自定义构建函数

  • 构建函数中只能写入组件

  • 语法:

    @Builder
    函数名(参数) {
      函数体;
    }
    
  • 自定义构建函数可以在 build() 中调用

  • 完善电商平台商品列表

    @Builder
    header() {
      Row() {
        Button() {
          Image($r('app.media.arrow'))
        }
        .backgroundColor(this.titleBgColor)
        .onClick(() => {
          router.back()
        })
        Text("商品列表")
        Blank()
        Button() {
          Image($r('app.media.refresh'))
        }
        .backgroundColor(this.titleBgColor)
        .onClick(() => {
          console.log("Refresh")
        })
      }
      .backgroundColor(this.titleBgColor)
    }
    
    @Builder
    productCard(item:IProduct) {
      Row() {
        Image(item.imageURL)
        Column({ space: 10 }) {
          Text(item.name)
          if(item.discounted) {
            Text("价格:¥" + item.price)
              .fontColor("#aaa")
              .decoration({ type: TextDecorationType.LineThrough})
            Text("折后价:¥" + (item.price - item.discounted))
            Text("优惠:¥" + item.discounted)
          } else {
            Text("价格:¥" + item.price)
          }
        }
      }
    }
    
    build() {
      Column() {
        this.header()
        List({ space: 20 }) {
          ForEach(this.products,
            (item) => {
              ListItem() {
                this.productCard(item)
              }
              .border({ width: 2, style: BorderStyle.Solid, color: this.contentBgColor, radius: 20 })
            },
            (item:IProduct) => {
              return item.id.toString()
            })
        }
      }
      .backgroundColor(this.contentBgColor)
    }
    

b. @Component 自定义组件

  • 自定义构建函数仅能在当前组件中使用,无法复用到其他组件,因此需要自定义组件

  • 一般写在 ets/components 目录下

  • 完善电商平台商品列表

    • components/Header.ets

      import router from '@ohos.router'
      
      @Component
      export default struct Header {
        title: string = "Undefined"
      
        titleBgColor: string = "#fafafa"
        contentBgColor: string = "#eee"
      
        build() {
          Row() {
            Button() {
              Image($r('app.media.arrow'))
            }
            .backgroundColor(this.titleBgColor)
            .onClick(() => {
              router.back()
            })
            Text(this.title)
            Blank()
            Button() {
              Image($r('app.media.refresh'))
            }
            .backgroundColor(this.titleBgColor)
            .onClick(() => {
              console.log("Refresh")
            })
          }
          .backgroundColor(this.titleBgColor)
        }
      }
      
    • entry/Page.ets

      build() {
        Column() {
          Header({ title: "商品列表"  })
          // ...
        }
      }
      
  • 自定义组件使用成本更高,但复用性更强,且其中数据独立

c. @BuilderParam 构建参数

  • 将自定义构建函数作为参数传递到自定义组件

  • 完善电商平台商品列表

    • components/Header.ets

      @Component
      export default struct Header {
        // ...
        @BuilderParam
        rightItem: () => void
      
        build() {
          Row() {
            // ...
            this.rightItem()
          }
        }
      }
      
    • entry/Page.ets

      import Header from '../components/Header'
      
      @Entry
      @Component
      struct Page {
        // ...
      
        @Builder
        refreshButton() {
          Button() {
            Image($r('app.media.refresh'))
          }
          .onClick(() => {
            console.log("Refresh")
          })
        }
      
        build() {
          Column() {
            Header({ title: "商品列表", rightItem: this.refreshButton  })
            // ...
          }
        }
      }
      

    完整代码:https://gitee.com/srigt/harmony/tree/master/商品列表

(3)页面布局

a. 线性布局

  • Row:行布局,从左至右
    • 主轴:从左至右
    • 侧轴(交叉轴):从上至下
  • Column:列布局,从上至下
    • 主轴:从上至下
    • 侧轴(交叉轴):从左至右
  • 主轴使用 justifyContent(FlexAlign.*) 调整对齐,FlexAlign 枚举包括:
    • Start:从开始处(默认)
    • Cneter:居中
    • End:从结束处
    • SpaceBetween:均分且开始和结束处不留空间
    • SpaceAround:均分且间隔比为 \(0.5:1:1:\ \ldots\ :1:0.5\)
    • SpaceEvenly:均分且间隔空间相同
  • 侧轴使用 aligmItems 调整对齐,分为:
    • VerticalAlignRow 行布局,其枚举包括:
      • Top:从顶部
      • Center:居中(默认)
      • Bottom:从底部
    • HorizontalAlignColumn 列布局,其枚举包括:
      • Start:从开始处
      • Center:居中(默认)
      • End:从结束处
  • layoutWeight:填充父容器主轴方向的空闲空间

b. 层叠布局

  • 子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件

  • 语法:Stack(value?: { alignContent?: Alignment })

  • 举例:

    @Entry
    @Component
    struct Index {
      build() {
        Stack({}) {
          Column() {}
          .width('100%')
          .height('100%')
          .backgroundColor(Color.Red)
          Row() {}
          .width("50%")
          .height("50%")
          .backgroundColor(Color.Green)
        }
      }
    }
    

c. 网格布局

  • 行列分割的单元格所组成,通过指定项目所在的单元格完成布局

  • 语法:Grid(scroller?: Scroller)

  • 类似 List 组件,Grid 要求其中每一项的子组件包含在 GridItem

  • 常用属性:

    • rowsTemplate():行模板,设置每行的模板,包括列数与列宽
    • columnsTemplate():列模板,设置每列的模板,包括行数与行宽
    • rowsGap():行间距
    • columnsGap():列间距
  • 举例:

    @Entry
    @Component
    struct Page {
      array: number[] = [1, 2, 3, 4, 5, 6]
      build() {
        Grid() {
          ForEach(this.array, (item: number) => {
            GridItem() {
              Text(item.toString())
                .width("100%")
                .height(100)
                .border({
                  width: 2,
                  color: Color.Black
                })
                .fontSize(30)
            }
          })
        }
        .width("100%")
        .height(220)
        .rowsTemplate("1fr 1fr")
        .columnsTemplate("1fr 1fr 1fr")
        .rowsGap(10)
        .columnsGap(10)
      }
    }
    

(4)数据请求

  • 一般数据请求步骤

    1. 导入对应模块

      import http from '@ohos.net.http';
      
    2. 在方法中,创建 HTTP 请求对象

      import http from '@ohos.net.http';
      
      @Component
      struct Index {
        httpHandler() {
          let httpRequest = http.createHttp()
        }
        // ...
      }
      
    3. 调用 request(url, options) 发送请求

      httpHandler() {
        let httpRequest = http.createHttp()
        let promise = httpRequest.request(
          "http://example.com",
          {
            method: http.RequestMethod.GET
          }
        )
      }
      
      • url 为请求地址、options 为请求配置
      • 一个 HTTP 请求对象仅能调用一次 request() 方法
    4. 获取响应结果

      httpHandler() {
        let httpRequest = http.createHttp()
        let promise = httpRequest.request(
          "http://example.com",
          {
            method: http.RequestMethod.GET
          }
        )
        promise.then(
          (httpResponse:http.HttpResponse) => {
            console.log('Result: ' + httpResponse.result.toString());
          }
        )
      }
      
  • 默认采用异步方式请求

    • 可以使用 asyncawait 变为同步

      Button("登录")
        .fontSize(22)
        .onClick(async () => {
          let httpRequest = http.createHttp()
          let response = await httpRequest.request(
            `http://10.200.21.163:8080/login?username=${this.username}&password=${this.password}`,
            {
              method: http.RequestMethod.GET
            }
          )
          console.log(response.result.toString())
        })
      
  • 完善登录页面

    Button("登录")
      .fontSize(22)
      .onClick(() => {
        let httpRequest = http.createHttp()
        let promise = httpRequest.request(
          `http://localhost:8080/login?username=${this.username}&password=${this.password}`,
          {
            method: http.RequestMethod.GET
          }
        )
        promise.then((httpResponse:http.HttpResponse) => {
          console.log(httpResponse.result.toString())
        })
      })
    

(5)动画效果

  • 通过设置关键帧实现动画效果

  • 使用 animateTo(value: AnimateParam, event: () => void): void 方法

    • value:对象类型,用于配置动画参数,包括延时、变化曲线等
    • event:回调函数,用于配置动画关键帧的数据
  • 举例:

    @Entry
    @Component
    struct Index {
      @State scaleX: number = 0
      @State scaleY: number = 0
      build() {
        Column({ space: 30 }) {
          Button("开始动画")
            .margin(30)
            .onClick(() => {
              animateTo({ duration: 500 }, () => {
                this.scaleX = 1
                this.scaleY = 1
              })
            })
          Row()
            .width(200)
            .height(200)
            .backgroundColor(Color.Red)
            .scale({
              x: this.scaleX,
              y: this.scaleY
            })
        }
        .width("100%")
        .height("100%")
      }
    }
    

0x03 渲染控制

(1)条件渲染

  • 使用 ifelseelse if 语句

  • ifelse if 后跟随的条件语句可以使用状态变量

  • 调整登录按钮

    Button() {
      Row() {
        if(this.isLoading) {
          LoadingProgress()
            .width(30)
            .color(Color.White)
        } else {
          Text("登录")
            .fontSize(22)
            .fontColor(Color.White)
        }
      }
    }
    

(2)循环渲染

  • 使用 ForEach 语句

  • 语法:

    ForEach(
      arr: Array,
      itemGenerator: (item: any, index: number) => void,
      keyGenerator?: (item: any, index: number) => string
    )
    
    • arr:数组,数组包含多个元素,数组长度决定组件渲染个数
    • itemGenerator:子组件生成函数,用于生成页面组件,参数分别为数组每项的值与索引
    • keyGenerator:(可选)键值生成函数,用于指定每项的 id:string,参数分别为数组每项的值与索引
  • 举例:

    @Entry
    @Component
    struct Index {
      students:string[] = ["Alex", "Bob", "Charles", "David"]
    
      build() {
        Column() {
          ForEach(this.students, (item:string, index:number) => {
            Row() {
              Text(index.toString())
                .fontSize(50)
              Blank()
              Text(item)
                .fontSize(50)
            }
            .width("80%")
          })
        }
        .width('100%')
        .height('100%')
      }
    }
    

(3)数据懒加载

  • 使用 LazyForEach 语句

  • 语法:

    LazyForEach(
      dataSource: IDataSource,
      itemGenerator: (item: any, index: number) => void,
      keyGenerator?: (item: any, index: number) => string
    ): void
    
  • 用法与 ForEach 类似,其中数据源为 IDataSource

    interface IDataSource {
      totalCount(): number;
      getData(index: number): Object;
      registerDataChangeListener(listener: DataChangeListener): void;
      unregisterDataChangeListener(listener: DataChangeListener): void;
    }
    
    • totalCount:获得数据总数
    • getData:获取索引值对应的数据
    • registerDataChangeListener:注册数据改变的监听器
    • unregisterDataChangeListener:注销数据改变的监听器

0x04 状态管理

(1)@State

  • 状态:组件中的需要 @State 修饰器修饰的数据

  • 特点:状态数据会通过声明式 UI 组件的方式展示到页面中,并且数据的变化会被 ArkUI 底层实时监控

  • 如果 @State 装饰的变量是对象,则 ArkUI 会监视对象和其中属性值的变化

  • 如果属性值是对象,且该对象的值发生了变化,则可以使用以下方法监视:

    • 重新 new 一个对象

    • 使用 @Observed 搭配 @ObjectLink

      @Observed
      class Car {
        name: string
        price: number
      
        constructor(name: string, price: number) {
          this.name = name
          this.price = price
        }
      }
      
      class Person {
        name: string
        car: Car
      
        constructor(name: string, car: Car) {
          this.name = name
          this.car = car
        }
      }
      
      @Component
      struct CarInfo {
        @ObjectLink car: Car
        build() {
          Text(`车名:${this.car.name}\n车价:${this.car.price}`)
        }
      }
      
      @Entry
      @Component
      struct Index {
        person: Person = new Person("张三", new Car("智界S7", 210000))
      
        build() {
          Column() {
            Text(`姓名:${this.person.name}`)
            CarInfo({ car: this.person.car })
            Button("车价减 1000")
              .onClick(() => {
                this.person.car.price -= 1000
              })
          }
        }
      }
      
  • 如果传递方法,需要绑定 this

    • 举例:待办列表

      @Observed
      class TodoItem {}
      
      @Component
      struct TodoComponent {
        @ObjectLink item: TodoItem
        index: number
        remove: (index: number) => void
      
        customSize: number
        build() {
          Row() {
            Button() {
              Image($r("app.media.todo"))
                .width(this.customSize)
            }
            Text(this.item.name)
            Blank()
            Button() {
              Image($r('app.media.remove'))
                .width(this.customSize)
            }
            .onClick(() => {
              this.remove(this.index)
            })
          }
        }
      }
      
      @Entry
      @Component
      struct Index {
        @State TodoList: TodoItem[] = []
        customSize: number = 25
        newItemName: string = ""
      
        remove(index: number) {
          this.TodoList.splice(index, 1)
        }
      
        @Builder
        Header() {}
      
        build() {
          Column({ space: 20 }) {
            Text("待办列表")
            this.Header()
            List({ space: 16 }) {
              ForEach(this.TodoList, (item: TodoItem, index: number) => {
                ListItem() {
                  TodoComponent({
                    customSize: this.customSize,
                    item: item,
                    index: index,
                    remove: this.remove.bind(this)
                  })
                }
              })
            }
          }
        }
      }
      

      完整代码:https://gitee.com/srigt/harmony/tree/master/待办列表

(3)@Prop

  • @Prop 专门用于处理父子组件的之间单向的数据传递

    • 子组件数据变化不会影响父组件
  • 举例:

    @Component
    struct Child {
      @Prop message: string
    
      build() {
        Text(`Child: ${this.message}`)
          .fontSize(20)
      }
    }
    
    @Entry
    @Component
    struct Parent {
      @State message: string = 'Hello World'
    
      build() {
        Column({ space: 30 }) {
          Text(`Parent: ${this.message}`)
            .fontSize(20)
            .onClick(() => {
              this.message = 'Changed'
            })
          Child({ message: this.message })
        }
        .width('100%')
        .height('100%')
      }
    }
    
    • 当触发点击事件后,父组件 message 的值发生了变化,在 @Prop 的作用下,子组件也随着变化重新渲染
  • 区别于 @State@Prop 不需要在子组件初始化,而是等待来自父组件的数据

  • @Prop 只能装饰简单类型的属性

  • @Prop 的原理是:将父组件的属性值复制一份到子组件

  • @Link@Prop 作用相同

    • 相同:都专门用于处理父子组件的之间的数据传递
    • 不同:@Link 可以双向数据传递,并且可以装饰任何类型的属性
  • 举例:

    @Component
    struct Child {
      @Link message: string
    
      build() {
        Text(`Child: ${this.message}`)
          .fontSize(20)
          .onClick(() => {
            this.message = 'Changed'
          })
      }
    }
    
    @Entry
    @Component
    struct Parent {
      @State message: string = 'Hello World'
    
      build() {
        Column({ space: 30 }) {
          Text(`Parent: ${this.message}`)
            .fontSize(20)
          Child({ message: $message })
        }
        .width('100%')
        .height('100%')
      }
    }
    
    • 当触发点击事件后,子组件 message 的值发生了变化,在 @Link 的作用下,父组件也随着变化重新渲染
  • 如果在子组件使用 @Link,则父组件传递时,需要使用 $,如 子组件({ 子组件属性名: $父组件属性名 })

  • @Link 的原理是,将父组件属性的地址值传递给子组件

(5)@Provide 与 @Consume

  • @Provide@Consume 搭配使用,实现任意组件之间双向的数据传递

  • 采用隐式数据传递

    • 提供方组件仅负责提供数据,而不指定目标组件
    • 消费方组件直接消费来自提供方组件的数据
  • 提供方可以为数据配置别名

  • 举例:

    @Component
    struct Grandchild {
      @Consume msg: string
      build() {
        Text(`Grandchild: ${this.msg}`)
          .fontSize(20)
          .onClick(() => {
            this.msg = 'Change from grandchild'
          })
      }
    }
    
    @Component
    struct Child {
      build() {
        Grandchild()
      }
    }
    
    @Entry
    @Component
    struct Parent {
      @Provide('msg') message: string = 'Hello World'
    
      build() {
        Column({ space: 30 }) {
          Text(`Parent: ${this.message}`)
            .fontSize(20)
            .onClick(() => {
              this.message = 'Change from parent'
            })
          Child()
        }
        .width('100%')
        .height('100%')
      }
    }
    
  • 此方法对性能有所损耗(缺点)

(6)@Watch 监视器

  • 监视对象是组件中的数据,当数据发生改变,监视器就会触发相应的方法

  • 举例:

    @Entry
    @Component
    struct Index {
      @State @Watch('calcTotal') array: number[] = [0, 1, 2, 3]
      @State total: number = 0
    
      calcTotal(): void {
        this.total = 0
        this.array.forEach(element => this.total += element);
      }
    
      aboutToAppear() {
        this.calcTotal()
      }
    
      build() {
        Column() {
          Text(`数组全部元素和为:${this.total}`)
          Button("向数组添加元素")
            .onClick(() => {
              this.array.push(10)
            })
        }
        .width("100%")
        .height('100%')
      }
    }
    

0x05 页面路由

(1)概念

  • 路由:一种实现在一个应用程序中页面之间相互跳转与数据传递的技术
  • 页面栈:一个类似栈的页面容器,当前页面为栈底页面
    • 为防止页面栈溢出,页面栈中最多存放 32 个页面

(2)使用步骤

  1. src/main/resources/base/profile/main_pages.json 中注册路由

    {
      "src": [
        "pages/A",
        "pages/B"
      ]
    }
    
  2. 在页面中引入路由模块:import router from '@ohos.router'

  3. 在 A 页面调用方法:pushUrl(options: RouterOptions): Promise<void>

    • 其中,RouterOptions 为:

      interface RouterOptions {
        url: string;
        params?: Object;
      }
      
    import router from '@ohos.router'
    
    @Entry
    @Component
    struct A {
      build() {
        Column() {
          Text("A 页面")
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
          Button("跳转到 B 页面")
            .onClick(() => {
              router.pushUrl({ url: 'pages/B' })
            })
        }
        .width("100%")
        .height("100%")
      }
    }
    
    • 也可以使用 replaceUrl(options: RouterOptions): Promise<void>,区别在于 replaceUrl 方法会替换栈顶页面,导致无法使用下述 back 方法返回上一个页面
  4. 在 B 页面调用方法:back(options?: RouterOptions ): void

    import router from '@ohos.router'
    
    @Entry
    @Component
    struct B {
      build() {
        Column() {
          Text("B 页面")
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
          Button("返回")
            .onClick(() => {
              router.back()
            })
        }
        .width('100%')
        .height('100%')
      }
    }
    
  5. 使用 A 页面传递参数

    router.pushUrl({
      url: 'pages/B',
      params: {
        name: "张三",
        age: 18
      }
    })
    
  6. 在 B 页面接受参数

    interface IParams {
      name: string
      age: number
    }
    
    @Entry
    @Component
    struct B {
      @State person: IParams = {
        name: "",
        age: 0
      }
      aboutToAppear() {
        this.person = router.getParams() as IParams
      }
      build() {
        Column() {
          // ...
          Text(`${this.person.name} - ${this.person.age}`)
        }
      }
    }
    

(3)路由模式

  • 路由模式有两种,包括:

    • 单例模式:Single,每个页面仅创建一次
    • 标准模式:Standard,每次都创建新的页面实例
  • 路由模式在 pushUrl 方法的第二参数中指定,如:

    router.pushUrl(
      { url: 'pages/B' },
      router.RouterMode.Single
    )
    

综合案例

认证

HarmonyOS应用开发者高级认证

-End-

posted @ 2024-04-26 09:47  SRIGT  阅读(693)  评论(1编辑  收藏  举报