【HarmonyOS之旅】基于ArkTS开发(二) -> UI开发二

目录

1 -> 声明式UI开发指导

1.1 -> 开发说明

1.2 -> 创建页面

1.3 -> 修改组件样式

1.4 -> 更新页面内容

2 -> 创建简单视图

2.1 -> 构建Stack布局

2.2 -> 构建Flex布局

2.3 -> 构建食物数据模型

2.4 -> 构建食物列表List布局

2.5 -> 构建食物分类Grid布局

2.6 -> 页面跳转与数据传递

2.6.1 -> 页面跳转

2.6.2 -> 页面间数据传递


1 -> 声明式UI开发指导

1.1 -> 开发说明

声明式UI的通用开发历程如下表所示。

任务简介
准备开发环境
  • 了解声明式UI的工程结构。
  • 了解资源分类与访问。
学习ArkTS语言ArkTS是HarmonyOS优选的主力应用开发语言,当前,ArkTS在TS基础上主要扩展了声明式UI能力。
开发页面
  • 根据页面的使用场景,选择合适的布局。
  • 根据页面需要实现的内容,添加系统内置组件,并修改组件样式。
  • 更新页面内容,丰富页面展现形式。
页面多样化绘图和动画。
页面之间的跳转使用页面路由实现多个页面之前的跳转。
性能提升避免低性能代码对应用的性能造成负面影响。

1.2 -> 创建页面

先根据页面预期效果选择布局结构创建页面,并在页面中添加基础的系统内置组件。下述示例采用了弹性布局(Flex),对页面中的Text组件进行横纵向居中布局显示。

// test.ets
@Entry
@Component
struct MyComponent {build() {Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {Text('One Piece')}.width('100%').height('100%')}
}

1.3 -> 修改组件样式

在页面中添加系统内置组件时,若不设置属性方法,则会显示其默认样式。通过更改组件的属性样式或者组件支持的通用属性样式,改变组件的UI显示。

  1. 通过修改Text组件的构造参数,将Text组件的显示内容修改为“Tomato”。

  2. 修改Text组件的fontSize属性更改组件的字体大小,将字体大小设置为26,通过fontWeight属性更改字体粗细,将其设置为500。

// test2.ets
@Entry
@Component
struct MyComponent {build() {Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {Text('Tomato').fontSize(26).fontWeight(500)}.width('100%').height('100%')}
}

1.4 -> 更新页面内容

在创建基本的页面之后,可根据组件的状态来更新页面内容。以下示例展示了简单的更新页面方法。

说明

更新组件的状态之前,请先初始化组件的成员变量。自定义组件的成员变量可以通过本地初始化和在构造组件时通过构造参数初始化两种方式实现,具体允许哪种方式取决于该变量所使用的装饰器。

// test3.ets
@Entry
@Component
struct ParentComp {@State isCountDown: boolean = truebuild() {Column() {Text(this.isCountDown ? 'Count Down' : 'Stopwatch').fontSize(20).margin(20)if (this.isCountDown) {// 图片资源放在media目录下Image($r("app.media.countdown")).width(120).height(120)TimerComponent({ counter: 10, changePerSec: -1, showInColor: Color.Red })} else {// 图片资源放在media目录下Image($r("app.media.stopwatch")).width(120).height(120)TimerComponent({ counter: 0, changePerSec: +1, showInColor: Color.Black })}Button(this.isCountDown ? 'Switch to Stopwatch' : 'Switch to Count Down').onClick(() => {this.isCountDown = !this.isCountDown})}.width('100%')}
}// 自定义计时器/倒计时组件
@Component
struct TimerComponent {@State counter: number = 0private changePerSec: number = -1private showInColor: Color = Color.Blackprivate timerId: number = -1build() {Text(`${this.counter}sec`).fontColor(this.showInColor).fontSize(20).margin(20)}aboutToAppear() {this.timerId = setInterval(() => {this.counter += this.changePerSec}, 1000)}aboutToDisappear() {if (this.timerId > 0) {clearTimeout(this.timerId)this.timerId = -1}}
}

初始创建和渲染:

  1. 创建父组件ParentComp;

  2. 本地初始化ParentComp的状态变量isCountDown;

  3. 执行ParentComp的build函数;

  4. 创建Column组件;

    a. 创建Text组件,设置其文本展示内容,并将Text组件实例添加到Column中;                      b. 判断if条件,创建true条件下的元素;

                i. 使用给定的构造函数创建TimerComponent;

                ii. 创建Image组件,并设置其图片源地址;

        c. 创建Button内置组件,设置相应的内容。

状态更新:

用户单击按钮时:

  1. ParentComp的isCountDown状态变量的值更改为false;

  2. 执行ParentComp的build函数;

  3. Column组件被重用,并重新初始化;

  4. Column的子组件会重用内存中的对象,并且重新初始化;

         a. Text组件被重用,使用新的文本内容重新初始化;

         b. 判断if条件,使用false条件下的元素;

                 i. 创建false条件下的组件;

                ii. 销毁原来true条件下的组件;

