HarmonyOS鸿蒙开发学习:鸿蒙基础-基础环境-ArkTS-组件-样式

鸿蒙基础-基础环境-ArkTS-组件-样式

DevEcoStudio编辑器下载链接

链接: https://pan.baidu.com/s/18C9i35YPh4GsHpbSif8KQw?pwd=d6e9 提取码: d6e9

安装教程

下载对应的版本

  • windows
  • mac英特尔
  • mac Arm

API12 的编辑器

API12的模拟器(mac英特尔安装API11)

  • Mac Arm芯片版本安装

解压编辑器版本

将左侧内容拖入

点击打开

  • 打开项目的配置项


新建一个路径 HarmonyOS-SDK

将资源下载包中的sdk的内容进行解压,放入到刚刚新建的目录

将设置中的安装路径改变成新建的目录,最终点击确认,sdk就安装好了

打开原有项目发现

一个配置文件的依赖,线上没有提供云下载地址,只能采用离线的版本 。hvigor

下载资源包中,还有提供一个dependencies, 这就是我们需要替换的版本



安装模拟器

  • 需要实名登录
  • 安装插件


internal-plugin-SNAPSHOT.zip

如果大家的mac安装不上 这个插件,可以采用文档中提供的,下载之后安装即可

下载资源中的模拟器,拷贝到sdk的目录

注意: 模拟器的名字是小写的

  • 创建镜像目录

注意

** 如果用的是API12的模拟器,需要把镜像放入到HarmonyOS-NEXT-DB1 目录下。**

** 如果用的是API11的模拟器,需要把镜像放入到HarmonyOS-NEXT-DP2 目录下。**

system-image

HarmonyOS-NEXT-DB1

phone_arm/phone_x86

HarmonyOS-NEXT-DP2

phone_arm/phone_x86

Windows安装模拟器

起步

起步-鸿蒙简介

  • HarmonyOS 是新一代的智能终端操作系统,为不同设备的智能化、互联与协同提供了统一的语言。带来简洁,流畅,连续,安全可靠的全场景交互体验。

历程:

时间 事件
2019 HarmonyOS 1.0,华为在东莞举行华为开发者大会,正式发布操作系统鸿蒙 OS,主要用于物联网
2020 HarmonyOS 2.0,基于开源项目 OpenHarmony 开发的面向多种全场景智能设备的商用版本
2021 HarmonyOS 3.0,先后优化游戏流畅度、地图三维体验、系统安全,另外系统的稳定性也得到了增强
2023.2 HarmonyOS 3.1,系统纯净能力进一步提升,对后台弹窗、 隐藏应用、后台跳转等情况
2023.7 华为 Mate 50 系列手机获推 HarmonyOS 4.0
2024 HarmonyOS Next 开发者预览版发布,将不在兼容安卓应用

起步-DevEco Studio

安装 DevEco Studio 编辑器

DevEcoStudio编辑器下载链接

链接: https://pan.baidu.com/s/1TyrmbTkrOEsTB8HcaMR4og?pwd=fjjw 提取码: fjjw

  1. 安装:DevEco Studio → 一路 Next(只演示windows)






  1. 运行: 选择not import System Img

这里最好别选在C盘

  • 下一步


  • 下载sdk
  • 安装完成
  • 安装完成
  • 创建一个新项目
  • 填写信息

等待创建完成

  • 安装中文插件(windows)
  • Mac的选择
  • 选择Plugins
  • 点OK重启
  • 看到效果

起步-认识工作区

  • 通过左侧目录找到对应的应用文件,在编辑区进行代码编写,在右侧看预览效果
  • 连按两下shift,可以快速寻找文件

起步-如何排错

写代码时,我们会经常遇到这种情况,右侧出现不能够开启预览器的提示,让我们打开预览器日志看错误

  • 解决该问题的思路 1. 按照编辑器提示的,打开预览器日志
  • 如果是明确的语法错误或者api错误,编辑器会指出我们代码的行数,我们可以点击提示的代码行,直接跳到对应位置,直接检查代码的问题
  • 跳到对应的位置

解决思路2: 如果当前文件不多,可以点开你创建的所有文件,查看文件中是否有报错的地方,文件报错,在右侧以及代码区会有明显的报错提示

解决思路3: 通过统一构建,暴露哪些文件及代码无法编译通过

如何刷新看效果

预览器是有热更新的

  • 原则上写完右侧自动更新
  • 如果不自动刷新- 语法错了,编辑器卡住了
  • 直接点击刷新按钮
  • 刷新只能针对 带有@Entry和@Preview的文件,否则无法看到效果

起步-审查和多设备预览

效果预览方法:

info

  • Preview(预览器)
  • 本地模拟器(只有Mac(ARM)芯片)
  • 远程模拟器
  • 云手机(需要审核及申请,暂无消息)
  • 本地真机(Meta60-Meta60Pro X5或者是装载OpenHarmony的工程机(价格较低,但是依然存在一些问题))

danger

只有装了Next预览版系统的手机才可以进行真机调试和预览,目前Next预览器系统装机量有限,需要申请和审核,小道消息Q2季度会进一步扩大开放名额

  • 预览器的多设备预览

danger

2in1的意思是 平板电脑二合一的状态

  • 审查元素
  • 真机预览调试

info

  • 刷了Next预览版本的手机插上数据线,连接到电脑之后(注意:这里需要开启手机的usb调试,设置中搜索usb调试,打开,并且处理开发者模式,打开手机设置-关于手机,连续点击手机系统的版本号,直到出现您已处于开发者模式的提示未知)

  • 运行

    info

    点击绿色三角- 运行到真机

  • debugger模式

    info

    点击小虫子-断点调试到真机(4.0版本中真机不支持断点调试-next真机支持断点调试)

起步-工程目录结构

info

我们详解下目录结构

  • AppScope > app.json5:应用的全局配置信息。

  • **entry:**应用/服务模块,编译构建生成一个HAP。

    • src > main > ets:用于存放ArkTS源码。
    • src > main > ets > entryability:应用/服务的入口。
    • src > main > ets > pages:应用/服务包含的页面。
    • **src > main > resources:**用于存放应用/服务模块所用到的资源文件,如图形、多媒体、字符串、布局文件等。
    • src > main > module.json5:Stage模型模块配置文件,主要包含HAP的配置信息、应用在具体设备上的配置信息以及应用的全局配置信息。
  • **entry > build-profile.json5:**当前的模块信息、编译信息配置项,包括buildOption、targets配置等。

  • entry > hvigorfile.ts:模块级编译构建任务脚本。

  • entry >oh-package.json5:配置三方包声明文件的入口及包名。

  • oh_modules :用于存放三方库依赖信息,包含应用/服务所依赖的第三方库文件。关于原npm工程适配ohpm操作,请参考历史工程适配OHPM包管理

  • **build-profile.json5:**应用级配置信息,包括签名、产品配置等。

  • **hvigorfile.ts:**应用级编译构建任务脚本。

  • 资源目录结构

什么是Stage模型

info

通过上层的目录结构我们要分析出如下关系

State

-Module(模块-对应Hap包)

-ability(应用服务入口)

-pages(页面)

-component(自定义组件)

-resources(资源)

应用模型Stage&FA

鸿蒙的战略 兼容安卓-把java + 前端拉入到开发阵容中-FA模型
官方介绍

  • 应用模型是HarmonyOS为开发者提供的应用程序所需能力的抽象提炼,它提供了应用程序必备的组件和运行机制。有了应用模型,开发者可以基于一套统一的模型进行应用开发,使应用开发更简单、高效。

换言之- 应用模型是鸿蒙开发一切的基础,因为只有基于该应用模型我们才可以开发对应的应用和业务。

应用模型包含几个要素**应用组件-**应用进程-应用线程-应用任务管理-应用配置文件

提问 :应用模型是只有一个吗?
回答 :鸿蒙前后推出了两种应用模型- FA(Feature Ability),Stage,目前FA已经不再主推

  • HarmonyOS Next也将Stage模型作为主推模型,所以我们本次训练营将学习Stage模型相关的应用开发能力。

下面是官方的Stage模型概念图

总结:应用模型是开发鸿蒙应用的基础底座,但是鸿蒙先后推出了FA和Stage,鸿蒙4.0和鸿蒙Next都将Stage作为主推方向,所以我们主要基于Stage模型来学习和开发我们目前的应用

什么是UIAbility-(界面能力组件)

从上一个小节中,我们发现Stage模型提到了UIAbility组件包含UI界面绘制,主要和用户交互。

  • UIAbility组件是一种包含UI界面的应用组件,主要用于和用户交互。

官网介绍-UIAbility是系统调度的基本单元,可以给应用提供绘制界面的窗口。

info

UIAbility的设计理念:

  1. 原生支持应用组件级的跨端迁移和多端协同。
  2. 支持多设备和多窗口形态。

UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口。一个应用可以包含一个或多个UIAbility组件。例如,在支付应用中,可以将入口功能和收付款功能分别配置为独立的UIAbility。
每一个UIAbility组件实例都会在最近任务列表中显示一个对应的任务。

对于开发者而言,可以根据具体场景选择单个还是多个UIAbility,划分建议如下:

  • 如果开发者希望在任务视图中看到一个任务,则建议使用一个UIAbility,多个页面的方式。

  • 如果开发者希望在任务视图中看到多个任务,或者需要同时开启多个窗口,则建议使用多个UIAbility开发不同的模块功能。

  • 场景- 支付/小程序/鉴权

为使应用能够正常使用UIAbility,需要在module.json5配置文件abilities标签中声明UIAbility的名称、入口、标签等相关信息。

typescript 复制代码
{
  "module": {
    ...
    "abilities": [
      {
        "name": "EntryAbility", // UIAbility组件的名称
        "srcEntry": "./ets/entryability/EntryAbility.ets", // UIAbility组件的代码路径
        "description": "$string:EntryAbility_desc", // UIAbility组件的描述信息
        "icon": "$media:icon", // UIAbility组件的图标
        "label": "$string:EntryAbility_label", // UIAbility组件的标签
        "startWindowIcon": "$media:icon", // UIAbility组件启动页面图标资源文件的索引
        "startWindowBackground": "$color:start_window_background", // UIAbility组件启动页面背景颜色资源文件的索引
        ...
      }
    ]
  }
}

warning

上述文件不用我们手动填写,我们新建ability的时候,会自动填入

组件基础

组件-什么是ArkTS

info

ArkTS提供了语言运行时相关能力

ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,保持了TS的基本风格,同时通过规范定义强化开发期静态检查和分析,提升程序执行稳定性和性能。

特别注意:尤其是学过鸿蒙4.0的,Next版本极大增强了TS语言中动态类型的限制,几乎不再支持动态类型

从API version 10开始,ArkTS进一步通过规范强化静态检查和分析,对比标准TS的差异可以参考从TypeScript到ArkTS的适配规则

  • 强制使用静态类型:静态类型是ArkTS最重要的特性之一。如果使用静态类型,那么程序中变量的类型就是确定的。同时,由于所有类型在程序实际运行前都是已知的,编译器可以验证代码的正确性,从而减少运行时的类型检查,有助于性能提升。
  • 禁止在运行时改变对象布局:为实现最大性能,ArkTS要求在程序执行期间不能更改对象布局。
  • 限制运算符语义:为获得更好的性能并鼓励开发者编写更清晰的代码,ArkTS限制了一些运算符的语义。比如,一元加法运算符只能作用于数字,不能用于其他类型的变量。
  • 不支持Structural typing:对Structural typing的支持需要在语言、编译器和运行时进行大量的考虑和仔细的实现,当前ArkTS不支持该特性。根据实际场景的需求和反馈,我们后续会重新考虑。
  • 由于文档权限的限制,这里有一份openHarmony的 上的Next版本的ts类型的迁移说明 地址 (内容基本一致)

openHarmony 和HarmonyOS的关系和区别

OpenHarmony是指鸿蒙(HarmonyOS)的开源版本,由华为官方开源,遵循Apache 2.0许可证。OpenHarmony包含了HarmonyOS的核心代码库,但并不包括商业版HarmonyOS中的一些特有功能和技术。

HarmonyOS是华为自主研发的分布式操作系统,旨在打造一个全场景、全终端的智能生态系统。它支持多种设备类型,包括手机、平板、智能手表、智能音箱、电视、汽车、家居等,可以实现设备之间的协同工作和资源共享。

OpenHarmony和HarmonyOS的关系就像是Java和Android的关系一样。可以将OpenHarmony看作是HarmonyOS的底层技术和代码库的一部分,而HarmonyOS则是在此基础上进行了更多的开发和扩展,添加了更多的功能和应用。相比之下,OpenHarmony更加开放和自由,可以被开发者用于构建各种类型的设备和应用,而HarmonyOS则更加注重整合、统一和优化整个生态系统。

  • ArkTS的特性-扩展能力
  1. **基本语法 **
    • 定义声明式UI、自定义组件、动态扩展UI元素;
    • 提供ArkUI系统组件,提供组件事件、方法、属性;
    • 共同构成 UI 开发主体
  2. **状态管理 **
    • 组件状态、组件数据共享、应用数据共享、设备共享;
  3. **渲染控制 **
    • 条件渲染、循环渲染、数据懒加载;

