1# 构建食物分类Grid布局 2 3健康饮食应用在主页提供给用户两种食物显示方式:列表显示和网格显示。开发者将实现通过页签切换不同食物分类的网格布局。 4 5 61. 将Category枚举类型引入FoodCategoryList页面。 7 8 ```ts 9 import { Category, FoodData } from '../model/FoodData' 10 ``` 11 122. 创建FoodCategoryList和FoodCategory组件,其中FoodCategoryList作为新的页面入口组件,在入口组件调用initializeOnStartup方法。 13 14 ```ts 15 @Component 16 struct FoodList { 17 private foodItems: FoodData[] 18 build() { 19 ...... 20 } 21 } 22 23 @Component 24 struct FoodCategory { 25 private foodItems: FoodData[] 26 build() { 27 ...... 28 } 29 } 30 31 @Entry 32 @Component 33 struct FoodCategoryList { 34 private foodItems: FoodData[] = initializeOnStartup() 35 build() { 36 ...... 37 } 38 } 39 ``` 40 413. 在FoodCategoryList组件内创建showList成员变量,用于控制List布局和Grid布局的渲染切换。需要用到条件渲染语句if...else...。 42 43 ```ts 44 @Entry 45 @Component 46 struct FoodCategoryList { 47 private foodItems: FoodData[] = initializeOnStartup() 48 private showList: boolean = false 49 50 build() { 51 Stack() { 52 if (this.showList) { 53 FoodList({ foodItems: this.foodItems }) 54 } else { 55 FoodCategory({ foodItems: this.foodItems }) 56 } 57 } 58 } 59 } 60 ``` 61 624. 在页面右上角创建切换List/Grid布局的图标。设置Stack对齐方式为顶部尾部对齐TopEnd,创建Image组件,设置其点击事件,即showList取反。 63 64 ```ts 65 @Entry 66 @Component 67 struct FoodCategoryList { 68 private foodItems: FoodData[] = initializeOnStartup() 69 private showList: boolean = false 70 71 build() { 72 Stack({ alignContent: Alignment.TopEnd }) { 73 if (this.showList) { 74 FoodList({ foodItems: this.foodItems }) 75 } else { 76 FoodCategory({ foodItems: this.foodItems }) 77 } 78 Image($r('app.media.Switch')) 79 .height(24) 80 .width(24) 81 .margin({ top: 15, right: 10 }) 82 .onClick(() => { 83 this.showList = !this.showList 84 }) 85 }.height('100%') 86 } 87 } 88 ``` 89 905. 添加@State装饰器。点击右上角的switch标签后,页面没有任何变化,这是因为showList不是有状态数据,它的改变不会触发页面的刷新。需要为其添加\@State装饰器,使其成为状态数据,它的改变会引起其所在组件的重新渲染。 91 92 ```ts 93 @Entry 94 @Component 95 struct FoodCategoryList { 96 private foodItems: FoodData[] = initializeOnStartup() 97 @State private showList: boolean = false 98 99 build() { 100 Stack({ alignContent: Alignment.TopEnd }) { 101 if (this.showList) { 102 FoodList({ foodItems: this.foodItems }) 103 } else { 104 FoodCategory({ foodItems: this.foodItems }) 105 } 106 Image($r('app.media.Switch')) 107 .height(24) 108 .width(24) 109 .margin({ top: 15, right: 10 }) 110 .onClick(() => { 111 this.showList = !this.showList 112 }) 113 }.height('100%') 114 } 115 } 116 117 ``` 118 119 点击切换图标,FoodList组件出现,再次点击,FoodList组件消失。 120 121  122 1236. 创建显示所有食物的页签(All)。在FoodCategory组件内创建Tabs组件和其子组件TabContent,设置tabBar为All。设置TabBars的宽度为280,布局模式为Scrollable,即超过总长度后可以滑动。Tabs是一种可以通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图TabContent。 124 125 ```ts 126 @Component 127 struct FoodCategory { 128 private foodItems: FoodData[] 129 build() { 130 Stack() { 131 Tabs() { 132 TabContent() {}.tabBar('All') 133 } 134 .barWidth(280) 135 .barMode(BarMode.Scrollable) 136 } 137 } 138 } 139 ``` 140 141  142 1437. 创建FoodGrid组件,作为TabContent的子组件。 144 145 ```ts 146 @Component 147 struct FoodGrid { 148 private foodItems: FoodData[] 149 build() {} 150 } 151 152 @Component 153 struct FoodCategory { 154 private foodItems: FoodData[] 155 build() { 156 Stack() { 157 Tabs() { 158 TabContent() { 159 FoodGrid({ foodItems: this.foodItems }) 160 }.tabBar('All') 161 } 162 .barWidth(280) 163 .barMode(BarMode.Scrollable) 164 } 165 } 166 } 167 ``` 168 1698. 实现2 \* 6的网格布局(一共12个食物数据资源)。创建Grid组件,设置列数columnsTemplate('1fr 1fr'),行数rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr'),行间距和列间距rowsGap和columnsGap为8。创建Scroll组件,使其可以滑动。 170 171 ```ts 172 @Component 173 struct FoodGrid { 174 private foodItems: FoodData[] 175 build() { 176 Scroll() { 177 Grid() { 178 ForEach(this.foodItems, (item: FoodData) => { 179 GridItem() {} 180 }, (item: FoodData) => item.id.toString()) 181 } 182 .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr') 183 .columnsTemplate('1fr 1fr') 184 .columnsGap(8) 185 .rowsGap(8) 186 } 187 .scrollBar(BarState.Off) 188 .padding({left: 16, right: 16}) 189 } 190 } 191 ``` 192 1939. 创建FoodGridItem组件,展示食物图片、名称和卡路里,实现其UI布局,为GridItem的子组件。每个FoodGridItem高度为184,行间距为8,设置Grid总高度为(184 + 8) \* 6 - 8 = 1144。 194 195 ```ts 196 @Component 197 struct FoodGridItem { 198 private foodItem: FoodData 199 build() { 200 Column() { 201 Row() { 202 Image(this.foodItem.image) 203 .objectFit(ImageFit.Contain) 204 .height(152) 205 .width('100%') 206 } 207 Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 208 Text(this.foodItem.name) 209 .fontSize(14) 210 .flexGrow(1) 211 .padding({ left: 8 }) 212 Text(this.foodItem.calories + 'kcal') 213 .fontSize(14) 214 .margin({ right: 6 }) 215 } 216 .height(32) 217 .width('100%') 218 .backgroundColor('#FFe5e5e5') 219 } 220 .height(184) 221 .width('100%') 222 } 223 } 224 225 @Component 226 struct FoodGrid { 227 private foodItems: FoodData[] 228 build() { 229 Scroll() { 230 Grid() { 231 ForEach(this.foodItems, (item: FoodData) => { 232 GridItem() { 233 FoodGridItem({foodItem: item}) 234 } 235 }, (item: FoodData) => item.id.toString()) 236 } 237 .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr') 238 .columnsTemplate('1fr 1fr') 239 .columnsGap(8) 240 .rowsGap(8) 241 .height(1144) 242 } 243 .scrollBar(BarState.Off) 244 .padding({ left: 16, right: 16 }) 245 } 246 } 247 ``` 248 249  250 25110. 创建展示蔬菜(Category.Vegetable)、水果(Category.Fruit)、坚果(Category.Nut)、海鲜(Category.SeaFood)和甜品(Category.Dessert)分类的页签。 252 253 ```ts 254 @Component 255 struct FoodCategory { 256 private foodItems: FoodData[] 257 258 build() { 259 Stack() { 260 Tabs() { 261 TabContent() { 262 FoodGrid({ foodItems: this.foodItems }) 263 }.tabBar('All') 264 265 TabContent() { 266 FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Vegetable)) }) 267 }.tabBar('Vegetable') 268 269 TabContent() { 270 FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Fruit)) }) 271 }.tabBar('Fruit') 272 273 TabContent() { 274 FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Nut)) }) 275 }.tabBar('Nut') 276 277 TabContent() { 278 FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Seafood)) }) 279 }.tabBar('Seafood') 280 281 TabContent() { 282 FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Dessert)) }) 283 }.tabBar('Dessert') 284 } 285 .barWidth(280) 286 .barMode(BarMode.Scrollable) 287 } 288 } 289 } 290 ``` 291 29211. 设置不同食物分类的Grid的行数和高度。因为不同分类的食物数量不同,所以不能用'1fr 1fr 1fr 1fr 1fr 1fr '常量来统一设置成6行。 293 创建gridRowTemplate和HeightValue成员变量,通过成员变量设置Grid行数和高度。 294 295 ```ts 296 @Component 297 struct FoodGrid { 298 private foodItems: FoodData[] 299 private gridRowTemplate: string = '' 300 private heightValue: number 301 302 build() { 303 Scroll() { 304 Grid() { 305 ForEach(this.foodItems, (item: FoodData) => { 306 GridItem() { 307 FoodGridItem({ foodItem: item }) 308 } 309 }, (item: FoodData) => item.id.toString()) 310 } 311 .rowsTemplate(this.gridRowTemplate) 312 .columnsTemplate('1fr 1fr') 313 .columnsGap(8) 314 .rowsGap(8) 315 .height(this.heightValue) 316 } 317 .scrollBar(BarState.Off) 318 .padding({ left: 16, right: 16 }) 319 } 320 } 321 ``` 322 323 调用aboutToAppear接口计算行数(gridRowTemplate )和高度(heightValue)。 324 325 ```ts 326 aboutToAppear() { 327 var rows = Math.round(this.foodItems.length / 2); 328 this.gridRowTemplate = '1fr '.repeat(rows); 329 this.heightValue = rows * 192 - 8; 330 } 331 ``` 332 333 自定义组件提供了两个生命周期的回调接口aboutToAppear和aboutToDisappear。aboutToAppear的执行时机在创建自定义组件后,执行自定义组件build方法之前。aboutToDisappear在自定义组件销毁之前的时机执行。 334 335  336 337 ```ts 338 @Component 339 struct FoodGrid { 340 private foodItems: FoodData[] 341 private gridRowTemplate: string = '' 342 private heightValue: number 343 344 aboutToAppear() { 345 var rows = Math.round(this.foodItems.length / 2); 346 this.gridRowTemplate = '1fr '.repeat(rows); 347 this.heightValue = rows * 192 - 8; 348 } 349 350 build() { 351 Scroll() { 352 Grid() { 353 ForEach(this.foodItems, (item: FoodData) => { 354 GridItem() { 355 FoodGridItem({ foodItem: item }) 356 } 357 }, (item: FoodData) => item.id.toString()) 358 } 359 .rowsTemplate(this.gridRowTemplate) 360 .columnsTemplate('1fr 1fr') 361 .columnsGap(8) 362 .rowsGap(8) 363 .height(this.heightValue) 364 } 365 .scrollBar(BarState.Off) 366 .padding({ left: 16, right: 16 }) 367 } 368 } 369 ``` 370 371 