        c. 重用Button组件,使用新的图片源地址。

2 -> 创建简单视图

2.1 -> 构建Stack布局

1. 创建食物名称。

删掉工程模板的build方法的代码,创建Stack组件,将Text组件放进Stack组件的花括号中,使其成为Stack组件的子组件。Stack组件为堆叠组件,可以包含一个或多个子组件,其特点是后一个子组件覆盖前一个子组件。

@Entry
@Component
struct MyComponent {build() {Stack() {Text('Tomato').fontSize(26).fontWeight(500)}}
}

  

2. 食物图片展示。

创建Image组件,指定Image组件的url,Image组件和Text组件都是必选构造参数组件。为了让Text组件在Image组件上方显示,所以要先声明Image组件。图片资源放在resources下的rawfile文件夹内,引用rawfile下资源时使用$rawfile('filename')的形式,filename为rawfile目录下的文件相对路径。当前$rawfile仅支持Image控件引用图片资源。

@Entry
@Component
struct MyComponent {build() {Stack() {Image($rawfile('Tomato.png'))Text('Tomato').fontSize(26).fontWeight(500)}}
}

  

3. 通过资源访问图片。

除指定图片路径外,也可以使用引用媒体资源符$r引用资源,需要遵循resources文件夹的资源限定词的规则。右键resources文件夹,点击New>Resource Directory,选择Resource Type为Media(图片资源)。

将Tomato.png放入media文件夹内。就可以通过$r('app.type.name')的形式引用应用资源,即$r('app.media.Tomato')。

@Entry
@Component
struct MyComponent {build() {Stack() {Image($r('app.media.Tomato')).objectFit(ImageFit.Contain).height(357)Text('Tomato').fontSize(26).fontWeight(500)}}
}

4. 设置Image宽高,并且将image的objectFit属性设置为ImageFit.Contain,即保持图片长宽比的情况下,使得图片完整地显示在边界内。

如果Image填满了整个屏幕,原因如下:

  1. Image没有设置宽高。

  2. Image的objectFit默认属性是ImageFit.Cover,即在保持长宽比的情况下放大或缩小,使其填满整个显示边界。

@Entry
@Component
struct MyComponent {build() {Stack() {Image($r('app.media.Tomato')).objectFit(ImageFit.Contain).height(357)Text('Tomato').fontSize(26).fontWeight(500)}}
}

  

5. 设置食物图片和名称布局。

设置Stack的对齐方式为底部起始端对齐,Stack默认为居中对齐。设置Stack构造参数alignContent为Alignment.BottomStart。其中Alignment和FontWeight一样,都是框架提供的内置枚举类型。

@Entry
@Component
struct MyComponent {build() {Stack({ alignContent: Alignment.BottomStart }) {Image($r('app.media.Tomato')).objectFit(ImageFit.Contain).height(357)Text('Tomato').fontSize(26).fontWeight(500)}}
}

  

6. 调整Text组件的外边距margin,使其距离左侧和底部有一定的距离。

margin是简写属性,可以统一指定四个边的外边距,也可以分别指定。具体设置方式如下:

  1. 参数为Length时,即统一指定四个边的外边距,比如margin(20),即上、右、下、左四个边的外边距都是20。
  2. 参数为{top?: Length, right?: Length, bottom?: Length, left?:Length},即分别指定四个边的边距,比如margin({ left: 26, bottom: 17.4 }),即左边距为26,下边距为17.4。
@Entry
@Component
struct MyComponent {build() {Stack({ alignContent: Alignment.BottomStart }) {Image($r('app.media.Tomato')).objectFit(ImageFit.Contain).height(357)Text('Tomato').fontSize(26).fontWeight(500).margin({left: 26, bottom: 17.4})}   }
}

  

7. 调整组件间的结构,语义化组件名称。

创建页面入口组件为FoodDetail,在FoodDetail中创建Column,设置水平方向上居中对齐 alignItems(HorizontalAlign.Center)。MyComponent组件名改为FoodImageDisplay,为FoodDetail的子组件。

Column是子组件竖直排列的容器组件,本质为线性布局,所以只能设置交叉轴方向的对齐。

@Component
struct FoodImageDisplay {build() {Stack({ alignContent: Alignment.BottomStart }) {Image($r('app.media.Tomato')).objectFit(ImageFit.Contain)Text('Tomato').fontSize(26).fontWeight(500).margin({ left: 26, bottom: 17.4 })}.height(357)   }
}@Entry
@Component
struct FoodDetail {build() {Column() {FoodImageDisplay()}.alignItems(HorizontalAlign.Center)}
}

2.2 -> 构建Flex布局

可以使用Flex弹性布局来构建食物的食物成分表,弹性布局在本场景的优势在于可以免去多余的宽高计算,通过比例来设置不同单元格的大小,更加灵活。

