暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

168.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局高级篇

原创 若城 2025-06-29
156

[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局高级篇

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

1. 引言

在前两篇教程中,我们分别介绍了HarmonyOS NEXT中可滚动网格布局的基础知识和进阶技巧。本篇教程将深入探讨可滚动网格布局的高级应用,包括复杂布局案例、高级交互技术、自定义网格布局算法等内容,帮助开发者掌握Grid组件的高级用法,构建出更加专业、精美的应用界面。

2. 复杂布局案例解析

2.1 应用商店首页布局分析

在我们的示例代码中,实现了一个类似应用商店的首页布局。下面我们对这个复杂布局进行深入分析:

build() { Column() { // 顶部搜索栏 this.SearchBar() // 主要内容区域 Scroll(this.scroller) { Column() { // 应用分类标签栏 this.CategoryTabs() // 推荐应用标题 this.FeaturedAppsTitle() // 推荐应用网格 Grid(this.scroller) { ForEach(this.featuredApps, (app: FeaturedApp) => { GridItem() { // 应用卡片内容 this.AppCard(app) } }) } .columnsTemplate('1fr') .rowsGap(16) .width('100%') .layoutWeight(1) .padding({ left: 16, right: 16 }) .backgroundColor('#F5F5F5') .onScrollIndex((first: number) => { console.info('first visible item index: ' + first) }) } .width('100%') } .scrollBar(BarState.Off) .scrollable(ScrollDirection.Vertical) // 底部导航栏 this.BottomNavBar() } .width('100%') .height('100%') .backgroundColor('#F5F5F5') }

这个布局由以下几个主要部分组成:

  1. 顶部搜索栏:固定在页面顶部,提供搜索功能
  2. 主要内容区域:使用Scroll组件实现垂直滚动
    • 应用分类标签栏:显示不同的应用分类
    • 推荐应用标题:显示推荐应用的标题
    • 推荐应用网格:使用Grid组件显示推荐应用列表
  3. 底部导航栏:固定在页面底部,提供导航功能

这种布局结构非常适合内容丰富的应用首页,既提供了良好的内容组织,又保证了良好的用户体验。

2.2 嵌套滚动结构

在我们的示例中,使用了嵌套的滚动结构:外层是Scroll组件,内层是Grid组件。这种结构有以下优点:

  1. 灵活的布局控制:外层Scroll负责整体页面的滚动,内层Grid负责网格内容的布局
  2. 统一的滚动体验:通过共享同一个scroller控制器,确保滚动行为的一致性
  3. 复杂内容的组织:可以在网格之外添加其他内容,如标题、标签栏等

实现这种嵌套滚动结构的关键是正确设置和共享scroller控制器:

// 创建滚动控制器 private scroller: Scroller = new Scroller() // 在Scroll和Grid中共享同一个控制器 Scroll(this.scroller) { // 内容 } Grid(this.scroller) { // 网格内容 }

通过这种方式,我们可以实现复杂的嵌套滚动结构,同时保持良好的滚动体验。

2.3 混合布局策略

在复杂应用中,我们通常需要混合使用多种布局组件。以下是一个更复杂的混合布局示例:

Scroll(this.scroller) { Column() { // 轮播图部分 Swiper() { ForEach(this.banners, (banner) => { Image(banner.image) .width('100%') .height(200) .borderRadius(16) }) } .width('100%') .height(200) .margin({ top: 16, bottom: 16 }) .autoPlay(true) // 应用分类标签栏 this.CategoryTabs() // 热门应用部分 - 水平滚动列表 Text('热门应用') .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ top: 16, bottom: 8 }) Scroll() { Row() { ForEach(this.popularApps, (app) => { this.PopularAppCard(app) }) } } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .width('100%') .height(150) // 推荐应用标题 this.FeaturedAppsTitle() // 推荐应用网格 Grid(this.scroller) { ForEach(this.featuredApps, (app: FeaturedApp) => { GridItem() { this.AppCard(app) } }) } .columnsTemplate('1fr') .rowsGap(16) .width('100%') .layoutWeight(1) } }

这个示例展示了如何在一个页面中混合使用多种布局组件:

  1. Swiper:用于显示轮播图
  2. 水平Scroll:用于显示热门应用的水平滚动列表
  3. Grid:用于显示推荐应用的网格布局

通过这种混合布局策略,我们可以创建出更加丰富、多样的用户界面。

3. 高级网格项设计

3.1 复杂网格项结构

在我们的示例中,每个应用卡片都是一个复杂的网格项,包含多种信息和交互元素:

@Builder AppCard(app: FeaturedApp) { Column() { // 应用图标和基本信息 Row() { Image(app.icon) .width(64) .height(64) .borderRadius(16) Column() { Text(app.name) .fontSize(16) .fontWeight(FontWeight.Bold) Text(app.developer) .fontSize(14) .opacity(0.6) // 星级评分 this.StarRating(app.rating) // 下载量和大小 Row() { Text(app.downloads) .fontSize(12) .opacity(0.6) Text(' • ') .fontSize(12) .opacity(0.6) Text(app.size) .fontSize(12) .opacity(0.6) } } .layoutWeight(1) .alignItems(HorizontalAlign.Start) .margin({ left: 16 }) // 获取/购买按钮 Button(app.price === 'Free' ? 'GET' : app.price) .height(32) .fontSize(14) .borderRadius(16) .backgroundColor(app.price === 'Free' ? '#0A59F7' : '#FFFFFF') .fontColor(app.price === 'Free' ? '#FFFFFF' : '#0A59F7') } .width('100%') .padding(16) // 应用截图 Scroll() { Row() { ForEach(app.screenshots, (screenshot) => { Image(screenshot) .width(240) .height(160) .borderRadius(8) .margin({ right: 8 }) }) } } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .width('100%') // 分类标签 Row() { ForEach(app.categories, (category) => { Text(category) .fontSize(12) .backgroundColor('#F0F0F0') .borderRadius(12) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .margin({ right: 8 }) }) } .width('100%') .padding({ left: 16, right: 16, bottom: 16 }) } .width('100%') .backgroundColor('#FFFFFF') .borderRadius(16) }

这个网格项包含以下元素:

  1. 应用图标和基本信息:包括图标、名称、开发者、评分、下载量和大小
  2. 获取/购买按钮:根据应用是免费还是付费显示不同的按钮
  3. 应用截图:使用水平滚动的Scroll组件显示多张应用截图
  4. 分类标签:显示应用所属的分类

这种复杂的网格项设计可以在有限的空间内展示丰富的信息,提升用户体验。

3.2 自定义构建器的高级应用

在我们的示例中,使用了自定义构建器来创建可复用的UI组件,如星级评分:

@Builder StarRating(rating: number) { Row() { ForEach([1, 2, 3, 4, 5], (star) => { Image(star <= rating ? '/images/star_filled.png' : '/images/star_empty.png') .width(16) .height(16) .margin({ right: 4 }) }) Text(rating.toFixed(1)) .fontSize(14) .opacity(0.6) .margin({ left: 4 }) } .margin({ top: 4, bottom: 4 }) }

自定义构建器的高级应用包括:

  1. 参数化构建器:通过传递参数来控制构建器的输出
  2. 条件渲染:根据条件显示不同的UI元素
  3. 组合构建器:在一个构建器中使用其他构建器

以下是一个更复杂的自定义构建器示例:

@Builder AppCardVariant(app: FeaturedApp, variant: string = 'default', onAction?: (app: FeaturedApp, action: string) => void) { // 根据变体类型显示不同的卡片样式 if (variant === 'compact') { // 紧凑型卡片 Row() { Image(app.icon) .width(48) .height(48) .borderRadius(12) Column() { Text(app.name) .fontSize(14) .fontWeight(FontWeight.Bold) this.StarRating(app.rating) } .layoutWeight(1) .margin({ left: 12 }) Button('GET') .height(28) .fontSize(12) .borderRadius(14) .onClick(() => { onAction?.(app, 'download') }) } .width('100%') .padding(12) .backgroundColor('#FFFFFF') .borderRadius(12) } else if (variant === 'featured') { // 特色卡片 Column() { Image(app.screenshots[0]) .width('100%') .height(200) .borderRadius({ topLeft: 16, topRight: 16 }) Row() { Image(app.icon) .width(56) .height(56) .borderRadius(12) .margin({ top: -28 }) Column() { Text(app.name) .fontSize(16) .fontWeight(FontWeight.Bold) Text(app.developer) .fontSize(14) .opacity(0.6) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) .margin({ left: 12 }) Button('GET') .height(32) .fontSize(14) .borderRadius(16) .onClick(() => { onAction?.(app, 'download') }) } .width('100%') .padding(16) this.StarRating(app.rating) .margin({ left: 16, bottom: 16 }) } .width('100%') .backgroundColor('#FFFFFF') .borderRadius(16) } else { // 默认卡片 this.AppCard(app) } }

这个高级构建器根据传入的变体类型显示不同样式的应用卡片,并支持自定义操作回调,展示了自定义构建器的强大功能。

3.3 动态内容与条件渲染

在复杂的网格项中,我们通常需要根据数据动态渲染内容。以下是一些高级的动态内容和条件渲染技巧:

@Builder AppCard(app: FeaturedApp) { Column() { // 基本信息 // ... // 条件渲染:只有当有截图时才显示截图部分 if (app.screenshots && app.screenshots.length > 0) { Scroll() { Row() { ForEach(app.screenshots, (screenshot) => { Image(screenshot) .width(240) .height(160) .borderRadius(8) .margin({ right: 8 }) }) } } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .width('100%') } // 动态内容:根据应用类型显示不同的标签 Row() { if (app.isEditor) { Text('编辑推荐') .fontSize(12) .fontColor('#FF6B22') .backgroundColor('#FFF0E6') .borderRadius(12) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .margin({ right: 8 }) } if (app.isNew) { Text('新上架') .fontSize(12) .fontColor('#14B37D') .backgroundColor('#E6F9F1') .borderRadius(12) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .margin({ right: 8 }) } // 分类标签 ForEach(app.categories, (category) => { Text(category) .fontSize(12) .backgroundColor('#F0F0F0') .borderRadius(12) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .margin({ right: 8 }) }) } .width('100%') .padding({ left: 16, right: 16, bottom: 16 }) // 条件渲染:只有付费应用才显示应用内购买信息 if (app.price !== 'Free' && app.inAppPurchases) { Text('包含应用内购买项目') .fontSize(12) .opacity(0.6) .width('100%') .textAlign(TextAlign.Center) .padding({ bottom: 16 }) } } }

这个示例展示了如何根据应用数据动态渲染不同的内容,包括:

  1. 条件显示截图:只有当应用有截图时才显示截图部分
  2. 动态标签:根据应用属性显示不同的标签,如"编辑推荐"、"新上架"等
  3. 条件显示购买信息:只有付费应用才显示应用内购买信息

通过这些技巧,我们可以创建出更加智能、动态的网格项,提升用户体验。

4. 高级交互与动画

4.1 滚动驱动动画

滚动驱动动画是一种高级的交互效果,可以根据滚动位置动态调整UI元素的属性。以下是一个实现滚动驱动动画的示例:

// 创建滚动控制器 private scroller: Scroller = new Scroller() // 滚动位置状态 @State scrollY: number = 0 // 监听滚动事件 onPageShow() { this.scroller.scrollTo({ xOffset: 0, yOffset: 0 }) // 添加滚动监听 this.scroller.onScroll((offset: { xOffset: number, yOffset: number }) => { this.scrollY = offset.yOffset }) } build() { Stack() { // 主要内容 Scroll(this.scroller) { // 内容 } // 顶部搜索栏 - 滚动驱动动画 Row() { // 搜索栏内容 } .width('100%') .height(56) .padding({ left: 16, right: 16 }) .backgroundColor(Color.lerp(new Color('#FFFFFF00'), new Color('#FFFFFFFF'), Math.min(this.scrollY / 100, 1))) .shadow({ radius: 8, color: Color.lerp(new Color('#00000000'), new Color('#00000022'), Math.min(this.scrollY / 100, 1)), offsetY: 2 }) } }

这个示例实现了一个滚动时渐变的顶部搜索栏:

  1. 当页面滚动到顶部时,搜索栏是透明的
  2. 随着页面向下滚动,搜索栏逐渐变为白色背景并显示阴影
  3. 使用Color.lerp函数根据滚动位置计算颜色和阴影的插值

这种滚动驱动动画可以创造出更加流畅、自然的交互体验。

4.2 视差滚动效果

视差滚动是一种高级的滚动效果,可以使不同层次的元素以不同的速度滚动,创造出深度感。以下是一个实现视差滚动的示例:

@State scrollY: number = 0 build() { Stack() { // 背景层 - 慢速滚动 Image('/images/background.png') .width('100%') .height('120%') .objectFit(ImageFit.Cover) .translate({ y: -this.scrollY * 0.3 }) // 慢速滚动 // 中间层 - 中速滚动 Image('/images/middle_layer.png') .width('100%') .height('110%') .objectFit(ImageFit.Cover) .translate({ y: -this.scrollY * 0.6 }) // 中速滚动 // 前景层 - 正常滚动 Scroll(this.scroller) { Column() { // 内容 } .height(2000) // 确保有足够的滚动空间 } .scrollBar(BarState.Off) .onScroll((offset: { xOffset: number, yOffset: number }) => { this.scrollY = offset.yOffset }) } }

这个示例创建了三层视差滚动效果:

  1. 背景层:以0.3倍的速度滚动,创造出远处的效果
  2. 中间层:以0.6倍的速度滚动,创造出中等距离的效果
  3. 前景层:以正常速度滚动,包含主要内容

通过这种视差滚动效果,可以为应用添加深度感和立体感,提升视觉体验。

4.3 高级手势交互

结合Grid和手势,我们可以实现更加复杂的交互效果。以下是一个实现网格项缩放和旋转效果的示例:

@State gestureX: number = 0 @State gestureY: number = 0 @State scale: number = 1.0 @State rotation: number = 0 GridItem() { Column() { // 网格项内容 } .scale({ x: this.scale, y: this.scale }) .rotate({ angle: this.rotation }) .translate({ x: this.gestureX, y: this.gestureY }) .gesture( GestureGroup(GestureMode.Parallel, PinchGesture() .onActionUpdate((event: GestureEvent) => { this.scale = Math.max(0.5, Math.min(2.0, this.scale * event.scale)) }) .onActionEnd(() => { // 缩放结束后恢复正常大小 animateTo({ duration: 300, curve: Curve.Ease }, () => { this.scale = 1.0 }) }), RotationGesture() .onActionUpdate((event: GestureEvent) => { this.rotation += event.angle }) .onActionEnd(() => { // 旋转结束后恢复正常角度 animateTo({ duration: 300, curve: Curve.Ease }, () => { this.rotation = 0 }) }), PanGesture() .onActionUpdate((event: GestureEvent) => { this.gestureX += event.offsetX this.gestureY += event.offsetY }) .onActionEnd(() => { // 拖拽结束后恢复正常位置 animateTo({ duration: 300, curve: Curve.Ease }, () => { this.gestureX = 0 this.gestureY = 0 }) }) ) ) }

这个示例实现了三种手势交互:

  1. 捏合手势:用于缩放网格项
  2. 旋转手势:用于旋转网格项
  3. 拖拽手势:用于移动网格项

所有手势结束后,网格项会通过动画恢复到正常状态。这种高级手势交互可以为应用添加更加丰富、自然的交互体验。

5. 高级布局算法与自定义

5.1 自定义网格布局算法

在某些特殊场景下,Grid组件的默认布局可能无法满足需求。这时,我们可以实现自定义的网格布局算法:

// 自定义瀑布流布局 class WaterfallLayout { private columnHeights: number[] = [] private columnCount: number = 2 private itemPositions: Map<number, { x: number, y: number }> = new Map() constructor(columnCount: number = 2) { this.columnCount = columnCount this.resetLayout() } resetLayout() { this.columnHeights = new Array(this.columnCount).fill(0) this.itemPositions.clear() } // 计算项目位置 calculateItemPosition(itemId: number, itemHeight: number) { // 找出高度最小的列 let minColumnIndex = 0 let minHeight = this.columnHeights[0] for (let i = 1; i < this.columnCount; i++) { if (this.columnHeights[i] < minHeight) { minHeight = this.columnHeights[i] minColumnIndex = i } } // 计算位置 const columnWidth = 100 / this.columnCount const x = minColumnIndex * columnWidth const y = this.columnHeights[minColumnIndex] // 更新列高度 this.columnHeights[minColumnIndex] += itemHeight + 16 // 16是间距 // 保存位置 this.itemPositions.set(itemId, { x, y }) return { x: `${x}%`, y: y } } getItemPosition(itemId: number) { return this.itemPositions.get(itemId) } getContentHeight() { return Math.max(...this.columnHeights) } } // 使用自定义布局 @State waterfallLayout: WaterfallLayout = new WaterfallLayout(2) @State itemHeights: Map<number, number> = new Map() aboutToAppear() { // 预设项目高度(实际应用中可能需要动态计算) this.featuredApps.forEach((app, index) => { // 模拟不同高度的项目 const height = 200 + Math.random() * 200 this.itemHeights.set(app.id, height) }) // 计算初始布局 this.waterfallLayout.resetLayout() this.featuredApps.forEach(app => { this.waterfallLayout.calculateItemPosition(app.id, this.itemHeights.get(app.id) || 200) }) } build() { Column() { // 其他内容 // 自定义瀑布流布局 Stack() { ForEach(this.featuredApps, (app: FeaturedApp) => { Column() { // 应用卡片内容 this.AppCard(app) } .width(`${100 / this.waterfallLayout.columnCount}%`) .height(this.itemHeights.get(app.id) || 200) .position({ x: this.waterfallLayout.getItemPosition(app.id)?.x || '0%', y: this.waterfallLayout.getItemPosition(app.id)?.y || 0 }) }) } .width('100%') .height(this.waterfallLayout.getContentHeight()) .margin({ top: 16 }) } }

这个示例实现了一个自定义的瀑布流布局算法:

  1. WaterfallLayout类:负责计算每个项目的位置
    • 跟踪每列的当前高度
    • 为新项目选择高度最小的列
    • 计算并存储每个项目的位置
  2. 布局应用
    • 预设每个项目的高度(实际应用中可能需要动态计算)
    • 使用Stack组件和绝对定位实现自定义布局
    • 根据计算的位置放置每个项目

通过这种方式,我们可以实现Grid组件无法直接支持的复杂布局,如瀑布流布局。

5.2 动态网格布局

在某些场景下,我们需要根据内容动态调整网格布局。以下是一个实现动态网格布局的示例:

@State gridItems: Array<{ id: number, content: string, rowSpan: number, columnSpan: number }> = [] aboutToAppear() { // 生成动态网格项 this.generateGridItems() } generateGridItems() { const items = [] let id = 0 // 添加一个大项目(跨2行2列) items.push({ id: id++, content: '特色内容', rowSpan: 2, columnSpan: 2 }) // 添加4个普通项目 for (let i = 0; i < 4; i++) { items.push({ id: id++, content: `内容 ${i + 1}`, rowSpan: 1, columnSpan: 1 }) } // 添加一个宽项目(跨2列) items.push({ id: id++, content: '宽内容', rowSpan: 1, columnSpan: 2 }) // 添加更多普通项目 for (let i = 0; i < 6; i++) { items.push({ id: id++, content: `内容 ${i + 5}`, rowSpan: 1, columnSpan: 1 }) } this.gridItems = items } build() { Column() { // 动态网格布局 Grid() { ForEach(this.gridItems, (item) => { GridItem() { Text(item.content) .width('100%') .height('100%') .textAlign(TextAlign.Center) .backgroundColor('#F0F0F0') .borderRadius(8) } .rowStart(0) // 自动布局 .rowEnd(0) .columnStart(0) .columnEnd(0) // 设置跨行跨列 .forceRebuild(true) .gridSpan({ rowSpan: item.rowSpan, columnSpan: item.columnSpan }) }) } .columnsTemplate('1fr 1fr') .columnsGap(16) .rowsGap(16) .width('100%') .height(600) } }

这个示例实现了一个动态网格布局:

  1. 动态生成网格项:包括不同大小的项目(普通项目、大项目、宽项目)
  2. 设置跨行跨列:使用gridSpan属性设置每个项目的跨行跨列
  3. 自动布局:将rowStart、rowEnd、columnStart、columnEnd设置为0,让Grid自动布局

通过这种方式,我们可以创建出更加灵活、动态的网格布局。

6. 总结

本教程深入探讨了HarmonyOS NEXT中可滚动网格布局的高级应用,包括复杂布局案例解析、高级网格项设计、高级交互与动画、高级布局算法与自定义等内容。通过这些高级技巧,开发者可以构建出更加专业、精美、交互丰富的应用界面。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论