ArkTS以声明方式组合和扩展组件来描述应用程序的UI,同时还提供了基本的属性、事件和子组件配置方法,帮助开发者实现应用交互逻辑。

  • 命令式UI- document.createElement("div")-
  • 声明式UI

下图是关于ArtTS的一个整体的应用架构(官网)

info

总结:

  • AktTS提供原有前端范畴内的一切TypeScript和JavaScript的类型及方法支持
  • Next版本加了很多限制-要看迁移指南
  • ArkTS采用声明式UI的方法来绘制页面,设置属性,绑定事件

ArkTS重点迁移说明

  • 对象字面量不能用于类型声明
  • 不支持in操作符
  • 不支持解构赋值
  • 不支持通过索引访问字段


有一种情况下,可以通过索引访问字段,即传入的参数类型为object, 但是传入时必须用class的类型传入,如下

typescript 复制代码
@Entry
@Component
struct Index {
  @State message: string = 'Hello World1';
  test (obj: object) {
   console.log(Object.keys(obj).map(item => `${item}=${obj[item]}`).toString())
  }
  aboutToAppear(): void {
    let o = new TestObj()
    this.test(o)
  }
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}
class TestObj {
  a: number = 0
  b: string = "123"
}

以上场景一般用作请求封装时,无法确定侦测传入参数的类型时使用

  • 部分支持延展运算符
  • 不支持bind/call/apply改变this指向

this始终指向调用者。谁调用这个方法,this指向谁

基础-组件结构

info

接下来,我们来解析我们的UI的结构

ArkTS通过装饰器 @Component@Entry 装饰 struct 关键字声明的数据结构,构成一个自定义组件。

自定义组件中提供了一个 build 函数,开发者需在该函数内以链式调用的方式进行基本的 UI 描述,UI 描述的方法请参考 UI 描述规范。

  • struct-自定义组件基于struct实现

要想实现一段UI的描述,必须使用struct关键字来声明- 注意不能有继承关系-组件名不能系统组件名重名

语法: struct 组件名 {}

typescript 复制代码
@Component
struct Index {
  
}
@CustomDialog 
struct Index2 {
  
}

info

struct关键字声明的UI描述-必须被@Component或者@CustomDialog修饰

  • Component修饰符

Component装饰器只能修饰struct关键字声明的结构,被修饰后的struct具备组件的描述(渲染)能力

  • build函数

用于定义组件的UI描述,一个struct结构必须实现build函数

typescript 复制代码
@Component
struct MyComponent {
  build() {
  }
}

info

注意:

1.build函数是组件(Component)必须提供以及实现的一个函数,build函数可以没有内容,如果有的话,必须有且只有一个容器组件(可以放置子组件的组件)- 只有entry里面有限制- component里面没有限制

2.Component的组件build函数 可以放没有子组件的组件

  • 常见容器组件- Flex-Column-Row-List-Grid-Panel

  • entry修饰符

entry将自定义组件定义为UI页面的入口,也就是我们原来前端常说的一个页面,最多可以使用entry装饰一个自定义组件(在一个ets文件中)-如下面的代码就是不被允许的

typescript 复制代码
@Entry
@Component
struct Index {

  build() {
  
  }
}
@Entry
@Component
struct Index2 {
  build() {

  }
}

info

entry修饰的组件,最终会被注册,具体文件位置-main/resources/base/profile/main_pages.json

  1. 自动注册-新建组件时,采用新建Page的方式
  2. 手动注册-新建一个ets文件,自己在main_pages.json中手动添加路径

注意:
如果你手动删除了某一个带entry的组件,你需要手动去main_page中去删除该路径,否则编译会报错

  • 组件复用

在很多情况下,由于业务的复杂度,我们经常会将一个大的业务拆成若干个组件,进行组装,这里我们非常灵活的复用组件,比如

info

  • 我们可以把上图抽象成三个组件- Header- Main- Footer

代码

typescript 复制代码
import { MeiTuanFooter } from '../views/MeiTuan/MeiTuanFooter';
import { MeiTuanHeader } from '../views/MeiTuan/MeiTuanHeader';
import { MeiTuanMain } from '../views/MeiTuan/MeiTuanMain';

@Entry
@Component
struct MeiTuan {
  @State message: string = 'Hello World';

  build() {
    Column() {
      MeiTuanHeader()  // 60
      MeiTuanMain()
        .layoutWeight(1)
        .backgroundColor(Color.Green)
      MeiTuanFooter()  // 60
    }.height('100%')
  }
}

@Preview
@Component
struct MeiTuanHeader {
  build() {
    Row () {
      Text("美团头部")
        .width('100%')
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height(60)
    .backgroundColor(Color.Pink)
  }
}
export { MeiTuanHeader }
@Component
struct MeiTuanMain {
  build() {
    Row() {
      Text("美团中部")
        .fontColor(Color.Red)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
  }
}

export { MeiTuanMain }

@Component
struct MeiTuanFooter {
  build() {
    Row () {
      Text("美团底部")
        .fontColor(Color.White)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height(60)
    .backgroundColor(Color.Blue)
  }
}
export  { MeiTuanFooter  }

info

总结:

  • 一个UI描述必须使用struct来声明,不能继承
  • struct必须被Component或者CustomDialog修饰
  • struct必须实现build方法,build方法可以没有元素,但是有的话有且只有一个可容纳子组件的容器组件(entry修饰的组件)
  • entry修饰符表示该组件是页面级组件,一个文件中只允许修饰一个struct组件
  • 采用分拆组件的形式可以有效解解耦我们的业务

基础-系统组件(ArkUI)

ArkUI: Ability Kit在UIAbility组件可以使用ArkUI提供的组件、事件、动效、状态管理等能力。

这里所指的Kit其实是Next版本中,鸿蒙将各个能力集进行了统一的分类

ArkUI将组件大概分为这么几类

  • 基础组件
  • 容器组件
  • 媒体组件(只有一个Video)
  • 绘制组件
  • 画布组件
  • 高级组件 额外新增
  • 安全组件 额外新增

大家关心的地图功能在Next版本中以API形式呈现

基本组件使用

  • Text 文本组件-(Span子组件 ImageSpan组件)
  • Column 列组件,纵向排列,Flex布局主轴是Y (任意子组件)
  • Row 行组件,横向排列,Flex布局主轴是X (任意子组件)
  • Flex 以弹性方式布局子组件的容器组件。(存在二次布局,官方推荐有性能要求,使用Column和Row代替) (任意子组件)
  • Button 按钮组件 (单子组件)
  • TextInput 输入框组件 (无子组件)
  • Image (无子组件)
  • Button (单个子组件)
  • List (仅支持ListItem子组件)
  • Scroll (仅支持单个子组件)
  • Stack(堆叠容器,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件)
  • Grid(网格容器,由"行"和"列"分割的单元格所组成,通过指定"项目"所在的单元格做出各种各样的布局。仅支持GridItem组件)
  • GridRow(栅格容器组件,仅可以和栅格子组件(GridCol)在栅格布局场景中使用。)
  • GirdCol(栅格子组件,必须作为栅格容器组件(GridRow)的子组件使用。)

组件使用

info

  • 使用组件采用 **组件名() **的语法
  • 有构造参数采用 **组件名(参数)**的语法
  • 组件里放置子组件采用 **组件名() { 子组件的语法 } **的链式语法
  • 组件设置属性采用 **组件名().width().height() **的语法
  • 组件又有属性又有子组件采用 **组件名(){ ... 子组件 }.width().height() **的语法

基础布局

横向布局-采用Row
百分比说明: 鸿蒙的里面的百分比指的是相对当前父容器,并不是当前的手机屏幕
在写宽度高度时,直接写数字默认单位为vp虚拟像素,屏幕会进行适配。

Row组件默认情况下,子组件内容会垂直方向居中-** 内容超出不会换行**

typescript 复制代码
@Entry
@Component
struct ComponentCase {
  @State message: string = 'Hello World'

  build() {
    Column() {
      // css 支持调整布局
      Row({ space: 15 }) {
        Column()
          .width(100)
          .height(200)
          .backgroundColor(Color.Pink)
        Column()
          .width(100)
          .height(200)
          .backgroundColor(Color.Red)
        Column()
          .width(100)
          .height(200)
          .backgroundColor(Color.Blue)
      }
      .width('100%')
      // .justifyContent(FlexAlign.Start)

      .justifyContent(FlexAlign.Center)

      // .justifyContent(FlexAlign.End)
      // .justifyContent(FlexAlign.SpaceBetween)
      // .justifyContent(FlexAlign.SpaceAround)
      // .justifyContent(FlexAlign.SpaceEvenly)
    }
    .width('100%')
    .height('100%')
  }
}

纵向布局

typescript 复制代码
@Entry
@Component
struct ComponentCase {
  @State message: string = 'Hello World'

  build() {
    Column({ space: 10 }) {
      // css 支持调整布局
      Row({ space: 15 }) {
        Column()
          .width(100)
          .height(200)
          .backgroundColor(Color.Pink)
        Column()
          .width(100)
          .height(200)
          .backgroundColor(Color.Red)
        Column()
          .width(100)
          .height(200)
          .backgroundColor(Color.Blue)
      }
      .width('100%')
      // .justifyContent(FlexAlign.Start)
      .justifyContent(FlexAlign.Center)

      Column({ space: 15 }) {
        Column()
          .width(200)
          .height(100)
          .backgroundColor(Color.Pink)
        Column()
          .width(200)
          .height(100)
          .backgroundColor(Color.Red)
        Column()
          .width(200)
          .height(100)
          .backgroundColor(Color.Blue)
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)

    }
    .width('100%')
    .height('100%')
  }
}

自定义组件应用

typescript 复制代码
@Entry
@Component
struct Layout {

  build() {
    Column() {
      RowCase()
      ColumnCase()
    }.height('100%').backgroundColor(Color.Grey)
  }
}

@Component
struct RowCase {
  build() {
    Row() {
      Column().height(150).width(100).backgroundColor(Color.Pink)
      Column().height(150).width(100).backgroundColor(Color.Red)
      Column().height(150).width(100).backgroundColor(Color.Blue)
    }
    .width('100%')
    .alignItems(VerticalAlign.Top)
    .justifyContent(FlexAlign.SpaceAround)
  }
}

@Component
struct ColumnCase {
  build() {
    Column() {
      Column().height(100).width(150).backgroundColor(Color.Pink)
      Column().height(100).width(150).backgroundColor(Color.Red)
      Column().height(100).width(150).backgroundColor(Color.Blue)
    }.height(400).width('100%').justifyContent(FlexAlign.SpaceEvenly)
  }
}
  • 百度的小案例
typescript 复制代码
@Entry
@Component
struct Baidu {