 1. 创建ContentTable组件,使其成为页面入口组件FoodDetail的子组件。

@Component
struct FoodImageDisplay {build() {Stack({ alignContent: Alignment.BottomStart }) {Image($r('app.media.Tomato')).objectFit(ImageFit.Contain).height(357)Text('Tomato').fontSize(26).fontWeight(500).margin({ left: 26, bottom: 17.4 })}   }
}@Component
struct ContentTable {build() {}
}@Entry
@Component
struct FoodDetail {build() {Column() {FoodImageDisplay()ContentTable()}.alignItems(HorizontalAlign.Center)}
}

2. 创建Flex组件展示Tomato两类成分。

一类是热量Calories,包含卡路里(Calories);一类是营养成分Nutrition,包含蛋白质(Protein)、脂肪(Fat)、碳水化合物(Carbohydrates)和维生素C(VitaminC)。

先创建热量这一类。创建Flex组件,高度为280,上、右、左内边距为30,包含三个Text子组件分别代表类别名(Calories),含量名称(Calories)和含量数值(17kcal)。Flex组件默认为水平排列方式。

已省略FoodImageDisplay代码,只针对ContentTable进行扩展。

@Component
struct ContentTable {build() {Flex() {Text('Calories').fontSize(17.4).fontWeight(FontWeight.Bold)Text('Calories').fontSize(17.4)Text('17kcal').fontSize(17.4)}.height(280).padding({ top: 30, right: 30, left: 30 })}
}@Entry
@Component
struct FoodDetail {build() {Column() {FoodImageDisplay()ContentTable()}.alignItems(HorizontalAlign.Center)}
}

  

3. 调整布局,设置各部分占比。

分类名占比(layoutWeight)为1,成分名和成分含量一共占比(layoutWeight)2。成分名和成分含量位于同一个Flex中,成分名占据所有剩余空间flexGrow(1)。

@Component
struct FoodImageDisplay {build() {Stack({ alignContent: Alignment.BottomStart }) {Image($r('app.media.Tomato')).objectFit(ImageFit.Contain).height(357)Text('Tomato').fontSize(26).fontWeight(500).margin({ left: 26, bottom: 17.4 })}  }
}@Component
struct ContentTable {build() {Flex() {Text('Calories').fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex() {Text('Calories').fontSize(17.4).flexGrow(1)Text('17kcal').fontSize(17.4)}.layoutWeight(2)}.height(280).padding({ top: 30, right: 30, left: 30 })}
}@Entry
@Component
struct FoodDetail {build() {Column() {FoodImageDisplay()ContentTable()}.alignItems(HorizontalAlign.Center)}
}

  

4. 仿照热量分类创建营养成分分类。

营养成分部分(Nutrition)包含:蛋白质(Protein)、脂肪(Fat)、碳水化合物(Carbohydrates)和维生素C(VitaminC)四个成分,后三个成分在表格中省略分类名,用空格代替。

设置外层Flex为竖直排列FlexDirection.Column, 在主轴方向(竖直方向)上等距排列FlexAlign.SpaceBetween,在交叉轴方向(水平轴方向)上首部对齐排列ItemAlign.Start。

@Component
struct ContentTable {build() {Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {Flex() {Text('Calories').fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex() {Text('Calories').fontSize(17.4).flexGrow(1)Text('17kcal').fontSize(17.4)}.layoutWeight(2)}Flex() {Text('Nutrition').fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex() {Text('Protein').fontSize(17.4).flexGrow(1)Text('0.9g').fontSize(17.4)}.layoutWeight(2)}Flex() {Text(' ').fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex() {Text('Fat').fontSize(17.4).flexGrow(1)Text('0.2g').fontSize(17.4)}.layoutWeight(2)}Flex() {Text(' ').fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex() {Text('Carbohydrates').fontSize(17.4).flexGrow(1)Text('3.9g').fontSize(17.4)}.layoutWeight(2)}Flex() {Text(' ').fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex() {Text('vitaminC').fontSize(17.4).flexGrow(1)Text('17.8mg').fontSize(17.4)}.layoutWeight(2)}}.height(280).padding({ top: 30, right: 30, left: 30 })}
}@Entry
@Component
struct FoodDetail {build() {Column() {FoodImageDisplay()ContentTable()}.alignItems(HorizontalAlign.Center)}
}

5. 使用自定义构造函数@Builder简化代码。可以发现,每个成分表中的成分单元其实都是一样的UI结构。

  

当前对每个成分单元都进行了声明,造成了代码的重复和冗余。可以使用@Builder来构建自定义方法,抽象出相同的UI结构声明。@Builder修饰的方法和Component的build方法都是为了声明一些UI渲染结构,遵循一样的ArkTS语法。可以定义一个或者多个@Builder修饰的方法,但Component的build方法必须只有一个。

在ContentTable内声明@Builder修饰的IngredientItem方法,用于声明分类名、成分名称和成分含量UI描述。

@Component
struct ContentTable {@Builder IngredientItem(title:string, name: string, value: string) {Flex() {Text(title).fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex({ alignItems: ItemAlign.Center }) {Text(name).fontSize(17.4).flexGrow(1)Text(value).fontSize(17.4)}.layoutWeight(2)}}
}

在ContentTable的build方法内调用IngredientItem接口,需要用this去调用该Component作用域内的方法,以此来区分全局的方法调用。

@Component
struct ContentTable {......build() {Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {this.IngredientItem('Calories', 'Calories', '17kcal')this.IngredientItem('Nutrition', 'Protein', '0.9g')this.IngredientItem('', 'Fat', '0.2g')this.IngredientItem('', 'Carbohydrates', '3.9g')this.IngredientItem('', 'VitaminC', '17.8mg')}.height(280).padding({ top: 30, right: 30, left: 30 })}
}

ContentTable组件整体代码如下。

@Component
struct ContentTable {@Builder IngredientItem(title:string, name: string, value: string) {Flex() {Text(title).fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex() {Text(name).fontSize(17.4).flexGrow(1)Text(value).fontSize(17.4)}.layoutWeight(2)}}build() {Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {this.IngredientItem('Calories', 'Calories', '17kcal')this.IngredientItem('Nutrition', 'Protein', '0.9g')this.IngredientItem('', 'Fat', '0.2g')this.IngredientItem('', 'Carbohydrates', '3.9g')this.IngredientItem('', 'VitaminC', '17.8mg')}.height(280).padding({ top: 30, right: 30, left: 30 })}
}@Entry
@Component
struct FoodDetail {build() {Column() {FoodImageDisplay()ContentTable()}.alignItems(HorizontalAlign.Center)}
}

  

2.3 -> 构建食物数据模型

在创建视图中,逐一去表述食物的各个信息,如食物名称、卡路里、蛋白质、脂肪、碳水和维生素C。这样的编码形式在实际的开发中肯定是不切实际的,所以要创建食物数据模型来统一存储和管理数据。

  

1. 新建model文件夹,在model目录下创建FoodData.ets。

2. 定义食物数据的存储模型FoodData和枚举变量Category,FoodData类包含食物id、名称(name)、分类(category)、图片(image)、热量(calories)、蛋白质(protein)、脂肪(fat)、碳水(carbohydrates)和维生素C(vitaminC)属性。

ArkTS语言是在ts语言的基础上的扩展,同样支持ts语法。

enum Category  {Fruit,Vegetable,Nut,Seafood,Dessert
}let NextId = 0;
class FoodData {id: string;name: string;image: Resource;category: Category;calories: number;protein: number;fat: number;carbohydrates: number;vitaminC: number;constructor(name: string, image: Resource, category: Category, calories: number, protein: number, fat: number, carbohydrates: number, vitaminC: number) {this.id = `${ NextId++ }`;this.name = name;this.image = image;this.category = category;this.calories = calories;this.protein = protein;this.fat = fat;this.carbohydrates = carbohydrates;this.vitaminC = vitaminC;}
}

3. 存入食物图片资源。在resources >base> media目录下存入食物图片资源,图片名称为食物名称。

4. 创建食物资源数据。在model文件夹下创建FoodDataModels.ets,在该页面中声明食物成分数组FoodComposition。

实际开发中,可以自定义更多的数据资源,当食物资源很多时,建议使用数据懒加载LazyForEach。

const FoodComposition: any[] = [{ 'name': 'Tomato', 'image': $r('app.media.Tomato'), 'category': Category.Vegetable, 'calories': 17, 'protein': 0.9, 'fat': 0.2, 'carbohydrates': 3.9, 'vitaminC': 17.8 },{ 'name': 'Walnut', 'image': $r('app.media.Walnut'), 'category': Category.Nut, 'calories': 654 , 'protein': 15, 'fat': 65, 'carbohydrates': 14, 'vitaminC': 1.3 }]

5. 创建initializeOnStartUp方法来初始化FoodData的数组。在FoodDataModels.ets中使用了定义在FoodData.ets的FoodData和Category,所以要将FoodData.ets的FoodData类export,在FoodDataModels.ets内import FoodData和Category。

// FoodData.ets
export enum Category {......
}
export class FoodData {......
}
// FoodDataModels.ets
import { Category, FoodData } from './FoodData'export function initializeOnStartup(): Array<FoodData> {let FoodDataArray: Array<FoodData> = []FoodComposition.forEach(item => {FoodDataArray.push(new FoodData(item.name, item.image, item.category, item.calories, item.protein, item.fat, item.carbohydrates, item.vitaminC ));})return FoodDataArray;
}

已完成好健康饮食应用的数据资源准备,接下来将通过加载这些数据来创建食物列表页面。

2.4 -> 构建食物列表List布局

使用List组件和ForEach循环渲染,构建食物列表布局。

1. 在pages目录新建页面FoodCategoryList.ets。右键点击pages文件夹,选择“New > Page”,将Page name修改为“FoodCategoryList”。

2. 新建FoodList组件作为页面入口组件,FoodListItem为其子组件。List组件是列表组件,适用于重复同类数据的展示,其子组件为ListItem,适用于展示列表中的单元。

@Component
struct FoodListItem {build() {}
}@Entry
@Component
struct FoodList {build() {List() {ListItem() {FoodListItem()}}}
}

3. 引入FoodData类和initializeOnStartup方法。

import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'

4. FoodList和FoodListItem组件数值传递。

在FoodList组件内创建类型为FoodData[]成员变量foodItems,调用initializeOnStartup方法为其赋值。在FoodListItem组件内创建类型为FoodData的成员变量foodItem。将父组件foodItems数组的第一个元素的foodItems[0]作为参数传递给FoodListItem。

import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'@Component
struct FoodListItem {private foodItem: FoodDatabuild() {}
}@Entry
@Component
struct FoodList {private foodItems: FoodData[] = initializeOnStartup()build() {List() {ListItem() {FoodListItem({ foodItem: this.foodItems[0] })}}}
}

5. 声明子组件FoodListItem 的UI布局。创建Flex组件,包含食物图片缩略图,食物名称,和食物对应的卡路里。

import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'@Component
struct FoodListItem {private foodItem: FoodDatabuild() {Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {Image(this.foodItem.image).objectFit(ImageFit.Contain).height(40).width(40)       .margin({ right: 16 })Text(this.foodItem.name).fontSize(14).flexGrow(1)Text(this.foodItem.calories + ' kcal').fontSize(14)}.height(64).margin({ right: 24, left:32 })}
}@Entry
@Component
struct FoodList {private foodItems: FoodData[] = initializeOnStartup()build() {List() {ListItem() {FoodListItem({ foodItem: this.foodItems[0] })}}}
}

6. 创建两个FoodListItem。在List组件创建两个FoodListItem,分别给FoodListItem传递foodItems数组的第一个元素this.foodItems[0]和第二个元素foodItem: this.foodItems[1]。

import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'@Component
struct FoodListItem {private foodItem: FoodDatabuild() {Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {Image(this.foodItem.image).objectFit(ImageFit.Contain).height(40).width(40)              .margin({ right: 16 })Text(this.foodItem.name).fontSize(14).flexGrow(1)Text(this.foodItem.calories + ' kcal').fontSize(14)}.height(64).margin({ right: 24, left:32 })}
}@Entry
@Component
struct FoodList {private foodItems: FoodData[] = initializeOnStartup()build() {List() {ListItem() {FoodListItem({ foodItem: this.foodItems[0] })}ListItem() {FoodListItem({ foodItem: this.foodItems[1] })}}}
}

7. 单独创建每一个FoodListItem肯定是不合理的。这就需要引入ForEach循环渲染,ForEach语法如下。

ForEach(arr: any[], // Array to be iterateditemGenerator: (item: any) => void, // child component generatorkeyGenerator?: (item: any) => string // (optional) Unique key generator, which is recommended.
)

ForEach组有三个参数,第一个参数是需要被遍历的数组,第二个参数为生成子组件的lambda函数,第三个参数是键值生成器。出于性能原因,即使第三个参数是可选的,强烈建议开发者提供。keyGenerator使开发框架能够更好地识别数组更改,而不必因为item的更改重建全部节点。

遍历foodItems数组循环创建ListItem组件,foodItems中每一个item都作为参数传递给FoodListItem组件。

ForEach(this.foodItems, item => {ListItem() {FoodListItem({ foodItem: item })}
}, item => item.id.toString())

整体的代码如下。

import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'@Component
struct FoodListItem {private foodItem: FoodDatabuild() {Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {Image(this.foodItem.image).objectFit(ImageFit.Contain).height(40).width(40)     .margin({ right: 16 })Text(this.foodItem.name).fontSize(14).flexGrow(1)Text(this.foodItem.calories + ' kcal').fontSize(14)}.height(64).margin({ right: 24, left:32 })}
}@Entry
@Component
struct FoodList {private foodItems: FoodData[] = initializeOnStartup()build() {List() {ForEach(this.foodItems, item => {ListItem() {FoodListItem({ foodItem: item })}}, item => item.id.toString())}}
}

8. 添加FoodList标题。

@Entry
@Component
struct FoodList {private foodItems: FoodData[] = initializeOnStartup()build() {Column() {Flex({justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center}) {Text('Food List').fontSize(20).margin({ left:20 })}.height('7%').backgroundColor('#FFf1f3f5')List() {ForEach(this.foodItems, item => {ListItem() {FoodListItem({ foodItem: item })}}, item => item.id.toString())}.height('93%')}}
}

2.5 -> 构建食物分类Grid布局

健康饮食应用在主页提供给用户两种食物显示方式:列表显示和网格显示。开发者将实现通过页签切换不同食物分类的网格布局。

1. 将Category枚举类型引入FoodCategoryList页面。

import { Category, FoodData } from '../model/FoodData'

2. 创建FoodCategoryList和FoodCategory组件,其中FoodCategoryList作为新的页面入口组件,在入口组件调用initializeOnStartup方法。

@Component
struct FoodList {private foodItems: FoodData[]build() {......}
}@Component
struct FoodCategory {private foodItems: FoodData[]build() {......}
}@Entry
@Component
struct FoodCategoryList {private foodItems: FoodData[] = initializeOnStartup()build() {......}
}

3. 在FoodCategoryList组件内创建showList成员变量,用于控制List布局和Grid布局的渲染切换。需要用到条件渲染语句if...else...。

@Entry
@Component
struct FoodCategoryList {private foodItems: FoodData[] = initializeOnStartup()private showList: boolean = falsebuild() {Stack() {if (this.showList) {FoodList({ foodItems: this.foodItems })} else {FoodCategory({ foodItems: this.foodItems })}}}
}

4. 在页面右上角创建切换List/Grid布局的图标。设置Stack对齐方式为顶部尾部对齐TopEnd,创建Image组件,设置其点击事件,即showList取反。

@Entry
@Component
struct FoodCategoryList {private foodItems: FoodData[] = initializeOnStartup()private showList: boolean = falsebuild() {Stack({ alignContent: Alignment.TopEnd }) {if (this.showList) {FoodList({ foodItems: this.foodItems })} else {FoodCategory({ foodItems: this.foodItems })}Image($r('app.media.Switch')).height(24).width(24).margin({ top: 15, right: 10 }).onClick(() => {this.showList = !this.showList})}.height('100%')}
}

5. 添加@State装饰器。点击右上角的switch标签后,页面没有任何变化,这是因为showList不是有状态数据,它的改变不会触发页面的刷新。需要为其添加@State装饰器,使其成为状态数据,它的改变会引起其所在组件的重新渲染。

@Entry
@Component
struct FoodCategoryList {private foodItems: FoodData[] = initializeOnStartup()@State private showList: boolean = falsebuild() {Stack({ alignContent: Alignment.TopEnd }) {if (this.showList) {FoodList({ foodItems: this.foodItems })} else {FoodCategory({ foodItems: this.foodItems })}Image($r('app.media.Switch')).height(24).width(24).margin({ top: 15, right: 10 }).onClick(() => {this.showList = !this.showList})}.height('100%')}
}

点击切换图标,FoodList组件出现,再次点击,FoodList组件消失。

6. 创建显示所有食物的页签(All)。

在FoodCategory组件内创建Tabs组件和其子组件TabContent,设置tabBar为All。设置TabBars的宽度为280,布局模式为Scrollable,即超过总长度后可以滑动。Tabs是一种可以通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图TabContent。

@Component
struct FoodCategory {private foodItems: FoodData[]build() {Stack() {Tabs() {TabContent() {}.tabBar('All')}.barWidth(280).barMode(BarMode.Scrollable)}}
}

7. 创建FoodGrid组件,作为TabContent的子组件。

@Component
struct FoodGrid {private foodItems: FoodData[]build() {}
}@Component
struct FoodCategory {private foodItems: FoodData[]build() {Stack() {Tabs() {TabContent() {FoodGrid({ foodItems: this.foodItems })}.tabBar('All')}.barWidth(280).barMode(BarMode.Scrollable)}}
}

8. 实现2 * 6的网格布局(一共12个食物数据资源)。

创建Grid组件,设置列数columnsTemplate('1fr 1fr'),行数rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr'),行间距和列间距rowsGap和columnsGap为8。创建Scroll组件,使其可以滑动。

@Component
struct FoodGrid {private foodItems: FoodData[]build() {Scroll() {Grid() {ForEach(this.foodItems, (item: FoodData) => {GridItem() {}}, (item: FoodData) => item.id.toString())}.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr').columnsTemplate('1fr 1fr').columnsGap(8).rowsGap(8)}.scrollBar(BarState.Off).padding({left: 16, right: 16})}
}

9. 创建FoodGridItem组件,展示食物图片、名称和卡路里,实现其UI布局,为GridItem的子组件。每个FoodGridItem高度为184,行间距为8,设置Grid总高度为(184 + 8) * 6 - 8 = 1144。

@Component
struct FoodGridItem {private foodItem: FoodDatabuild() {Column() {Row() {Image(this.foodItem.image).objectFit(ImageFit.Contain).height(152).width('100%')}Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {Text(this.foodItem.name).fontSize(14).flexGrow(1).padding({ left: 8 })Text(this.foodItem.calories + 'kcal').fontSize(14).margin({ right: 6 })}.height(32).width('100%').backgroundColor('#FFe5e5e5')}.height(184).width('100%')}
}@Component
struct FoodGrid {private foodItems: FoodData[]build() {Scroll() {Grid() {ForEach(this.foodItems, (item: FoodData) => {GridItem() {FoodGridItem({foodItem: item})}}, (item: FoodData) => item.id.toString())}.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr').columnsTemplate('1fr 1fr').columnsGap(8).rowsGap(8).height(1144)}.scrollBar(BarState.Off).padding({ left: 16, right: 16 })}
}

10. 创建展示蔬菜(Category.Vegetable)、水果(Category.Fruit)、坚果(Category.Nut)、海鲜(Category.SeaFood)和甜品(Category.Dessert)分类的页签。

@Component
struct FoodCategory {private foodItems: FoodData[]build() {Stack() {Tabs() {TabContent() {FoodGrid({ foodItems: this.foodItems })}.tabBar('All')TabContent() {FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Vegetable)) })}.tabBar('Vegetable')TabContent() {FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Fruit)) })}.tabBar('Fruit')TabContent() {FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Nut)) })}.tabBar('Nut')TabContent() {FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Seafood)) })}.tabBar('Seafood')TabContent() {FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Dessert)) })}.tabBar('Dessert')}.barWidth(280).barMode(BarMode.Scrollable)}}
}

