OpenHarmony应用开发-构建完整实例
素年锦时静待君丶
发布于 2023-4-12 17:56
浏览
0收藏
版本:v3.2 Beta5
构建食物分类Grid布局
健康饮食应用在主页提供给用户两种食物显示方式:列表显示和网格显示。开发者将实现通过页签切换不同食物分类的网格布局。
- 将Category枚举类型引入FoodCategoryList页面。
import { Category, FoodData } from '../model/FoodData'
- 创建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() {
......
}
}
- 在FoodCategoryList组件内创建showList成员变量,用于控制List布局和Grid布局的渲染切换。需要用到条件渲染语句if…else…。
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
private showList: boolean = false
build() {
Stack() {
if (this.showList) {
FoodList({ foodItems: this.foodItems })
} else {
FoodCategory({ foodItems: this.foodItems })
}
}
}
}
- 在页面右上角创建切换List/Grid布局的图标。设置Stack对齐方式为顶部尾部对齐TopEnd,创建Image组件,设置其点击事件,即showList取反。
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
private showList: boolean = false
build() {
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%')
}
}
- 添加@State装饰器。点击右上角的switch标签后,页面没有任何变化,这是因为showList不是有状态数据,它的改变不会触发页面的刷新。需要为其添加@State装饰器,使其成为状态数据,它的改变会引起其所在组件的重新渲染。
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
@State private showList: boolean = false
build() {
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组件消失。
- 创建显示所有食物的页签(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)
}
}
}
- 创建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)
}
}
}
- 实现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})
}
}
- 创建FoodGridItem组件,展示食物图片、名称和卡路里,实现其UI布局,为GridItem的子组件。每个FoodGridItem高度为184,行间距为8,设置Grid总高度为(184 + 8) * 6 - 8 = 1144。
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
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 })
}
}
- 创建展示蔬菜(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)
}
}
}
- 设置不同食物分类的Grid的行数和高度。因为不同分类的食物数量不同,所以不能用’1fr 1fr 1fr 1fr 1fr 1fr '常量来统一设置成6行。 创建gridRowTemplate和HeightValue成员变量,通过成员变量设置Grid行数和高度。
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate: string = ''
private heightValue: number
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 })
}
}
调用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: number
aboutToAppear() {
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 })
}
}
页面跳转与数据传递
本节将学习页面跳转和数据传递,实现:
- 页面跳转:点击食物分类列表页面的食物条目后,跳转到食物详情页;点击食物详情页的返回按钮,返回到食物列表页。
- 页面间数据传递:点击不同的食物条目后,FoodDetail接受前一个页面的数据,渲染对应的食物详情页。
页面跳转
声明式UI范式提供了两种机制来实现页面间的跳转:
- 路由容器组件Navigator,包装了页面路由的能力,指定页面target后,使其包裹的子组件都具有路由能力。
- 路由RouterAPI接口,通过在页面上引入router,可以调用router的各种接口,从而实现页面路由的各种操作。
下面我们就分别学习这两种跳转机制来实现食物分类列表页面和食物详情页的链接。
- 点击FoodListItem后跳转到FoodDetail页面。在FoodListItem内创建Navigator组件,使其子组件都具有路由功能,目标页面target为’pages/FoodDetail’。
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.backgroundColor('#FFf1f3f5')
.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 })
}
}
- 点击FoodGridItem后跳转到FoodDetail页面。调用页面路由router模块的push接口,将FoodDetail页面推到路由栈中,实现页面跳转。使用router路由API接口,需要先引入router。
import router from '@ohos.router'
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
Column() {
......
}
.height(184)
.width('100%')
.onClick(() => {
router.pushUrl({ url: 'pages/FoodDetail' })
})
}
}
- 在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)
.backgroundColor('#FFedf2f5')
.padding({ top: 13, bottom: 15, left: 28.3 })
.onClick(() => {
router.back()
})
}
}
- 在FoodDetail组件内创建Stack组件,包含子组件FoodImageDisplay和PageTitle子组件,设置其对齐方式为左上对齐TopStart。
@Entry
@Component
struct FoodDetail {
build() {
Column() {
Stack( { alignContent: Alignment.TopStart }) {
FoodImageDisplay()
PageTitle()
}
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
页面间数据传递
我们已经完成了FoodCategoryList页面和FoodDetail页面的跳转和回退,但是点击不同的FoodListItem/FoodGridItem,跳转的FoodDetail页面都是西红柿Tomato的详细介绍,这是因为没有构建起两个页面的数据传递,需要用到携带参数(parameter)路由。
- 在FoodListItem组件的Navigator设置其params属性,params属性接受key-value的Object。
// FoodList.ets
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
......
}
.params({ foodData: this.foodItem })
}
}
FoodGridItem调用的routerAPI同样有携带参数跳转的能力,使用方法和Navigator类似。
router.pushUrl({
url: 'pages/FoodDetail',
params: { foodData: this.foodItem }
})
- FoodDetail页面引入FoodData类,在FoodDetail组件内添加foodItem成员变量。
// FoodDetail.ets
import { FoodData } from '../model/FoodData'
@Entry
@Component
struct FoodDetail {
private foodItem: FoodData
build() {
......
}
}
- 获取foodData对应的value。调用router.getParams()[‘foodData’]来获取到FoodCategoryList页面跳转来时携带的foodData对应的数据。
@Entry
@Component
struct FoodDetail {
private foodItem: FoodData = router.getParams()['foodData']
build() {
......
}
}
- 重构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)
.backgroundColor('#FFedf2f5')
.padding({ top: 13, bottom: 15, left: 28.3 })
.onClick(() => {
router.back()
})
}
}
@Component
struct FoodImageDisplay {
private foodItem: FoodData
build() {
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)
.backgroundColor('#FFedf2f5')
}
}
@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()['foodData']
build() {
Column() {
Stack( { alignContent: Alignment.TopStart }) {
FoodImageDisplay({ foodItem: this.foodItem })
PageTitle()
}
ContentTable({ foodItem: this.foodItem })
}
.alignItems(HorizontalAlign.Center)
}
}
相关实例
针对页面布局与连接,有以下示例工程可供参考:
- DefiningPageLayoutAndConnection:页面布局和连接(ArkTS)(API8)本示例构建了食物分类列表页面和食物详情页,向开发者展示了List布局、Grid布局以及页面路由的基本用法。
标签
已于2023-4-12 17:56:54修改
赞
收藏
回复
回复
相关推荐