  build() {
    Column({ space: 20 }) {
      Image("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png")
        .width(160)
      Row() {
        TextInput()
          .borderRadius({
            topLeft: 6,
            bottomLeft: 6
          })
          .height(40)
          .layoutWeight(1)
          .backgroundColor(Color.White)
          .border({
             color: "#c4c7ce",
             width: 2
          })
        Button("百度一下")
          .type(ButtonType.Normal)
          .backgroundColor("#516aee")
          .padding({
            left: 10,
            right: 10,
            top: 6,
            bottom: 6
          })
          .translate({
            x: -2
          })
          .borderRadius({
            topRight: 6,
            bottomRight: 6
          })
      }
      .padding({
        left: 10,
        right: 10
      })
      .width('100%')
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}

warning

Row 和Column的布局方式成为线性布局- 不是横向排列就是纵向排列

  • 线性布局中永远不会产生换行
  • 均不支持出现滚动条
  • 横向排列的垂直居中,总行排列的水平居中
  • 主轴-排列方向的轴
  • 侧轴-排列方向垂直的轴

堆叠布局

info

只要在Stack内部-后者永远会覆盖前者

typescript 复制代码
@Entry
@Component
struct Baidu {
  build() {
    Column({ space: 12 }) {
      //   图片
      Row() {
        Stack({alignContent:Alignment.TopEnd}) {
          Image('https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png')
            .width(160)
          Text('鸿蒙版')
            .fontSize(12)
            .offset({
              x:-20,
              y:30
            })
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)

      //   输入框+按钮
      Row() {
        TextInput()
          .layoutWeight(1)
          .border({
            width: 2,
            color: '#ccc'
          })
          .height(40)
          .borderRadius({
            topLeft: 4,
            bottomLeft: 4
          })
          .backgroundColor('#fff')
        Button('百度一下')
          .type(ButtonType.Normal)
          .translate({
            x: -2
          })
          .borderRadius({
            topRight: 4,
            bottomRight: 4
          })
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .justifyContent(FlexAlign.Center)
  }
}

info

Stack的参数 可以设置子组件的排列方式-alignContent

  • Top(顶部)
  • TopStart(左上角)
  • TopEnd(右上角)
  • Start(左侧)
  • End(右侧)
  • Center(中间)
  • Bottom(底部)
  • BottomStart(左下角)
  • BottomEnd(右下角)
typescript 复制代码
@Entry
@Component
struct FontJump {
  build() {
    Row() {
      Stack() {
        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ff2d83b3')
          .translate({
            x:-2,
            y:2
          })
          .zIndex(1)
        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffe31fa9')
          .translate({
            x:2,
            y:-2
          })
          .zIndex(2)

        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ff030000')
          .translate({
            x:0,
            y:0
          })
          .zIndex(3)

      }
      .width('100%')
    }
    .height('100%')
  }
}

弹性布局

typescript 复制代码
@Entry
@Component
struct ComponentCase {
  @State message: string = 'Hello World'
  build() {
    Scroll() {
      Row() {
        Column() {
          Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceAround }) {
            Column()
              .width(100)
              .height(200)
              .backgroundColor(Color.Pink)
            Column()
              .width(100)
              .height(200)
              .backgroundColor(Color.Red)
            Column()
              .width(100)
              .height(200)
              .backgroundColor(Color.Blue)
          }
          .margin({
            top: 200
          })

        }
        .width('100%')
      }
    }

  }
}

warning

Flex布局设置属性设置方向 是通过参数的,并非通过属性

  • 属性?
  • 参数?

网格布局

Grid布局

  • 想要控制屏幕的分栏 分几列, 怎么分 特别像前端的栅格布局
  • Row组件默认情况下,里面的元素的纵向是居中的
  • Column组件默认横向是居中的

info

Grid组件下只能放置GridItem组件

Grid可以设置columnsTemplate和rowsTemplate

columnsTemplate是设置横向的分配,如果设置 1fr 1fr 表示,等分为两份, 如果设置1fr 2fr表示左边一份,右边两份, 在设置columnsTemplate不设置rowsTemplate的情况下,如果内容超出容器区域,会自动出现滚动条 columnsGap设置列和列之间的间距,rowsGap设置行和行之间的间距

typescript 复制代码
@Entry
@Component
struct GridCase {
  build() {
    Grid() {
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
      GridItemCase()
    }
    .width("100%")
    .height("100%")
    .columnsTemplate("1fr 1fr")
    .columnsGap(10)
    .rowsGap(10)
    .padding(10)

  }
}

@Component
struct GridItemCase {
  build() {
    GridItem() {
      Row() {
        Column() {
          Text("内容")
        }
        .width('100%')
      }
      .height(200)
      .borderRadius(4)
      .backgroundColor(Color.Pink)
    }

  }
}

滚动条说明

在基本的布局组件 Column/Row/Flex/Stack中不论内容超出与否,皆不会出现滚动条

  • 出现滚动条的组件
  • Grid
  • List(列表)
  • Scroll(滚动条)
  • Swiper(轮播)
  • WaterFlow(瀑布流)

出现滚动条的前提条件是- 上述组件中的子组件的内容超出了父容器组件的宽度或者高度

  • 使用最基本的Scroll组件出现一个滚动条
  • 先实现基本的布局
typescript 复制代码
@Entry
@Component
struct ScrollCase {
  build() {
    Column() {
      Row()
        .width('100%')
        .height(50)
        .backgroundColor(Color.Red)

      Column() {

      }
      .width('100%')
      .layoutWeight(1)
      .backgroundColor(Color.Orange)

      Row()
        .width('100%')
        .height(50)
        .backgroundColor(Color.Blue)
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .width('100%')
    .height('100%')
  }
}
  • 实现区域滚动
typescript 复制代码
@Entry
@Component
struct ScrollCase {
  build() {
    Column() {
      Row()
        .width('100%')
        .height(50)
        .backgroundColor(Color.Red)
      Scroll(){
        Column() {
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
          ScrollItem()
        }
        .width('100%')
        .backgroundColor(Color.Orange)
      }
      .layoutWeight(1)
      Row()
        .width('100%')
        .height(50)
        .backgroundColor(Color.Blue)
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .width('100%')
    .height('100%')
  }
}
@Component
struct ScrollItem {
  build() {
    Row() {
      Text("滚动区域内容")
    }
    .width('100%')
    .height(80)
    .backgroundColor(Color.Pink)
    .borderRadius(8)
    .margin({
      top: 20,
      bottom: 10
    })
    .justifyContent(FlexAlign.Center)
  }
}
  • 如何控制滚动

    info

    Scroll的滚动一般由用户的手指触发

  • 我们也可以使用一个对象来控制滚动条 scroller

typescript 复制代码
@Entry
@Component
struct ScrollCase02 {
  scroller: Scroller = new Scroller()
  build() {
    Row() {
      Column() {
        // 有且只有一个组件
        Scroll(this.scroller) {
          Row({ space: 20 }) {
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
          }
        }.
        height(200)
        .scrollable(ScrollDirection.Horizontal)
        .width('100%')
        .backgroundColor(Color.Orange)
        Row() {
          Button("滚到左侧")
            .onClick(() => {
              this.scroller.scrollEdge(Edge.Start)
            })
          Button("滚到右侧")
            .onClick(() => {
              this.scroller.scrollEdge(Edge.End)

            })
        }
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}

@Component
struct Actor {
  build() {
    Row() {
      Text("热辣滚烫-贾玲")
        .fontColor(Color.White)
    }
    .backgroundColor(Color.Red)
    .justifyContent(FlexAlign.Center)
    .width(100)
    .height(180)
  }
}
  • 横向滚动

    info

  • 只需要将Scroll的滚动方向调节成横向即可

typescript 复制代码
@Entry
@Component
struct ScrollCase02 {
  @State message: string = 'Hello World';
  scroller: Scroller = new Scroller()
  build() {
    Row() {
      Column() {
        // 有且只有一个组件
        Scroll(this.scroller) {
          Row({ space: 20 }) {
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
            Actor()
          }
        }.
        height(200)
        .scrollable(ScrollDirection.Horizontal)
        .width('100%')
        .backgroundColor(Color.Orange)
        Row() {
          Button("滚到左侧")
            .onClick(() => {
              this.scroller.scrollEdge(Edge.Start)
            })
          Button("滚到右侧")
            .onClick(() => {
              this.scroller.scrollEdge(Edge.End)

            })
        }
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}

@Component
struct Actor {
  build() {
    Row() {
      Text("热辣滚烫-贾玲")
        .fontColor(Color.White)
    }
    .backgroundColor(Color.Red)
    .justifyContent(FlexAlign.Center)
    .width(100)
    .height(180)
  }
}

在arkUI中,我们的内容如果超过了屏幕显示,则不会显示滚动条,需要使用Scroll来包裹

需要注意的是: 该组件滚动的前提是1.设置或使用了默认的滚动方向,2.子组件大与容器Scroll大小,否则不能滚动

组件-事件监听

监听原生组件的事件和设置属性的方式是一样的都是链式调用,值得注意的是,我们注册事件必须使用箭头函数的写法,Next版本禁止使用匿名函数的形式来给组件注册事件

  • 匿名函数 function () {} (ES5函数声明方式的一种,在鸿蒙中属于弃用语法
  • 组件外部函数 function () {} (组件内可以使用组件外的声明的函数,但需要注意this指向
  • 尝试给一个TextInput注册一个值改变事件提交事件,给登录按钮注册点击事件


danger

  • promAction.showToast()轻量级提示,需要引入一个包才可以使用的,自动消失
  • promAction.showDialog()弹层级提示,需要引入一个包才可以使用的,点击消失
  • AlertDialog.show() 弹层级提示,不需要引入包使用的,点击消失
typescript 复制代码
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct Event {

  build() {
    Row() {
      Column({ space: 15 }) {
        Row() {
          TextInput({ placeholder: '请输入用户名' })
            .backgroundColor('#f4f5f6')
            .width('100%')
            .onChange((value) => {
              promptAction.showToast({ message: value })
            })
            .onSubmit(() => {
              AlertDialog.show({
                message:'登录失败'
              })
            })
        }.padding({
          left: 20,
          right: 20
        })

        Row() {
          Button("登录")
            .width('100%')
            .onClick(() => {
              promptAction.showDialog({
                message: '登录成功'
              })
            })

        }.padding({
          left: 20,
          right: 20
        })

      }
      .width('100%')
    }
    .height('100%')
  }
}

info

请注意:在注册事件中的逻辑必须使用箭头函数 () => {}

  1. 因为function中this指向为undefind
  2. 箭头函数中的this指向当前struct实例,可以方便的调用方法和获取属性

info

当我们事件处理逻辑比较复杂,写在UI描述中无法抽提的时候,我们可以在struct结构体中定义

typescript 复制代码
struct Event {
  login () {
    AlertDialog.show({
      message: '登录成功'
    })
  }

  ...
  buile(){
    Button("登录")
            .width('100%')
            .onClick(() => {
              this.login()
            })
  }
   
}
  • 为什么说一定要用箭头函数
typescript 复制代码
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct FunctionCase {
  @State message: string = 'Hello World';
  showThis(){
    promptAction.showToast({
      message:this.message
    })
  }
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          //报错
          //.onClick(this.showThis)
          //推荐
          .onClick(()=>{
             this.showThis()
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

info

关于箭头函数和this的说明:

1.推荐使用箭头函数

2.call和apply是错误级别不支持,bind是警告,能用也别用!

3.this是当前上下文,一般指函数调用者

  • 尝试获取组件的大小-组件区域变化事件 onAreaChange
    info
    如何获取某个组件的大小呢?比如获取下面百度图片的大小
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct EventCase {
  // 登录方法
  login() {
    promptAction.showToast({ message: '登录成功' })
  }
  loginToButton(){
    promptAction.showToast({ message: '登录失败' })
  }
  build() {
    Row() {
      Column({ space: 20 }) {
        Image('https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png')
          .width(160)
          .onAreaChange((oldValue:Area,newValue:Area)=>{
            AlertDialog.show({
              message:`oldValue:${JSON.stringify(oldValue)}
                    newValue:${JSON.stringify(newValue)}`
            })
          })
        TextInput({ placeholder: '请输入用户名', text:'' })
          .height(40)
          .onChange((value) => {
            // 监听系统组件的事件的话  回调函数的参数都是有具体类型  一般的话不用给类型
            promptAction.showToast({ message: value })
          })
        TextInput({ placeholder: '请输入密码', text: '' })
          .height(40)
          .onChange((value) => {
            // 监听系统组件的事件的话  回调函数的参数都是有具体类型  一般的话不用给类型
            promptAction.showToast({ message: value })
          })
          .type(InputType.Password)
          .onSubmit(() => {
            this.login()
          })
        Button("登录")
          .width('100%')
          .onClick(this.loginToButton)
      }
      .padding({
        left: 20,
        right: 20
      })
      .width('100%')
    }
    .height('100%')
  }
}

基础-组件状态

info

当我们需要在组件中记录一些状态时,变量应该显示的在struct中声明,并注明类型

比如-登录账户和密码

typescript 复制代码
  username: string = "admin"
  password: string = "123456"
  • 实现一个简单的登录页面
  • @State修饰符的作用

如果没有@State修饰符,改变状态后,页面不会进行状态更新

info

1.State修饰的类型:Object、class、string、number、boolean、enum类型,以及这些类型的数组。

2.类型必须被指定,嵌套类型的场景请参考观察变化

3.不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。

加上该修饰符后,你惊奇的发现按钮随着数据的变化在变化,因为我们在值改变的时候赋值,造成了build的重新执行,来保证我们状态的变化。

可以理解成没有@State修饰符,数据只会作用页面一次!!!

typescript 复制代码
interface myObjType {
  age:number
}
interface moreStepType {
  info:myObjType
}
@Entry
@Component
struct ThisCase {
  noStateMessage: string = '6666666666666666666'
  @State
  message: string = '999999999999999999999'
  @State
  myObj:myObjType = { age: 18 };
  noStateObj:myObjType = { age: 19 };
  @State
  moreStepObj:moreStepType = {
    info:{
      age:20
    }
  }
  innerFunction: () => void = () => {
    this.myObj = { age: 19 }
  }

  build() {
    Row() {
      Column() {
        Text(JSON.stringify(this))
          .fontWeight(FontWeight.Bold)
            .onClick(() => {
              this.innerFunction()
            })
      }
      .width('100%')
      .padding(20)
    }
    .height('100%')
  }
}

info
改变状态:引用数据类型只能检测到自身和第一层变化

  • 添加一个登录验证的需求 :账号admin密码123456时登录提示登录成功 ,否则提示用户名或者密码错误
typescript 复制代码
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct EventCase {
  @State message: string = 'Hello World';
  @State
  username: string = ""
  @State
  password: string = ""
  // 登录方法
  login() {
    if(this.username.trim()  === 'admin' && this.password === '123456') {
      // promptAction.showToast({ message: '登录成功' })
      AlertDialog.show({ message: '登录成功' })
      return
    }
    promptAction.showToast({ message: '用户名或者密码错误' })
  }

  build() {
    Row() {
      Column({ space: 20 }) {
        TextInput({ placeholder: '请输入用户名', text: this.username })
          .height(40)
          .onChange((value) => {
            // 监听系统组件的事件的话  回调函数的参数都是有具体类型  一般的话不用给类型
            console.log(value)
          })
        TextInput({ placeholder: '请输入密码', text: this.password })
          .height(40)
          .onChange((value) => {
            // 监听系统组件的事件的话  回调函数的参数都是有具体类型  一般的话不用给类型
            console.log(value)
          })
          .type(InputType.Password)
          .onSubmit(() => {
            this.login()
          })
        Button("登录")
          .width('100%')
          .onClick(() => {
            this.login()
          })


      }
      .padding({
        left: 20,
        right: 20
      })
      .width('100%')
    }
    .height('100%')
  }
}
  • 再加一个小需求 ,当用户名和密码为空时,不让用户点按钮
    danger
    可用enable设置按钮是否启用
typescript 复制代码
getBtnEnable () {
    return  !!this.username && !!this.password
  }

 Row() {
           Button("登录")
          .width('100%')
          .onClick(() => {
            this.login()
          })
          // 隐士转化 ""
          .enabled(this.getBtnEnable())

接下来,我们来完成一个嵌套数据修改的案例,来观察State修饰符的特性

  • 声明一个关于人的接口
typescript 复制代码
interface IAddress {
  province: string
  city: string
  area: string
}
interface IPerson {
  name: string
  age: number
  sex: "男" | "女"
  address: IAddress
}

可能比较疑惑,为什么这里还需要声明两个接口,因为Next版本不支持对象字面量类型声明,所以这里需要,用interface代替

  • 类型怎么赋值呢?
  • 使用npm全局安装插件
bash 复制代码
$ npm i -g interface2class   # 特别注意 $符号不用输入,$符号表示它是一个命令行命令

danger

npm config get registry

  • 设置淘宝镜像
bash 复制代码
$ npm config get registry
$ npm config set registry https://registry.npmmirror.com/
  • 脚本禁用-只针对windows
    info
    解决方法
  1. 以管理员身份运行PowerShell
  2. 执行:get-ExecutionPolicy,如果显示Restricted,表示状态是禁止的
  3. 执行: set-ExecutionPolicy RemoteSigned
  4. 选择Y

info

脚本策略-操作系统允许npm包的命令可执行

  • 测试是否安装成功
bash 复制代码
$ i2c -V
  • 执行带有interface的文件
  • 自动生成实现类
  • 声明一个State类型
typescript 复制代码
 @State
  p: IPersonModel = new IPersonModel({
    name: '小张',
    age: 20,
    sex: "女",
    address: {
      province: '广东',
      city: '广州',
      area: '天河'
    }
  })
  • 使用UI组件实现双向绑定
typescript 复制代码
@Entry
@Component
struct StateCase {
  @State
  p: IPersonModel = new IPersonModel({
    name: '小张',
    age: 20,
    sex: "女",
    address: {
      province: '广东',
      city: '广州',
      area: '天河'
    }
  })
  build() {
    Column({ space: 10 }) {
      Text(JSON.stringify(this.p))
      Row({ space: 15 }) {
        Text("姓名:")
        TextInput({ text: this.p.name }).layoutWeight(1)
          .onChange(value => {
            this.p.name = value
          })
      }.padding(10)
      Row({ space: 15 }) {
        Text("年龄:")
        TextInput({ text: this.p.age.toString() }).layoutWeight(1)
          .onChange(value => {
             this.p.age = parseInt(value)
          })
      }.padding(10)
      Row({ space: 15 }) {
        Text("性别:")
        Select([{ value: '男' }, { value: '女' }])
          .layoutWeight(1)
          .value(this.p.sex)
          .onSelect((index: number, value: string) => {
            this.p.sex = value as "男" | "女"
          })
      }.padding(10)
      Row({ space: 15 }) {
        Text("地址:")
        TextInput({ text: this.p.address.province }).layoutWeight(1)
          .onChange((value) => {
            this.p.address.province = value
          })
        TextInput({ text: this.p.address.city }).layoutWeight(1)
          .onChange((value) => {
            this.p.address.city = value
          })
        TextInput({ text: this.p.address.area }).layoutWeight(1)
          .onChange((value) => {
            this.p.address.area = value
          })
      }.padding(10)

    }
  }
}
interface IAddress {
  province: string
  city: string
  area: string
}
interface IPerson {
  name: string
  age: number
  sex: "男" | "女"
  address: IAddress
}
export class IAddressModel implements IAddress {
  province: string = ''
  city: string = ''
  area: string = ''

  constructor(model: IAddress) {
    this.province = model.province
    this.city = model.city
    this.area = model.area
  }
}
export class IPersonModel implements IPerson {
  name: string = ""
  age: number = 0
  sex: "男" | "女" = "男"
  address: IAddress = new IAddressModel({} as IAddress)

  constructor(model: IPerson) {
    this.name = model.name
    this.age = model.age
    this.sex = model.sex
    this.address = model.address
  }
}
  • 更新数据-第一层数据
typescript 复制代码
  // 放置按钮
      Row({ space: 10 }) {
        Button("修改姓名")
          .onClick(() => {
             this.p.username = "老张"
          })
        Button("修改年龄")
          .onClick(() => {
            this.p.age++
          })
        Button("修改性别")
          .onClick(() => {
            this.p.sex = this.p.sex === "男" ? "女" : "男"
          })
      }
      .padding({
        left: 10,
        right: 10
      })
      .width("100%")
  • 更新第二层数据

danger

所有的鸿蒙的更新机制-关于对象层的, 所有的监听机制都只能监听到Object.keys(对象)中的可见属性,但凡超过一层,是监测不到变化的。

typescript 复制代码
  Row({ space: 10 }) {
        Button("修改地址")
          .onClick(() => {
            // this.p.address.province = "北京"
            // this.p.address.city = "北京"
            // this.p.address.area = "顺义"
            // this.p.age++
            // this.p.address = new IAddressModel({
            //   province: this.p.address.province,
            //   city: this.p.address.city,
            //   area:"海珠区"
            // })
            this.p.address.area = "海珠区" // 虽然不会造成更新 但是数据会变化
            this.p.address = new IAddressModel(this.p.address)
          })

      }
      .padding({
        left: 10,
        right: 10
      })
      .width("100%")

发现什么问题没有?

我们发现第一层数据 比如 name/age/sex都是可以双向同步的,但是第二层的数据无法进行响应式更新

  • 上述代码中,我们也没有使用 ,因为在目前的编辑器中使用 ,因为在目前的编辑器中使用 ,因为在目前的编辑器中使用绑定嵌套的数据编辑器会报错,虽然效果对,但是报错总归不太好,所以我们手动进行了数据同步

在鸿蒙开发中,我们应该明确,所有的响应式更新都只能是系统可观测的响应式更新,那什么是可观测的呢?

就是只有对象或者数组第一层的数据发生了变化,才可以被观测到,所以基于这种特性,我们需要来处理下

下方列出系统可观测的数据和行为变化




组件-双向绑定

  • 双向绑定

    info

    数据-视图

  • 数据驱动视图

  • 视图中的内容发生变化-数据也会同步修改

  • MVVM-M-V-VM

  • Model数据模型

  • V-View视图

  • VM- ViewModel 桥梁

在鸿蒙Next版本中,推出了一系列双向绑定的组件

  • 双向绑定- 数据和视图双向同步-数据变化-视图更新,视图更新,-数据更新

  • 鸿蒙Next版本所支持的所有双向绑定组件


  • 双向绑定语法

语法:内置组件双向同步 * 当前$$支持基础类型变量,以及@State、@Link和@Prop装饰的变量。

如果绑定TextInput组件,如 TextInput({ text: $$this.xxx })

danger

值得注意的是:上述组件中有的双向组件是属性,有的双向绑定是参数

  • 参数是在组件({ text: $$this.xx })

  • 属性是在组件().text($$this.xxx)

  • 不支持嵌套数据的双向绑定如 组件({ text: $$this.xx.xx })

  • 测试几个正常的双向绑定

typescript 复制代码
@Entry
@Component
struct MvvmCase {
  @State
  isChecked: boolean = false
  @State
  myDate: Date = new Date('2024-05-01')
  @State
  myRatio: boolean = false
  @State
  mySearch: string = ''
  @State
  myToggle: boolean = false
  @State
  mySelect: string = '请选择'
  build() {
    Column({ space: 12 }) {
      Column() {
        Text('Checkbox双向绑定')
        Checkbox().select($$this.isChecked)
        Text('' + this.isChecked)
      }

      MyDivider()
      Column() {
        Text('DatePicker双向绑定')
        DatePicker({
          selected: $$this.myDate
        })
        Text('' + this.myDate)
      }

      MyDivider()
      Column() {
        Text('Radio双向绑定')
        Radio({ value: 'radio', group: 'radioSelect' }).checked($$this.myRatio)
        Text('' + this.myRatio)
      }

      MyDivider()
      Column() {
        Text('Search双向绑定')
        Search({value:$$this.mySearch})
        Text('' + this.mySearch)
      }
      MyDivider()
      Column() {
        Text('Toggle双向绑定')
        Toggle({ type: ToggleType.Switch,isOn:$$this.myToggle})
        Text('' + this.myToggle)
      }
      MyDivider()
      Column() {
        Text('Select双向绑定')
        Select([{ value: 'aaa' },
          { value: 'bbb'},
          { value: 'ccc'},
          { value: 'ddd'}])
          .value($$this.mySelect)
        Text('' + this.mySelect)
      }
    }
    .width("100%")
    .height("100%")
  }
}

@Component
struct MyDivider {
  build() {
    Divider().height(5).backgroundColor(Color.Pink)
  }
}

样式

样式-语法(链式&枚举)

ArkTS以声明方式组合和扩展组件来描述应用程序的UI;

同时还提供了基本的属性、事件和子组件配置方法,帮助开发者实现应用交互逻辑。

1)样式属性:通用属性 和 组件属性

  • 属性方法以 . 链式调用的方式配置系统组件的样式和其他属性
typescript 复制代码
@Entry
@Component
struct Index {
  build() {
    Text('演示')
      .backgroundColor('red')
      .fontSize(50)
      .width('100%')
      .height(100)
  }
}

2)枚举值

  • 对于系统组件,ArkUI还为其属性预定义了一些枚举类型。文档链接
typescript 复制代码
@Entry
@Component
struct Index {
  build() {
    Text('演示')
      .fontSize(50)
      .width('100%')
      .height(100)
      .backgroundColor(Color.Blue)
      .textAlign(TextAlign.Center)
      .fontColor(Color.White)
  }
}

info

  • 样式相关属性通过链式函数的方式进行设置
  • 如果类型是枚举的,通过枚举传入对应的值

注意: 有的属性强烈建议 使用枚举(大部分枚举值都是数字,但是数字无法体现代码含义)

有的组件如fontColor可以使用系统自带颜色枚举,也可以使用色值

样式-单位px/vp/fp/lpx

官方定义

使用虚拟像素,使元素在不同密度的设备上具有一致的视觉体量。

1) vp 是什么?virtual pixel

  • 屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素,当数值不带单位时,默认单位 vp;在实际宽度为1440物理像素的屏幕上,1vp 约等于 3px(物理像素)
  • 系统还提供了对应的像素转化方法

info

在样式中,我们如果写px,那么px直接表示的是物理像素,也就是分辨率,那么我们的手机分辨率密度各有不同,无法针对这种密度写一个固定值,所以vp会自动根据手机密度去进行适配,所以vp它提供了一种灵活的方式来适应不同屏幕密度的显示效果。

设计图按照1080设计- 换算成360写vp就可以了

  • 上图的意思是,使用这个单位在不同屏幕物理分辨率的实际尺寸一致(A设备1英寸,B设备1英寸)。

2)在不同屏幕物理分辨率下,要想实现等比例适配, 可以吗?

如下图:

info

设置lpx基准值 - resources/base/profile/main-pages.json

添加window属性,设置desigWidth,不设置也可以使用lpx,默认720

typescript 复制代码
@Entry
@Component
struct PXCase {
  build() {
    Row() {
      Column() {
        Text('375lpx')
        .width('375lpx')
        .height('72lpx')
          .textAlign(TextAlign.Center)
        .backgroundColor(Color.Red)
        Divider().strokeWidth(2)
        Row(){
          Text('72lpx')
        }
        .width('72lpx')
        .height('25lpx')
        .backgroundColor(Color.Brown)
      }
      .width('100%')
    }
    .height('100%')
  }
}

danger

伸缩布局的方案

  • 设定基准值,使用lpx,类似于前端的rem
  • 监听元素的变化-可以拿到宽高-重新计算
  • layoutWeight(number)- 剩余资源再分配

伸缩 layoutWeight(flex: number) 占剩余空间多少份,可以理解成CSS的 flex: 1

如图-手机端

-pad

我们可以使用layoutWeight属性,让右侧内容去占满剩余宽度

typescript 复制代码
build() {
    Row() {
      Text("左侧内容")
      Text("右侧内容")
        .textAlign(TextAlign.End)
        .width('80%')
        .height(60)
        .backgroundColor('red')
        .layoutWeight(1)
    }.width('100%')
    .height('100%')

  }
typescript 复制代码
@Entry
@Component
struct LayoutCase {
  @State message: string = 'Hello World';

  build() {
    Column() {
      Row() {

      }
      .width('100%')
      .height(50)
      .backgroundColor(Color.Blue)

      Column() {

      }
      .width('100%')
      .backgroundColor(Color.Green)
      .layoutWeight(1)  // Grid中的columnsTemplate 1fr 1fr
      Column() {

      }
      .width('100%')
      .backgroundColor(Color.Orange)
      .layoutWeight(1)

      Row() {

      }
      .width('100%')
      .height(50)
      .backgroundColor(Color.Red)
    }
    .height("100%")
    .width("100%")
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

info

瓜分当前组件的剩余空间

Row() {

Text().width(10)

Text().width(10)

Text().layoutWeight(5) //表示这个元素占据剩余所有的空间 (100 - 10 - 10)* (5/(5 + 1))

Text().layoutWeight(1) //表示这个元素占据剩余所有的空间 100 - 10 - 10

}.width(100)

  • 内容等比例缩放-可以使用aspectRatio属性设置宽高比

设置元素宽高比 aspectRatio(ratio: number)

如我们如果希望一个元素始终占整个屏幕宽度的50%,且为一个正方形

typescript 复制代码
 Column()
        .width('50%')
        .height('50%')
        .backgroundColor('blue')
        .aspectRatio(1)
typescript 复制代码
@Entry
@Component
struct Index {
  build() {
    Text('left')
      .width('50%')
        // 宽高比例
      .aspectRatio(1)
      .backgroundColor('red')
  }
}

info

  • vp 是鸿蒙默认单位,和屏幕像素有关,最终表现视觉大小在任何设备一致(屏幕越大看的越多)
  • 鸿蒙一般以伸缩 layoutWeight、网格、栅格进行布局适配,如要等比例缩放可以设置高宽比 aspectRatio(屏幕越大内容越大)

Image和资源Resource

info

项目开发离不开图片-图片在页面中必须使用Image/ImageSpan/动画图片组件

Image为图片组件,常用于在应用中显示图片。Image支持加载string、PixelMapResource类型的数据源,支持png、jpg、bmp、svg和gif类型的图片格式。

  • 1.使用本地图片-拖一张图片放置到ets目录下-比如assets文件下
    info
    命名不要出现中文、空格、括号等特殊符号!
typescript 复制代码
Image('/assets/a.png')
          .width(100)
          .height(100)
  • 2.使用Resource下的图片-media
typescript 复制代码
//不需要图片后缀
Image($r('app.media.a'))
          .width(100)
          .height(100)
  • 3.使用Resource下的图片-rawfile
typescript 复制代码
 Image($rawfile('a.png'))
          .width(100)
          .height(100)
  • 4.使用网络图片
typescript 复制代码
        Image("https://foruda.gitee.com/avatar/1705232317138324256/1759638_itcast_panpu_1705232317.png")
          .width(100)
          .height(100)

info

尤其注意: 使用网络图片时,在preview中时,可以预览,但是在模拟器和真实项目中,必须申请网络权限

"requestPermissions": [{
  "name":"ohos.permission.INTERNET"
}],
  • 5.使用字体图标

阿里巴巴矢量图标库 iconfont

华为官方图标下载 链接

info

搜索下载需要的图标,下载类型为SVG,使用fillColor()可以进行图标颜色修改

部分图标不能修改,需要手动添加fill属性才能生效

typescript 复制代码
        Image($r('app.media.ic_like'))
          .width(100)
          .height(100)
          .fillColor(Color.Red)
  • 6.使用系统内置图标
typescript 复制代码
      //有的图标必须给大小
      Image($r('sys.media.ohos_ic_public_sound'))
          .width(100)
          .height(100)
          .fillColor(Color.Red)
typescript 复制代码
@Entry
@Component
struct ImageCase {
  build() {
    Row() {
      Column({space:10}) {
        // 本地自建目录
        Image('/assets/a.png')
          .width(100)
          .height(100)
        // 推荐目录
        Image($r('app.media.a'))
          .width(100)
          .height(100)
        // 源文件目录
        Image($rawfile('a.png'))
          .width(100)
          .height(100)
        // 网络图片
        Image("https://foruda.gitee.com/avatar/1705232317138324256/1759638_itcast_panpu_1705232317.png")
          .width(100)
          .height(100)
        // 字体图标用法
        Image($r('app.media.ic_like'))
          .width(100)
          .height(100)
          .fillColor(Color.Red)
        // 系统内置图标用法
        Image($r('sys.media.ohos_ic_public_sound'))
          .width(100)
          .height(100)
          .fillColor(Color.Red)
      }
      .width('100%')
    }
    .height('100%')
  }
}

info

我们已经知道resources/base下media和profile的作用了,那么element是干嘛的呢?

  • 自定义资源颜色-在color.json中定义一个颜色
typescript 复制代码
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    },
    {
      "name": "my_first_color",
      "value": "#FF00FF0F"
    }
  ]
}

在页面使用这个颜色

typescript 复制代码
 Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor($r('app.color.my_first_color'))
  • 自定义资源文字-在string.json中定义一个文字
typescript 复制代码
{
  "string": [
    {
      "name": "module_desc",
      "value": "module description"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "label"
    },
    {
      "name": "my_first_string",
      "value": "老潘"
    }
  ]
}

在页面上使用这个文字

typescript 复制代码
 Text($r('app.string.my_first_string'))
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor($r('app.color.my_first_color'))
  • 国际化处理
    danger
    需要注意的是,手动添加虽然可以使用,但是会引起警告
    推荐编辑添加,除了添加base/string.json,对en_US和zh_CN也进行添加


添加后运行到模拟器,改变系统语言会发现,呈现文字也会跟随改变

info

写一个知乎的评论练习练习排版布局和图片样式等

typescript 复制代码
@Entry
@Component
struct ZHCase {
  build() {
    Column() {
      // 1.标题
      Row() {
        Row() {
          Image($r('sys.media.ohos_ic_public_arrow_left'))
            .width(24)
            .height(24)
        }
        .width(30)
        .height(30)
        .backgroundColor('#f4f4f4')
        .borderRadius(15)
        .justifyContent(FlexAlign.Center)

        Text('评论回复')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .margin({
            right: 30
          })
      }
      .width('100%')
      .padding(16)
      .border({
        width: {
          bottom: 1
        },
        color: '#f4f5f6'
      })

      // 2.评论
      Row({space:16}){
        Image('https://foruda.gitee.com/avatar/1705232317138324256/1759638_itcast_panpu_1705232317.png')
          .width(60)
          .borderRadius(30)
        Column({space:16}){
          Text('潘神')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
          Text('写布局的口诀是从上到下,从左到右,从大到小,从外往里!🔥')
            .width('100%')
          Row(){
            Text('10-21·IP:北京')
              .fontSize(12)
              .fontColor('#ccc')
            Row({space:4}){
              Image($r('app.media.ic_like'))
                .width(16)
                .fillColor('#ccc')
              Text('100')
                .fontSize(12)
                .fontColor('#ccc')
            }
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

      }
      .padding(16)
      .alignItems(VerticalAlign.Top)
    }
    .width('100%')
    .justifyContent(FlexAlign.Start)
  }
}

样式-@Styles 复用

注意: Styles和Extend均只支持在当前文件下的全局或者组件内部定义,如果你想要在其他文件导出一个公共样式,导出公共使用,ArtTS是不支持的,这种方式还是需要考虑组件复用。
在开发过程中会出现大量代码在进行重复样式设置,@Styles 可以帮我们进行样式复用

info

通用属性 通用事件

在Styles修饰的函数中能够点出来就是通用属性和事件-Text的字体颜色-字体大小不属于通用属性

Styles修饰的函数不允许传参数

warning
注意: 全局Styles扩展符只能和使用它的组件位于同一个文件,不允许导入导出,导入导出也使用不了

  • 支持 全局 定义和 组件内 定义,同时存在组件内覆盖全局生效。
typescript 复制代码
import { promptAction } from '@kit.ArkUI'

@Styles
function payStyle () {
  .width('100%')
  .height(50)
  .borderRadius(4)
  .backgroundColor("#00c168")
  .onClick(() => {
    promptAction.showToast({ message: '微信支付成功' })
  })
}

@Entry
@Component
struct StyleCase {
  @Styles
  payStyle() {
    .width('100%')
    .height(50)
    .borderRadius(4)
    .backgroundColor("#ff1256e0")
    .onClick(() => {
      promptAction.showToast({ message: '支付宝支付成功' })
    })
 }


  build() {
    Column({ space: 20 }) {
      Row() {
        Button("微信支付", { type: ButtonType.Normal })
          .payStyle()
          .fontColor(Color.White)
      }
      .padding(10)
      Row() {
        Button("微信支付", { type: ButtonType.Normal })
          .payStyle()
          .fontColor(Color.White)
      }
      .padding(10)
      Row() {
        Button("微信支付", { type: ButtonType.Normal })
          .payStyle()
          .fontColor(Color.White)
      }
      .padding(10)

    }
  }
}

样式-@Extend 复用

info

假设我们就想针对 Text进行字体和样式的复用,此时可以使用Extend来修饰一个全局的方法

  • 使用 @Extend 装饰器修饰的函数只能是 全局
  • 函数可以进行 传参,如果参数是状态变量,状态更新后会刷新UI
  • 且参数可以是一个函数,实现复用事件且可处理不同逻辑

warning
注意: Extend扩展符只能和使用它的组件位于同一个文件,不允许导入导出,导入导出也使用不了

typescript 复制代码
import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct ExtendCase {

  build() {
     Column({ space: 20 }) {
       Button("微信支付")
         .payButton("alipay")
       Button("微信支付")
         .payButton("wechat")
       Button("微信支付")
         .payButton("alipay")
       Button("微信支付")
         .payButton("wechat")
           Button("微信支付")
         .payButton("alipay")
           Button("微信支付")
         .payButton("wechat")
           Button("微信支付")
         .payButton("alipay")

     }
     .padding(20)
    .width('100%')
  }
}

// 不允许导出
@Extend(Button)
function  payButton (type: "alipay" | "wechat") {
  .type(ButtonType.Normal)
  .fontColor(Color.White)
  .width('100%')
  .height(50)
  .borderRadius(4)
  .backgroundColor(type === "wechat" ? "#00c168" : "#ff1256e0")
  .onClick(() => {
    if(type === "alipay") {
      promptAction.showToast({ message: '支付宝支付成功' })
    }else {
      promptAction.showToast({ message: '微信支付成功' })
    }

  })
}

多态样式stateStyles

@Styles和@Extend仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。这就是我们本章要介绍的内容stateStyles(又称为:多态样式)。

ArkUI 提供以下五种状态:

  • focused:获焦态。
  • normal:正常态。
  • pressed:按压态。
  • disabled:不可用态。
  • selected: 选中态

warning

假设我们想做一个微信中点击的选中状态, 如图

该图在点击时会有变色,抬起时消失,此时就可以利用多态样式进行设置

  • 实现一个基本的Row样式
typescript 复制代码
@Entry
@Component
struct StateStylesCase {
  build() {
    Column({ space: 20 }) {
       Row() {
         Text("你今天想我了吗")
       }
       .padding(20)
       .height(80)
       .border({
         color: '#f3f4f5',
         width: 3
       })
       .borderRadius(4)
       // 多态样式
       .stateStyles({
         // 正常态
         normal: {
           .backgroundColor(Color.White)
         },
         pressed: {
           .backgroundColor("#eee")
         }
       })
      .width('100%')
    }
    .padding(20)
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}

info

按钮时,出现变色,需要同时设置pressed和normal两个属性, 如果只设置pressed,松手之后样式无法恢复

info
注意 - 多态样式只能设置通用属性

  • 禁用状态样式

    warning

    鸿蒙所有组件都会有一个enable属性,enable为true时表示该组件可用,为false时,表示该组件禁用,禁用态就表示组件禁用时的样式

  • 设置Row禁用时的状态背景为灰背景

typescript 复制代码
@Entry
@Component
struct StateStylesCase {
  @State
  btnEnable: boolean = true
  build() {
    Column({ space: 20 }) {
       Row() {
         Text("你今天想我了吗")
       }
       .padding(20)
       .height(80)
       .border({
         color: '#f3f4f5',
         width: 3
       })
       .borderRadius(4)
       // 多态样式
       .stateStyles({
         // 正常态
         normal: {
           .backgroundColor(Color.White)
         },
         pressed: {
           .backgroundColor("#eee")
         },
         disabled: {
           .backgroundColor("#999")
         }
       })
       .enabled(this.btnEnable)
      .width('100%')
      Button("禁用/解禁")
        .onClick(() => {
          this.btnEnable = !this.btnEnable
        })
    }
    .padding(20)
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}
  • 获焦状态
    info
    假设我们有个账号和密码输入框,需要在聚焦态时设置选中样式,同样可以使用多态样式来设置
    但是在预览器和真机中,有个非常诡异的设计,只能通过tab键切换才可以使得输入框进入聚焦态
    如图
typescript 复制代码
 TextInput({
          placeholder: '请输入账号'
        })
          .stateStyles({
            focused: {
              .border({
                color: Color.Red,
                width: 1
              })
            },
            normal: {
              .border({
                width: 0
              })
            }
          })
        TextInput({
          placeholder: '请输入密码',
        })
          .stateStyles({
            focused: {
              .border({
                color: Color.Red,
                width: 1
              })
            },
            normal: {
              .border({
                width: 0
              })
            }
          })
          .type(InputType.Password)
          .showPasswordIcon(true)
  • 全部代码
typescript 复制代码
@Entry
@Component
struct StateStylesCase {
 @State
 rowEnable: boolean = true
  build() {
    Row() {
      Column({ space: 20 }) {
        TextInput({
          placeholder: '请输入账号'
        })
          .stateStyles({
            focused: {
              .border({
                color: Color.Red,
                width: 1
              })
            },
            normal: {
              .border({
                width: 0
              })
            }
          })
        TextInput({
          placeholder: '请输入密码',
        })
          .stateStyles({
            focused: {
              .border({
                color: Color.Red,
                width: 1
              })
            },
            normal: {
              .border({
                width: 0
              })
            }
          })
          .type(InputType.Password)
          .showPasswordIcon(true)


        Row() {
          Text("你干什么吃的")
        }
        .height(60)
        .width('100%')
        .border({
          color: '#ccc',
          width: 1
        }).stateStyles({
          pressed: {
            .backgroundColor(Color.Gray)
          },
          normal: {
            .backgroundColor(Color.White)
          },
          disabled: {
            .backgroundColor("#f3f4f5")
          }
        })
        .enabled(this.rowEnable)
        Button("禁用")
          .onClick(() => {
            this.rowEnable = !this.rowEnable
          })
      }


    }
    .height('100%')
  }
}

info

  • 使用比较多的应该是 normal pressed 结合下的按压效果
  • enabled(true|false) 开启|禁用

界面渲染

渲染-条件渲染

info

在ArkTS中 我们要根据某个状态来控制元素或者组件的显示隐藏 可以采用条件渲染

  • if/else(创建销毁元素)

  • 元素高宽-透明度-位置控制 (属性控制)

  • visibility属性控制

  • 使用if/else

通过一个switch开关来控制图片的显示隐藏

typescript 复制代码
@Entry
@Component
struct ConditionCase {
  @State isShow: boolean = false;

  build() {
    Row() {
      Column() {
        Toggle({
          type: ToggleType.Switch,
          isOn: $$this.isShow
        })
        // 1.控制是否渲染
        // if(this.isShow){
        //   Image($r('app.media.b')).width(100)
        // }else{
        // Image($r('app.media.b')).width(100)
        // }
        // 2.控制是否展示
        Image($r('app.media.b')).width(100)// 展示
          // .visibility(Visibility.Visible)
          // 隐藏(不显示也会占位置,不影响其他元素排列的话优先使用这个,性能高)
          // .visibility(this.isShow?Visibility.Visible:Visibility.Hidden)
          // 隐藏(不显示就不占位置,和if else一样)
          .visibility(this.isShow ? Visibility.Visible : Visibility.None)
      }
      .width('100%')
    }
    .height('100%')
  }
}
  • 多种条件控制

info

分析:

1.页面排版布局样式实现

2.下拉框的双向绑定

3.条件渲染

typescript 复制代码
@Entry
@Component
struct ConditionCase02 {
  @State myVip: number = 0;
  @State optionValue:string = '暂不开通'

  build() {
    Row() {
      Column({space:20}) {
        Row(){
          Text('开通会员:')
          Select([{value:'暂不开通'},{value:'VIP'},{value:'SVIP'}])
            .width('50%')
            .selected($$this.myVip)
            .value($$this.optionValue)
        }
        Row({ space: 20 }) {
          Image($r('app.media.b')).width(30).borderRadius(30)
          Text('西北吴彦祖')
          if (this.myVip === 0) {
            Text('VIP')
              .VIPStyle(this.myVip)
              .backgroundColor('#ccc')
          } else if (this.myVip === 1) {
            Text('VIP')
              .VIPStyle(this.myVip)
              .backgroundColor('#ffffb803')
          } else if (this.myVip === 2) {
            Text('SVIP')
              .VIPStyle(this.myVip)
              .backgroundColor('#ffb00909')
          }

        }.width('100%')
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .padding(20)
    }
    .height('100%')
  }
}
@Extend(Text)
function VIPStyle(type: number) {
  .padding({
    left: 12,
    right: 12,
    bottom: 4,
    top: 4
  })
  .fontColor('#fff')
  .borderRadius(20)
  .fontSize(12)
}
  • 案例-实现加载数据的loading效果

warning

  • 封装loading组件
typescript 复制代码
@Preview // 表示该组件可预览 Preview只能看效果 没有交互
@Component
export struct HmLoading {
  @State // 响应式驱动视图
  value: number = 0
  timer: number = -1  // 显示声明
  aboutToAppear(): void {
   this.timer = setInterval(() => {
      if(this.value === 100) {
        this.value = 0
      }
      this.value++
    }, 10)
  }
  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  build() {
     Progress({
       total: 100,
       value: this.value,
       type: ProgressType.Ring
     })
  }
}

// 默认导出
export default HmLoading  // 一个文件只能有一个默认导出
// 按需导出
// export { HmLoading, HmLoading1, HmLoading3 }
  • 在LoadingCase中使用
typescript 复制代码
import HmLoading from './Components/HmLoading';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct LoadingCase {
  @State showLoading: boolean = false;

  // 页面刚进入 应该去获取数据  等待的过程中 显示进度 数据获取完 进度消失
  // 钩子函数
  aboutToAppear(): void {
     // 请求数据
    // setTimeout/setInterval
    this.showLoading = true
    setTimeout(() => {
      this.showLoading = false
    },10000)
  }
  build() {
    Row() {
      Column() {
        if(this.showLoading) {
          HmLoading()
        }
      }
      .width('100%')
    }
    .height('100%')
  }
}

// 封装一个Loading组件

渲染-循环渲染

  • ForEach-最常用的
  • LazyForEach-懒加载渲染(复杂-后面讲)
    info
    循环渲染使用 ForEach方法来进行

ForEach 接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用。

typescript 复制代码
ForEach(
  // 数据源
  arr: Array,
  // 组件生成函数
  itemGenerator: (item: 单项, index?: number) => void,
  // 键值生成函数
  keyGenerator?: (item: 单项, index?: number): string => string
)
  • 定义数据类型
typescript 复制代码
interface PayRecord {
  OrderName:string
  OrderDate:Date
  OrderAmount:number
}
@State PayRecordList: PayRecord[] = [
  {
    OrderName: '给老婆买口红',
    OrderDate: new Date('2024/05/11'),
    OrderAmount: 399.00
  },
  {
    OrderName: '给老婆买花',
    OrderDate: new Date('2024/05/11'),
    OrderAmount: 99.00
  },
  {
    OrderName: '给自己买手机',
    OrderDate: new Date('2024/05/11'),
    OrderAmount: 9999.00
  }
]
  • 在页面中生成数据,用ForEach循环
typescript 复制代码
interface PayRecord {
  OrderName:string
  OrderDate:Date
  OrderAmount:number
}
@Entry
@Component
struct ForEachCase {
  @State showDialog: boolean = false
  @State PayRecordList: PayRecord[] = [
    {
      OrderName: '给老婆买口红',
      OrderDate: new Date('2024/05/11'),
      OrderAmount: 399.00
    },
    {
      OrderName: '给老婆买花',
      OrderDate: new Date('2024/05/11'),
      OrderAmount: 99.00
    },
    {
      OrderName: '给自己买手机',
      OrderDate: new Date('2024/05/11'),
      OrderAmount: 9999.00
    }
  ]

  build() {
    Stack() {
      Column() {
        // 标题
        Row() {
          Text('支付记录')
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .margin({
              left: 30
            })
        }
        .width('100%')
        .padding(16)
        .border({
          width: {
            bottom: 1
          },
          color: '#f4f5f6'
        })

        // 列表
        Column() {
          // 要循环的结构体
          // Column({ space: 20 }) {
          //   Text('给老婆买了一朵花')
          //     .fontWeight(FontWeight.Bold)
          //     .width('100%')
          //   Row() {
          //     Text('¥43.00')
          //     Text('2024/5/11')
          //   }
          //   .width('100%')
          //   .justifyContent(FlexAlign.SpaceBetween)
          // }
          // .width('100%')
          // .padding(20)
          // 使用循环组件,依次生成结构体
          ForEach(this.PayRecordList, (item: PayRecord) => {
            Column({ space: 20 }) {
              Text(item.OrderName)
                .fontWeight(FontWeight.Bold)
                .width('100%')
              Row() {
                Text('¥' + item.OrderAmount.toFixed(2))
                  .fontColor(Color.Red)
                Text(item.OrderDate.toLocaleDateString())
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)
            }
            .width('100%')
            .padding(20)
          })

        }
        .justifyContent(FlexAlign.Start)
        .width('100%')
        .layoutWeight(1)

      }
      .width('100%')
      .height('100%')
    }
  }
}
  • 新建一个list数据,进行循环
    warning
    使用下面的接口
typescript 复制代码
interface GoodItem {
  goods_name: string
  goods_price: number
  goods_img: string
  goods_count: number
  id: number
}
  • 拷贝图片到assets

图片.zip

  • 声明数据
typescript 复制代码
@State list: GoodItem[] = [
    {
      "id": 1,
      "goods_name": "班俏BANQIAO超火ins潮卫衣女士2020秋季新款韩版宽松慵懒风薄款外套带帽上衣",
      "goods_img": "assets/1.webp",
      "goods_price": 108,
      "goods_count": 1,
    },
    {
      "id": 2,
      "goods_name": "嘉叶希连帽卫衣女春秋薄款2020新款宽松bf韩版字母印花中长款外套ins潮",
      "goods_img": "assets/2.webp",
      "goods_price": 129,
      "goods_count": 1,
    },
    {
      "id": 3,
      "goods_name": "思蜜怡2020休闲运动套装女春秋季新款时尚大码宽松长袖卫衣两件套",
      "goods_img": "assets/3.webp",
      "goods_price": 198,
      "goods_count": 1,
    },
    {
      "id": 4,
      "goods_name": "思蜜怡卫衣女加绒加厚2020秋冬装新款韩版宽松上衣连帽中长款外套",
      "goods_img": "assets/4.webp",
      "goods_price": 99,
      "goods_count": 1,
    },
    {
      "id": 5,
      "goods_name": "幂凝早秋季卫衣女春秋装韩版宽松中长款假两件上衣薄款ins盐系外套潮",
      "goods_img": "assets/5.webp",
      "goods_price": 156,
      "goods_count": 1,
    },
    {
      "id": 6,
      "goods_name": "ME&CITY女装冬季新款针织抽绳休闲连帽卫衣女",
      "goods_img": "assets/6.webp",
      "goods_price": 142.8,
      "goods_count": 1,
    },
    {
      "id": 7,
      "goods_name": "幂凝假两件女士卫衣秋冬女装2020年新款韩版宽松春秋季薄款ins潮外套",
      "goods_img": "assets/7.webp",
      "goods_price": 219,
      "goods_count": 2,
    },
    {
      "id": 8,
      "goods_name": "依魅人2020休闲运动衣套装女秋季新款秋季韩版宽松卫衣 时尚两件套",
      "goods_img": "assets/8.webp",
      "goods_price": 178,
      "goods_count": 1,
    },
    {
      "id": 9,
      "goods_name": "芷臻(zhizhen)加厚卫衣2020春秋季女长袖韩版宽松短款加绒春秋装连帽开衫外套冬",
      "goods_img": "assets/9.webp",
      "goods_price": 128,
      "goods_count": 1,
    },
    {
      "id": 10,
      "goods_name": "Semir森马卫衣女冬装2019新款可爱甜美大撞色小清新连帽薄绒女士套头衫",
      "goods_img": "assets/10.webp",
      "goods_price": 153,
      "goods_count": 1,
    }
  ]

使用ForEach遍历

typescript 复制代码
import { GoodItem } from './models'

@Entry
@Component
struct ForEachGoodCase {
  @State list: GoodItem[] = [
    {
      "id": 1,
      "goods_name": "班俏BANQIAO超火ins潮卫衣女士2020秋季新款韩版宽松慵懒风薄款外套带帽上衣",
      "goods_img": "assets/1.webp",
      "goods_price": 108,
      "goods_count": 1,
    },
    {
      "id": 2,
      "goods_name": "嘉叶希连帽卫衣女春秋薄款2020新款宽松bf韩版字母印花中长款外套ins潮",
      "goods_img": "assets/2.webp",
      "goods_price": 129,
      "goods_count": 1,
    },
    {
      "id": 3,
      "goods_name": "思蜜怡2020休闲运动套装女春秋季新款时尚大码宽松长袖卫衣两件套",
      "goods_img": "assets/3.webp",
      "goods_price": 198,
      "goods_count": 1,
    },
    {
      "id": 4,
      "goods_name": "思蜜怡卫衣女加绒加厚2020秋冬装新款韩版宽松上衣连帽中长款外套",
      "goods_img": "assets/4.webp",
      "goods_price": 99,
      "goods_count": 1,
    },
    {
      "id": 5,
      "goods_name": "幂凝早秋季卫衣女春秋装韩版宽松中长款假两件上衣薄款ins盐系外套潮",
      "goods_img": "assets/5.webp",
      "goods_price": 156,
      "goods_count": 1,
    },
    {
      "id": 6,
      "goods_name": "ME&CITY女装冬季新款针织抽绳休闲连帽卫衣女",
      "goods_img": "assets/6.webp",
      "goods_price": 142.8,
      "goods_count": 1,
    },
    {
      "id": 7,
      "goods_name": "幂凝假两件女士卫衣秋冬女装2020年新款韩版宽松春秋季薄款ins潮外套",
      "goods_img": "assets/7.webp",
      "goods_price": 219,
      "goods_count": 2,
    },
    {
      "id": 8,
      "goods_name": "依魅人2020休闲运动衣套装女秋季新款秋季韩版宽松卫衣 时尚两件套",
      "goods_img": "assets/8.webp",
      "goods_price": 178,
      "goods_count": 1,
    },
    {
      "id": 9,
      "goods_name": "芷臻(zhizhen)加厚卫衣2020春秋季女长袖韩版宽松短款加绒春秋装连帽开衫外套冬",
      "goods_img": "assets/9.webp",
      "goods_price": 128,
      "goods_count": 1,
    },
    {
      "id": 10,
      "goods_name": "Semir森马卫衣女冬装2019新款可爱甜美大撞色小清新连帽薄绒女士套头衫",
      "goods_img": "assets/10.webp",
      "goods_price": 153,
      "goods_count": 1,
    }
  ]

  build() {
    List({ space: 20 }) {
      ForEach(this.list, (item: GoodItem) => {
        ListItem() {
          Row({ space: 10 }) {
            Image(item.goods_img)
              .borderRadius(8)
              .width(120)
              .height(200)
            Column() {
              Text(item.goods_name)
                .fontWeight(FontWeight.Bold)
              Text("¥ "+item.goods_price.toString())
                .fontColor(Color.Red)
                .fontWeight(FontWeight.Bold)
            }
            .padding({
              top: 5,
              bottom: 5
            })
            .alignItems(HorizontalAlign.Start)
            .justifyContent(FlexAlign.SpaceBetween)
            .height(200)
            .layoutWeight(1)
          }
          .width('100%')
        }
      })
    }
    .padding(20)
  }
}
  • 练习两列布局
    warning
typescript 复制代码
import { GoodItem } from './models'
import { SegmentButton, SegmentButtonOptions } from '@ohos.arkui.advanced.SegmentButton'

@Entry
@Component
struct ForEachGoodCase {
  @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.tab({
    buttons: [{ text: '单列' }, { text: '双列' }]
  })
  @State
  tabSelectedIndexes: number[] = [0]
  @State list: GoodItem[] = [
    {
      "id": 1,
      "goods_name": "班俏BANQIAO超火ins潮卫衣女士2020秋季新款韩版宽松慵懒风薄款外套带帽上衣",
      "goods_img": "assets/1.webp",
      "goods_price": 108,
      "goods_count": 1,
    },
    {
      "id": 2,
      "goods_name": "嘉叶希连帽卫衣女春秋薄款2020新款宽松bf韩版字母印花中长款外套ins潮",
      "goods_img": "assets/2.webp",
      "goods_price": 129,
      "goods_count": 1,
    },
    {
      "id": 3,
      "goods_name": "思蜜怡2020休闲运动套装女春秋季新款时尚大码宽松长袖卫衣两件套",
      "goods_img": "assets/3.webp",
      "goods_price": 198,
      "goods_count": 1,
    },
    {
      "id": 4,
      "goods_name": "思蜜怡卫衣女加绒加厚2020秋冬装新款韩版宽松上衣连帽中长款外套",
      "goods_img": "assets/4.webp",
      "goods_price": 99,
      "goods_count": 1,
    },
    {
      "id": 5,
      "goods_name": "幂凝早秋季卫衣女春秋装韩版宽松中长款假两件上衣薄款ins盐系外套潮",
      "goods_img": "assets/5.webp",
      "goods_price": 156,
      "goods_count": 1,
    },
    {
      "id": 6,
      "goods_name": "ME&CITY女装冬季新款针织抽绳休闲连帽卫衣女",
      "goods_img": "assets/6.webp",
      "goods_price": 142.8,
      "goods_count": 1,
    },
    {
      "id": 7,
      "goods_name": "幂凝假两件女士卫衣秋冬女装2020年新款韩版宽松春秋季薄款ins潮外套",
      "goods_img": "assets/7.webp",
      "goods_price": 219,
      "goods_count": 2,
    },
    {
      "id": 8,
      "goods_name": "依魅人2020休闲运动衣套装女秋季新款秋季韩版宽松卫衣 时尚两件套",
      "goods_img": "assets/8.webp",
      "goods_price": 178,
      "goods_count": 1,
    },
    {
      "id": 9,
      "goods_name": "芷臻(zhizhen)加厚卫衣2020春秋季女长袖韩版宽松短款加绒春秋装连帽开衫外套冬",
      "goods_img": "assets/9.webp",
      "goods_price": 128,
      "goods_count": 1,
    },
    {
      "id": 10,
      "goods_name": "Semir森马卫衣女冬装2019新款可爱甜美大撞色小清新连帽薄绒女士套头衫",
      "goods_img": "assets/10.webp",
      "goods_price": 153,
      "goods_count": 1,
    }
  ]

  build() {
    Column() {
      SegmentButton({ options: this.tabOptions, selectedIndexes: $tabSelectedIndexes  })
      if(this.tabSelectedIndexes.includes(0)) {
        List({ space: 20 }) {
          ForEach(this.list, (item: GoodItem) => {
            ListItem() {
              Row({ space: 10 }) {
                Image(item.goods_img)
                  .borderRadius(8)
                  .width(120)
                  .height(200)
                Column() {
                  Text(item.goods_name)
                    .fontWeight(FontWeight.Bold)
                  Text("¥ "+item.goods_price.toString())
                    .fontColor(Color.Red)
                    .fontWeight(FontWeight.Bold)
                }
                .padding({
                  top: 5,
                  bottom: 5
                })
                .alignItems(HorizontalAlign.Start)
                .justifyContent(FlexAlign.SpaceBetween)
                .height(200)
                .layoutWeight(1)
              }
              .width('100%')
            }
          })
        }
        .padding(20)
      }
      else if(this.tabSelectedIndexes.includes(1)) {
        Grid() {
          ForEach(this.list, (item: GoodItem) => {

            GridItem() {
              Column() {
                Image(item.goods_img)
                  .height(200)
                Row({ space: 2 }) {
                  Text("¥ " +item.goods_price)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(Color.Red)
                  Text(item.goods_name)
                    .maxLines(1)
                    .layoutWeight(1)
                    .fontWeight(FontWeight.Bold)
                }
              }
            }
          })
        }
        .columnsGap(20)
        .rowsGap(20)
        .columnsTemplate("1fr 1fr")
        .padding(20)
      }
    }
  }
}

// 750 -UIios  30px * (720/750)
// 720 1080
  • key的推荐建议

ForEach的第三个属性是一个回调,它是生成唯一key的, 不传的话会帮助我们生成独一无二的key

index_ + JSON.stringify(item)

鸿蒙更新的原理:循环的比较-比较你的key存在不,0_"zhangsan" 如果存在相同的key,则不更新

只改动了某一条数据,可能所有列表都会更新

ForEach的第三个参数 宁可不给 也不要瞎给

warning

info

如果数组会发生插入,删除的操作,不要使用index作为key的值,不给key渲染也是正常的,但是可能会造成渲染性能降低

  • 下面是使用Index作为key的案例
typescript 复制代码
@Entry
@Component
struct Parent {
  @State simpleList: Array<string> = ['one', 'two', 'three'];

  build() {
    Column() {
      Button() {
        Text('在第1项后插入新项').fontSize(30)
      }
      .onClick(() => {
        this.simpleList.splice(1, 0, 'new item');
      })

      ForEach(this.simpleList, (item: string) => {
        ChildItem({ item: item })
      }, (item: string, index: number) => index.toString())
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}

@Component
struct ChildItem {
  @Prop item: string;

  build() {
    Text(this.item)
      .fontSize(30)
  }
}

info

我们发现虽然数据是正确的,但是由于鸿蒙针对组件的创建原则,因为key值是索引,发现key值存在的组件存在,并不会重新创建,所以会导致数据渲染非预期限制,要想改正这个问题,要保证key的唯一性,去掉key生成器可以保证唯一性

今日案例-知乎评论

拆解组件

  • 新建ZhiHu的文件夹,新建page
  • 新建components, 里面新建 HmNavBar和HmCommentItem
typescript 复制代码
@Preview
@Component
struct HmNavBar {
  title: string = "标题"
  build() {
    Row() {
      // 返回键
      Row() {
        Image($r('app.media.ic_public_left_arrow'))
          .width(16)
          .height(16)
      }
      .width(30)
      .height(30)
      .borderRadius(15)
      .backgroundColor("#f4f4f4")
      .justifyContent(FlexAlign.Center)
      .margin({
        left: 20
      })
      Text(this.title)
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .margin({
          right: 50
        })
    }
    .width('100%')
    .height(50)
    .border({
      color: "#f4f5f6",
      width: {
        bottom: 1
      }
    })
  }
}
export { HmNavBar }
  • HmCommentItem
typescript 复制代码
@Component
struct HmCommentItem {
  build() {
    Row({ space: 10 }) {
      Image("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F1bad8264-7428-44cf-a92d-3016a2de537b%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1711626934&t=5478cb3adef5d3e29e6952934797ca39")
        .width(40)
        .height(40)
        .borderRadius(20)

      Column({ space: 10 }) {
        Text("周杰伦")
          .fontColor("#303a43")
          .fontSize(18)
          .fontWeight(FontWeight.Bold)

        Text("黄河江最近一代都带蓝牙,意大利拌面必须使用42👌钢筋混凝土量子力学")
          .fontColor("#2f3642")
          .lineHeight(22)
        Row() {
          Text("10-21 .IP属地北京")
            .fontColor("#cacaca")
            .fontSize(12)

          Row({ space: 4 }) {
            Image($r("app.media.ic_public_like"))
              .width(12)
              .height(12)
              .fillColor("#cacaca")

            Text("100")
              .fontColor("#cacaca")
              .fontSize(12)

          }
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

    }
    .alignItems(VerticalAlign.Top)
    .padding(20)
    .width('100%')
  }
}
export { HmCommentItem }
  • 在components/index.ets统一导出
typescript 复制代码
export * from './HmCommentItem'
export * from './HmNavBar'
  • 在ZhiHU/ZHihu.ets中使用
typescript 复制代码
import { HmCommentItem, HmNavBar } from './components'

@Entry
@Component
struct ZhiHu {
  build() {
   Column() {
     HmNavBar({ title: '评论回复'  })
     HmCommentItem()
     Divider().strokeWidth(6)
     Row() {
       Text("评论数50")
     }
     .width('100%')
     .height(50)
     .padding({
       left: 20
     })
     .border({
       color: '#f3f4f5',
       width: {
         bottom: 1
       }
     })
     ForEach([1,2,3,4,5,6], () => {
       HmCommentItem()
     })
   }
  }
}

需要出现滚动区域。

使用了List组件,子组件必须有ListItem/ListItemGroup

typescript 复制代码
 List() {
       ForEach([1,2,3,4,5,6], () => {
         ListItem() {
           HmCommentItem()
         }
       })
     }.layoutWeight(1)

评论列表

在ZhiHu/models/index.ets下建立如下类型

  • 定义一个评论的interface
typescript 复制代码
export interface ReplyItem {
  avatar: ResourceStr // 头像
  author: string   // 作者
  id: number  // 评论的id
  content: string // 评论内容
  time: string // 发表时间
  area: string // 地区
  likeNum: number // 点赞数量
  likeFlag: boolean | null // 当前用户是否点过赞
}

info

用i2c生成对应的class

  • 我们前面的循环为什么没有用i2c,直接用的接口呢,因为这里涉及到后续的点赞和其他业务,所以这里直接用class更新起来会更方便
typescript 复制代码
export class ReplyItemModel implements ReplyItem {
  id: number = 0
  avatar: string | Resource = ''
  author: string = ''
  content: string = ''
  time: string = ''
  area: string = ''
  likeNum: number = 0
  likeFlag: boolean | null = null

  constructor(model: ReplyItem) {
    this.id = model.id
    this.avatar = model.avatar
    this.author = model.author
    this.content = model.content
    this.time = model.time
    this.area = model.area
    this.likeNum = model.likeNum
    this.likeFlag = model.likeFlag
  }
}
  • 定义一个评论列表数据- 在Entry组件中
    info
    因为我们需要的是class对象,所以每个对象都需要new一下
typescript 复制代码
 @State commentList: ReplyItemModel[] = [
   new ReplyItemModel({
     id: 1,
     avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
     author: '偏执狂-妄想家',
     content: '更何况还分到一个摩洛哥[惊喜]',
     time: '11-30',
     area: '海南',
     likeNum: 34,
     likeFlag: false
   }) ,
    new ReplyItemModel({
      id: 2,
      avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
      author: 'William',
      content: '当年希腊可是把1:0发挥到极致了',
      time: '11-29',
      area: '北京',
      likeNum: 58,
      likeFlag: false
    }),
    new ReplyItemModel({
      id: 3,
      avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
      author: 'Andy Garcia',
      content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
      time: '11-28',
      area: '上海',
      likeNum: 10,
      likeFlag: false
    }),
    new ReplyItemModel({
      id: 4,
      avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
      author: '正宗好鱼头',
      content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
      time: '11-27',
      area: '香港',
      likeNum: 139,
      likeFlag: false
    }),
    new ReplyItemModel({
      id: 5,
      avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
      author: '柱子哥',
      content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
      time: '11-27',
      area: '旧金山',
      likeNum: 29,
      likeFlag: false
    }),
    new ReplyItemModel({
      id: 6,
      avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
      author: '飞轩逸',
      content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
      time: '11-26',
      area: '里约',
      likeNum: 100,
      likeFlag: false
    })
  ]
  • 在主页中渲染
typescript 复制代码
 List() {
       ForEach(this.commentList, (item: ReplyItemModel) => {
         ListItem() {
           HmCommentItem({ item })
         }
       })
     }.layoutWeight(1)
  • CommentItem组件接收传入数据
typescript 复制代码
import { ReplyItem, ReplyItemModel } from '../models'

@Component
struct HmCommentItem {
  // 接收渲染的选项
  item: ReplyItemModel = new ReplyItemModel({} as ReplyItem) // 初始值 只是为了语法不报错

  build() {
    Row({ space: 10 }) {
      Image(this.item.avatar)
        .width(40)
        .height(40)
        .borderRadius(20)

      Column({ space: 10 }) {
        Text(this.item.author)
          .fontColor("#303a43")
          .fontSize(18)
          .fontWeight(FontWeight.Bold)

        Text(this.item.content)
          .fontColor("#2f3642")
          .lineHeight(22)
        Row() {
          Text(`${this.item.time} .IP属地${this.item.area}`)
            .fontColor("#cacaca")
            .fontSize(12)

          Row({ space: 4 }) {
            Image($r("app.media.ic_public_like"))
              .width(12)
              .height(12)
              .fillColor("#cacaca")

            Text(this.item.likeNum.toString())
              .fontColor("#cacaca")
              .fontSize(12)

          }
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

    }
    .alignItems(VerticalAlign.Top)
    .padding(20)
    .width('100%')
  }
}
export { HmCommentItem }
  • 顶部组件同样需要new 对象传入过去
typescript 复制代码
 HmCommentItem({
        item: new ReplyItemModel({
          id: 999,
          author: '周杰伦',
          avatar: $r("app.media.10"),
          likeNum: 10,
          likeFlag: false,
          time: '03-02',
          area: '北京',
          content: '人到了一定的年龄新陈代谢就慢了,吃了胖不吃瘦了皱纹就多,要靠锻炼 '
        })
     })

底部回复按钮

封装底部的回复组件

Zhihu/components/HmReplyInput.ets

typescript 复制代码
@Component
struct HmReplyInput {
  @State
  content: string = ""
  build() {
   Row({ space: 10 }) {
     TextInput({ text: $$this.content, placeholder: '~请留下您的神评论' })
       .layoutWeight(1)
       .height(40)
     Button("发布")
   }
   .padding({ left: 10, right: 10 })
    .width('100%')
    .height(60)
  }
}
export { HmReplyInput }
  • 组件在主页中使用
typescript 复制代码
 List() {
       ForEach(this.commentList, (item: ReplyItemModel) => {
         ListItem() {
           HmCommentItem({ item })
         }
       })
     }.layoutWeight(1)
     HmReplyInput()  // 在此刻显示

实现点赞

子组件如何调用父组件的函数

  • 子组件要声明一个函数
typescript 复制代码
test: () => void = () => {}  // test:变量类型 = 初始值
  • 父组件需要给子组件传入这个参数
typescript 复制代码
Child({
  test: () => {
    this.abc()
  }
})
  • 实现点赞
typescript 复制代码
  changeLike: () => void = () => {}
  • 注册点击事件
typescript 复制代码
 Row({ space: 4 }) {
            Image($r("app.media.ic_public_like"))
              .width(12)
              .height(12)
              .fillColor(this.item.likeFlag ? Color.Red : "#cacaca")

            Text(this.item.likeNum.toString())
              .fontColor(this.item.likeFlag ? Color.Red :"#cacaca")
              .fontSize(12)
          }
          .onClick(() => {
            this.changeLike()
          })
  • 父组件定义函数,传入函数
typescript 复制代码
changeLike (item: ReplyItemModel) {
    // 需要拿到点击的数据 拿到数据更新数据即可
   // item.likeNum
   // item.likeFlag
    if(item.likeFlag) {
      // 点过赞
      item.likeNum--
    }
    else {
      // 没有点过赞
      item.likeNum++
    }
    item.likeFlag = !item.likeFlag // 取反
    // State的修饰符的更新机制
    // 只能监测到第一层
   const index = this.commentList.findIndex(obj => obj.id === item.id)
   //this.commentList[index] = item // 这么写为什么不行 // 引用类型 基础类型
    this.commentList[index] = new ReplyItemModel(item)
  }
  • 传入子组件
typescript 复制代码
List() {
       ForEach(this.commentList, (item: ReplyItemModel) => {
         ListItem() {
           HmCommentItem({
             item,
             changeLike: () => {
               this.changeLike(item)
             }
           })
         }
       })
     }.layoutWeight(1)

更新的秘密

  • 鸿蒙里面的所有的更新都只能监测到一层的更新
  • 如果要更新数组里面的某一项的话
  • this.list[index] = 新值
  • this.list.splice(index, 1, 新值)
  • 关于key的秘密

尝试给了一个id作为key,为什么没有更新

因为鸿蒙会根据key的不同来更新的内容,如果key前后一样,它认为你没有变,那就不更新

提交代码

顶部的点赞

  • 将顶部的数据抽提出来
typescript 复制代码
  @State
  showTop: boolean = true
@State
  currentComment: ReplyItemModel = new ReplyItemModel({
    id: 999,
    author: '周杰伦',
    avatar: $r("app.media.10"),
    likeNum: 10,
    likeFlag: false,
    time: '03-02',
    area: '北京',
    content: '人到了一定的年龄新陈代谢就慢了,吃了胖不吃瘦了皱纹就多,要靠锻炼 '
  })
  • 赋值
typescript 复制代码
 if(this.showTop) {
       HmCommentItem({
         item: this.currentComment,
         changeLike: () => {
           this.changeLike(this.currentComment, "top")
           this.showTop = false
           setTimeout(() => {
             this.showTop = true
           }, 100)
         }
       })
     }
  • 改造点赞的方法
typescript 复制代码
changeLike (item: ReplyItemModel, type?: "top" | "bottom") {
    // 需要拿到点击的数据 拿到数据更新数据即可
   // item.likeNum
   // item.likeFlag
    if(item.likeFlag) {
      // 点过赞
      item.likeNum--
    }
    else {
      // 没有点过赞
      item.likeNum++
    }
    item.likeFlag = !item.likeFlag // 取反
    // promptAction.showToast({ message: JSON.stringify(item), duration: 300000 })
    if(type !== "top") {
      // State的修饰符的更新机制
      // 只能监测到第一层
      const index = this.commentList.findIndex(obj => obj.id === item.id)
      //this.commentList[index] = item // 这么写为什么不行 // 引用类型 基础类型
      //  this.commentList[index] = new ReplyItemModel(item)
      this.commentList.splice(index, 1, new ReplyItemModel(item))
    }
  }

提交代码

回复评论

  • 底部输入组件双向绑定
typescript 复制代码
import { promptAction } from '@kit.ArkUI'

@Component
struct HmReplyInput {
  @State
  content: string = ""
  publishComment: (content: string) => void = () => {}
  build() {
   Row({ space: 10 }) {
     TextInput({ text: $$this.content, placeholder: '~请留下您的神评论' })
       .layoutWeight(1)
       .height(40)
       .onSubmit(() => {
         // 键盘的确定事件
         if(this.content) {
           this.publishComment(this.content)
           this.content = ""
         }
       })
     Button("发布")
       .onClick(() => {
           if(this.content) {
             this.publishComment(this.content)
             this.content = ""
           }
       })
   }
   .padding({ left: 10, right: 10 })
    .width('100%')
    .height(60)
  }
}

export { HmReplyInput }
  • 调用父组件传入的publishComment的方法
  • 父组件实现的方法
typescript 复制代码
addComment(content: string) {
     this.commentList.unshift(new ReplyItemModel({
       id: Math.random() ,
       avatar: 'https://foruda.gitee.com/avatar/1705232317138324256/1759638_itcast_panpu_1705232317.png',
       author: '老潘',
       content,
       time: `${(new Date().getMonth() + 1).toString().padStart(2, "0")}-${new Date().getDate().toString().padStart(2,  "0")}`,
       area: '北京',
       likeNum: 0,
       likeFlag: false
     }))

    // 控制滚动条
    this.scroller.scrollEdge(Edge.Top)
  }
  • 实现传入方法
typescript 复制代码
  HmReplyInput({
       publishComment: (content: string) => {
          this.addComment(content)
       }
     })
  • 实现滚动顶部
typescript 复制代码
//创建scroller
scroller:Scroller = new Scroller()
//传入scroller
List({ scroller: this.scroller })
相关推荐
楚疏笃23 分钟前
鸿蒙学习自由流转与分布式运行环境-价值与架构定义(1)
学习·架构·harmonyos
架构文摘JGWZ39 分钟前
替代Postman ,17.3K star!
学习·工具
NightCyberpunk40 分钟前
Ajax与Vue初步学习
vue.js·学习·ajax
宇寒风暖1 小时前
软件工程——UML简介
笔记·学习·软件工程
ZZZ_O^O1 小时前
【贪心算法第五弹——300.最长递增子序列】
c++·学习·算法·leetcode·贪心算法
&黄昏的乐师1 小时前
Opencv+ROS实现颜色识别应用
人工智能·opencv·学习·计算机视觉
蒙特网站1 小时前
网站布局编辑器前端开发:设计要点与关键考量
前端·javascript·学习·html
SameX1 小时前
HarmonyOS Next 运用 FIDO 实现金融级安全认证实战
harmonyos
@曲终1 小时前
C语言学习 12(指针学习1)
c语言·经验分享·笔记·学习
一只小菜鸡..2 小时前
241126学习日志——[CSDIY] [ByteDance] 后端训练营 [19]
学习