11. 设置不同食物分类的Grid的行数和高度。因为不同分类的食物数量不同,所以不能用'1fr 1fr 1fr 1fr 1fr 1fr '常量来统一设置成6行。

创建gridRowTemplate和HeightValue成员变量,通过成员变量设置Grid行数和高度。

@Component
struct FoodGrid {private foodItems: FoodData[]private gridRowTemplate : string = ''private heightValue: numberbuild() {Scroll() {Grid() {ForEach(this.foodItems, (item: FoodData) => {GridItem() {FoodGridItem({foodItem: item})}}, (item: FoodData) => item.id.toString())}.rowsTemplate(this.gridRowTemplate).columnsTemplate('1fr 1fr').columnsGap(8).rowsGap(8).height(this.heightValue)}.scrollBar(BarState.Off).padding({left: 16, right: 16})}
}

调用aboutToAppear接口计算行数(gridRowTemplate)和高度(heightValue)。

aboutToAppear() {var rows = Math.round(this.foodItems.length / 2);this.gridRowTemplate = '1fr '.repeat(rows);this.heightValue = rows * 192 - 8;
}

自定义组件提供了两个生命周期的回调接口aboutToAppear和aboutToDisappear。aboutToAppear的执行时机在创建自定义组件后,执行自定义组件build方法之前。aboutToDisappear在自定义组件的去初始化的时机执行。

