• 鸿蒙系统应用基础开发


    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. 项目目录

    app
    AppScope
    entry
    src
    main
    ohosTest
    ets
    resource
    entryability
    pages
    EntryAbility.ts
    Index.ets
    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:  :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

      • 其中,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,区别在于 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
      )
      

    综合案例

    -End-

  • 相关阅读:
    软件生命周期过程
    小微企业可以申请高新技术企业吗?
    Adobe XD文件转PDF、再转成一张大图的办法
    最详细Pycharm远程代码调试配置方案【针对GPU集群】
    如何在nginx上设置html不缓存
    基于网络爬虫的新闻实时监测分析可视化系统(Java+MySQL+Web+Eclipse)
    CentOS7系统搭建web环境 php&nginx&pgsql
    188. 买卖股票的最佳时机 IV
    阿里P8大佬,耗时72小时整理的Docker实战笔记,你值得拥有
    【linux驱动】简单字符设备驱动
  • 原文地址:https://www.cnblogs.com/SRIGT/p/18159320