@Component
struct FoodGrid {private foodItems: FoodData[]private gridRowTemplate : string = ''private heightValue: numberaboutToAppear() {var rows = Math.round(this.foodItems.length / 2);this.gridRowTemplate = '1fr '.repeat(rows);this.heightValue = rows * 192 - 8;}build() {Scroll() {Grid() {ForEach(this.foodItems, (item: FoodData) => {GridItem() {FoodGridItem({foodItem: item})}}, (item: FoodData) => item.id.toString())}.rowsTemplate(this.gridRowTemplate).columnsTemplate('1fr 1fr').columnsGap(8).rowsGap(8).height(this.heightValue)}.scrollBar(BarState.Off).padding({left: 16, right: 16})}
}

2.6 -> 页面跳转与数据传递

2.6.1 -> 页面跳转

声明式UI范式提供了两种机制来实现页面间的跳转:

  1. 路由容器组件Navigator,包装了页面路由的能力,指定页面target后,使其包裹的子组件都具有路由能力。

  2. 路由RouterAPI接口,通过在页面上引入router,可以调用router的各种接口,从而实现页面路由的各种操作。

下面就分别学习这两种跳转机制来实现食物分类列表页面和食物详情页的链接。

1. 点击FoodListItem后跳转到FoodDetail页面。在FoodListItem内创建Navigator组件,使其子组件都具有路由功能,目标页面target为'pages/FoodDetail'。

@Component
struct FoodListItem {private foodItem: FoodDatabuild() {Navigator({ target: 'pages/FoodDetail' }) {Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {Image(this.foodItem.image).objectFit(ImageFit.Contain).height(40).width(40)         .margin({ right: 16 })Text(this.foodItem.name).fontSize(14).flexGrow(1)Text(this.foodItem.calories + ' kcal').fontSize(14)}.height(64)}.margin({ right: 24, left:32 })}
}

2. 点击FoodGridItem后跳转到FoodDetail页面。调用页面路由router模块的push接口,将FoodDetail页面推到路由栈中,实现页面跳转。使用router路由API接口,需要先引入router。

import router from '@ohos.router'@Component
struct FoodGridItem {private foodItem: FoodDatabuild() {Column() {......}.height(184).width('100%').onClick(() => {router.push({ url: 'pages/FoodDetail' })})}
}

3. 在FoodDetail页面增加回到食物列表页面的图标。在resources > base > media文件夹下存入回退图标Back.png。新建自定义组件PageTitle,包含后退的图标和Food Detail的文本,调用路由的router.back()接口,弹出路由栈最上面的页面,即返回上一级页面。

// FoodDetail.ets
import router from '@ohos.router'@Component
struct PageTitle {build() {Flex({ alignItems: ItemAlign.Start }) {Image($r('app.media.Back')).width(21.8).height(19.6)Text('Food Detail').fontSize(21.8).margin({left: 17.4})}.height(61).padding({ top: 13, bottom: 15, left: 28.3 }).onClick(() => {router.back()})}
}

4. 在FoodDetail组件内创建Stack组件,包含子组件FoodImageDisplay和PageTitle子组件,设置其对齐方式为左上对齐TopStart。

@Entry
@Component
struct FoodDetail {build() {Column() {Stack( { alignContent: Alignment.TopStart }) {FoodImageDisplay()PageTitle()}ContentTable()}.alignItems(HorizontalAlign.Center)}
}

2.6.2 -> 页面间数据传递

已经完成了FoodCategoryList页面和FoodDetail页面的跳转和回退,但是点击不同的FoodListItem/FoodGridItem,跳转的FoodDetail页面都是西红柿Tomato的详细介绍,这是因为没有构建起两个页面的数据传递,需要用到携带参数(parameter)路由。

1. 在FoodListItem组件的Navigator设置其params属性,params属性接受key-value的Object。

// FoodList.ets
@Component
struct FoodListItem {private foodItem: FoodDatabuild() {Navigator({ target: 'pages/FoodDetail' }) {......}.params({ foodData: this.foodItem })}
}

FoodGridItem调用的routerAPI同样有携带参数跳转的能力,使用方法和Navigator类似。

router.push({url: 'pages/FoodDetail',params: { foodData: this.foodItem }
})

2. FoodDetail页面引入FoodData类,在FoodDetail组件内添加foodItem成员变量。

// FoodDetail.ets
import { FoodData } from '../model/FoodData'@Entry
@Component
struct FoodDetail {private foodItem: FoodDatabuild() {......}
}

3. 获取foodData对应的value。调用router.getParams().foodData来获取到FoodCategoryList页面跳转来时携带的foodData对应的数据。

@Entry
@Component
struct FoodDetail {private foodItem: FoodData = router.getParams()['foodId']build() {......}
}

4. 重构FoodDetail页面的组件。在构建视图时,FoodDetail页面的食物信息都是直接声明的常量,现在要用传递来的FoodData数据来对其进行重新赋值。整体的FoodDetail.ets代码如下。

@Component
struct PageTitle {build() {Flex({ alignItems: ItemAlign.Start }) {Image($r('app.media.Back')).width(21.8).height(19.6)Text('Food Detail').fontSize(21.8).margin({left: 17.4})}.height(61).padding({ top: 13, bottom: 15, left: 28.3 }).onClick(() => {router.back()})}
}@Component
struct FoodImageDisplay {private foodItem: FoodDatabuild() {Stack({ alignContent: Alignment.BottomStart }) {Image(this.foodItem.image).objectFit(ImageFit.Contain)Text(this.foodItem.name).fontSize(26).fontWeight(500).margin({ left: 26, bottom: 17.4 })}.height(357)}
}@Component
struct ContentTable {private foodItem: FoodData@Builder IngredientItem(title:string, name: string, value: string) {Flex() {Text(title).fontSize(17.4).fontWeight(FontWeight.Bold).layoutWeight(1)Flex() {Text(name).fontSize(17.4).flexGrow(1)Text(value).fontSize(17.4)}.layoutWeight(2)}}build() {Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {this.IngredientItem('Calories', 'Calories', this.foodItem.calories + 'kcal')this.IngredientItem('Nutrition', 'Protein', this.foodItem.protein + 'g')this.IngredientItem('', 'Fat', this.foodItem.fat + 'g')this.IngredientItem('', 'Carbohydrates', this.foodItem.carbohydrates + 'g')this.IngredientItem('', 'VitaminC', this.foodItem.vitaminC + 'mg')}.height(280).padding({ top: 30, right: 30, left: 30 })}
}@Entry
@Component
struct FoodDetail {private foodItem: FoodData = router.getParams().foodDatabuild() {Column() {Stack( { alignContent: Alignment.TopStart }) {FoodImageDisplay({ foodItem: this.foodItem })PageTitle()}ContentTable({ foodItem: this.foodItem })}.alignItems(HorizontalAlign.Center)}
}

感谢各位大佬支持!!!

互三啦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/1510.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【React】新建React项目

目录 create-react-app基础运用React核心依赖React 核心思想&#xff1a;数据驱动React 采用 MVC体系package.jsonindex.html好书推荐 官方提供了快速构建React 项目的脚手架&#xff1a; create-react-app &#xff0c;目前使用它安装默认是19版本&#xff0c;我们这里降为18…

分多个AndroidManifest.xml来控制项目编译

使用场景 公司项目和我的项目的AndroidManifest.xml混在一起&#xff0c;我需要区分开来编译观察app运行 1.在app/src/main/ 下写多个AndroidManifest.xml AndroidManifest.own.xmlAndroidManifest.com.xml 2.编写powershell脚本 第一对脚本com-build.ps1和reset-com-mani…

linux进程

课本概念&#xff1a;程序的⼀个执行实例&#xff0c;正在执行的程序等内核观点&#xff1a;担当分配系统资源&#xff08;CPU时间&#xff0c;内存&#xff09;的实体。 进程信息被放在一个叫做进程控制块的数据结构中&#xff0c;可以理解为进程属性的集合.课本上称之为PCB&…

Hadoop•安装JDK

听说这里是目录哦 创建目录❤️‍&#x1f525;上传JDK安装包&#x1f497;查看JDK是否上传成功&#x1f498;安装JDK&#x1f496;配置JDK系统环境变量&#x1f493;验证JDK是否安装成功&#x1f49e;分发JDK安装目录&#x1f48c;分发系统环境变量文件&#x1f49d;若显示没有…

[Deep Learning] Anaconda+CUDA+CuDNN+Pytorch(GPU)环境配置-2025

文章目录 [Deep Learning] AnacondaCUDACuDNNPytorch(GPU)环境配置-20250. 引子1. 安装Anaconda1.1 安装包下载&#xff1a;1.2 启用安装包安装1.3 配置(系统)环境变量1.4 验证Anaconda是否安装完毕1.5 Anaconda换源 2. 安装CUDACuDNN2.1 判断本机的CUDA版本2.2 下载适合自己CU…

网络原理(四)—— 网络层、数据链路层 与 DNS

网络层 网络层这里重点介绍 IP 协议&#xff0c;首先先解析 IP 数据包&#xff1a; 先介绍第一行&#xff1a; 4位版本号是指使用了哪一个版本的 IP 协议&#xff0c;这里有 IPV4 和 IPV6 两种协议&#xff0c;现在主要使用的是 IPV4 这一个版本号&#xff0c; IPV6 在国内也…

Redis快速入门店铺营业状态设置

Redis简介 Redis是一种基于内存的键值对&#xff08;K-V&#xff09;数据库。 这意味着它与MySQL数据库类似&#xff0c;都能够用于存储数据&#xff0c;但两者又有着本质的区别。首先两者存储数据的结构不一样&#xff0c;Redis通过键&#xff08;key&#xff09;和值…

Node.js 如何实现文件夹内文件批量重命名

文章目录 一、引言二、Node.js 简介2.1 是什么2.2 优势 三、Node.js 批量重命名原理3.1 涉及的核心模块3.2 关键函数 四、实战步骤4.1 环境搭建4.2 代码实现4.3 代码解释 五、案例分析5.1 场景描述5.2 解决方案 六、可能遇到的问题与解决方法6.1 常见错误6.2 解决方案 七、总结…

MySQL(高级特性篇) 04 章——逻辑架构

一、逻辑架构剖析 &#xff08;1&#xff09;服务器处理客户端请求 那服务器进程对客户端进程发送的请求做了什么处理&#xff0c;才能产生最后的处理结果呢&#xff1f;这里以查询请求为例展示&#xff1a;下面具体展开看一下&#xff1a;Connectors是MySQL服务器之外的客户…

滚动字幕视频怎么制作

在当今的视频创作领域&#xff0c;滚动字幕被广泛应用于各种场景&#xff0c;为视频增添丰富的信息展示和独特的视觉效果。无论是影视剧中的片尾字幕、新闻节目中的资讯滚动&#xff0c;还是综艺节目中的人员与鸣谢信息展示&#xff0c;滚动字幕都发挥着不可或缺的作用。接下来…

源码编译安装httpd 2.4,提供系统服务管理脚本并测试(两种方法实现)

方法一&#xff1a;使用 systemd 服务文件 sudo yum install gcc make autoconf apr-devel apr-util-devel pcre-devel 1.下载源码 wget https://archive.apache.org/dist/httpd/httpd-2.4.46.tar.gz 2.解压源码 tar -xzf httpd-2.4.46.tar.gz 如果没有安装tar 记得先安装…

计算机视觉算法实战——步态识别(主页有源码)

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​ ​​​​​​​​​​​​​​​​​​ 1. 步态识别简介✨✨ 步态识别&#xff08;Gait Recognition&#xff09;是计算机视觉领域中的一个…

2025 年 UI 大屏设计新风向

在科技日新月异的 2025 年&#xff0c;UI 大屏设计领域正经历着深刻的变革。随着技术的不断进步和用户需求的日益多样化&#xff0c;新的设计风向逐渐显现。了解并掌握这些趋势&#xff0c;对于设计师打造出更具吸引力和实用性的 UI 大屏作品至关重要。 一、沉浸式体验设计 如…

Leetcode - 周赛431

目录 一&#xff0c;3411. 最长乘积等价子数组 二&#xff0c;3412. 计算字符串的镜像分数 三&#xff0c;3413. 收集连续 K 个袋子可以获得的最多硬币数量 四&#xff0c;3414. 不重叠区间的最大得分 一&#xff0c;3411. 最长乘积等价子数组 本题数据范围小&#xff0c;直…

深入Android架构(从线程到AIDL)_30 JNI架构原理_Java与C的对接03

目录 2.4 以C结构表达类(class)&#xff0c;并创建对象(object) 认识C函数指针 范例 2.5 在C函数里存取对象的属性(attribute) 范例 2.4 以C结构表达类(class)&#xff0c;并创建对象(object) 认识C函数指针 struct里不能定义函数本身&#xff0c;但能定义函数指针(func…

论文笔记(四十七)Diffusion policy: Visuomotor policy learning via action diffusion(下)

Diffusion policy: Visuomotor policy learning via action diffusion&#xff08;下&#xff09; 文章概括5. 评估5.1 模拟环境和数据集5.2 评估方法论5.3 关键发现5.4 消融研究 6 真实世界评估6.1 真实世界Push-T任务6.2 杯子翻转任务6.3 酱汁倒入和涂抹任务 7. 实际双臂任务…

EasyExcel - 行合并策略(二级列表)

&#x1f63c;前言&#xff1a;博主在工作中又遇到了新的excel导出挑战&#xff1a;需要导出多条文章及其下联合作者的信息&#xff0c;简单的来说是一个二级列表的数据结构。 &#x1f575;️‍♂️思路&#xff1a;excel导出实际上是一行一行的记录&#xff0c;再根据条件对其…

软件测试面试题整理

一、人格相关问题 1、自我介绍结构 姓名工作年限简单介绍上家公司的行业主要负责内容个人优势短期内的职业规划应聘该岗位的原因 2、对未来的发展方向怎么看&#xff1f; 没有标准答案&#xff0c;职业规划来讲&#xff0c;可以分为技术层面和管理层面去说&#xff0c;技术…

.NET framework、Core和Standard都是什么?

对于这些概念一直没有深入去理解&#xff0c;以至于经过.net这几年的发展进化&#xff0c;概念越来越多&#xff0c;越来越梳理不容易理解了。内心深处存在思想上的懒惰&#xff0c;以为自己专注于Unity开发就好&#xff0c;这些并不属于核心范畴&#xff0c;所以对这些概念总是…

CNN张量输入形状和特征图

CNN张量输入形状和特征图 这个是比较容易理解的张量的解释&#xff0c;比较直观 卷积神经网络 在这个神经网络编程系列中&#xff0c;我们正在逐步构建一个卷积神经网络&#xff08;CNN&#xff09;&#xff0c;所以让我们看看CNN的张量输入。 ​ ​ 在最后两篇文章中&…