174.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 高级篇
2025-06-30 22:57:35
104次阅读
0个评论
[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 高级篇
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
在前两篇文章中,我们介绍了HarmonyOS NEXT响应式网格布局的基础知识和进阶技巧。本篇将深入探讨响应式网格布局的高级应用,包括复杂布局案例、高级组件封装、性能优化策略以及实际项目中的最佳实践,帮助开发者掌握响应式网格布局的精髓。
1. 复杂布局案例
1.1 新闻门户首页布局
新闻门户首页通常包含多种内容区块,如头条新闻、专题报道、分类新闻等。下面是一个复杂新闻首页的布局实现:
@Component
struct NewsHomePage {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
build() {
Column() {
// 顶部导航栏
this.NavigationBar()
// 内容区域
Grid() {
// 头条新闻区域
GridItem() {
this.HeadlineNews()
}.area('headline')
// 专题报道区域
GridItem() {
this.FeaturedNews()
}.area('featured')
// 最新新闻区域
GridItem() {
this.LatestNews()
}.area('latest')
// 热门新闻区域
GridItem() {
this.HotNews()
}.area('hot')
// 分类新闻区域
GridItem() {
this.CategoryNews()
}.area('category')
}
.width('100%')
.layoutWeight(1)
.columnsTemplate(this.getColumnsTemplate())
.rowsTemplate(this.getRowsTemplate())
.areas(this.getGridAreas())
.backgroundColor('#F8F8F8')
}
.width('100%')
.height('100%')
}
// 根据断点获取列模板
getColumnsTemplate(): string {
switch (this.currentBreakpoint) {
case 'sm':
return '1fr' // 小屏单列
case 'md':
return '2fr 1fr' // 中屏两列,左侧占比大
case 'lg':
return '2fr 1fr 1fr' // 大屏三列
default:
return '1fr'
}
}
// 根据断点获取行模板
getRowsTemplate(): string {
switch (this.currentBreakpoint) {
case 'sm':
return 'auto auto auto auto auto' // 小屏五行
case 'md':
return 'auto auto auto' // 中屏三行
case 'lg':
return 'auto auto' // 大屏两行
default:
return 'auto'
}
}
// 根据断点获取网格区域定义
getGridAreas() {
switch (this.currentBreakpoint) {
case 'sm':
return [
['headline'],
['featured'],
['latest'],
['hot'],
['category']
]
case 'md':
return [
['headline', 'featured'],
['latest', 'hot'],
['category', 'category']
]
case 'lg':
return [
['headline', 'featured', 'hot'],
['latest', 'category', 'category']
]
default:
return [['headline']]
}
}
// 组件定义...
@Builder NavigationBar() {
// 导航栏实现
}
@Builder HeadlineNews() {
// 头条新闻实现
}
@Builder FeaturedNews() {
// 专题报道实现
}
@Builder LatestNews() {
// 最新新闻实现
}
@Builder HotNews() {
// 热门新闻实现
}
@Builder CategoryNews() {
// 分类新闻实现
}
}
1.2 电商产品展示布局
电商应用中的产品展示页面需要根据不同设备显示不同数量的产品:
@Component
struct ProductGrid {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
@State products: Product[] = []
build() {
Grid() {
ForEach(this.products, (product: Product) => {
GridItem() {
Column() {
// 产品图片
Image(product.imageUrl)
.width('100%')
.aspectRatio(1)
.objectFit(ImageFit.Cover)
.borderRadius(8)
// 产品信息
Column() {
// 产品名称
Text(product.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 价格信息
Row() {
Text(`¥${product.price.toFixed(2)}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6B6B')
if (product.originalPrice > product.price) {
Text(`¥${product.originalPrice.toFixed(2)}`)
.fontSize(14)
.fontColor('#999999')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 8 })
}
}
.width('100%')
.margin({ top: 8 })
// 销量和评分
if (this.currentBreakpoint !== 'sm') {
Row() {
Text(`销量 ${product.sales}`)
.fontSize(12)
.fontColor('#999999')
Text(`评分 ${product.rating.toFixed(1)}`)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 8 })
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding({ top: 8 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(12)
}
})
}
.width('100%')
.columnsTemplate(this.getColumnsTemplate())
.rowsGap(16)
.columnsGap(16)
.padding(16)
}
// 根据断点获取列模板
getColumnsTemplate(): string {
switch (this.currentBreakpoint) {
case 'sm':
return '1fr 1fr' // 小屏两列
case 'md':
return '1fr 1fr 1fr' // 中屏三列
case 'lg':
return '1fr 1fr 1fr 1fr' // 大屏四列
default:
return '1fr 1fr'
}
}
}
2. 高级组件封装
2.1 响应式卡片组件
封装一个通用的响应式卡片组件,可在不同布局中复用:
@Component
struct ResponsiveCard {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
@Prop title: string = ''
@Prop subtitle: string = ''
@Prop content: string = ''
@Prop imageUrl: string = ''
@Prop tags: string[] = []
@Prop footerText: string = ''
@Prop aspectRatio: number = 16/9
build() {
Column() {
if (this.currentBreakpoint === 'sm') {
// 小屏布局:图片在上,内容在下
this.CardImage()
this.CardContent()
} else {
// 中大屏布局:根据aspectRatio决定是横向还是纵向布局
if (this.aspectRatio >= 1) {
// 横向布局:图片在上,内容在下
this.CardImage()
this.CardContent()
} else {
// 纵向布局:图片在左,内容在右
Row() {
this.CardImage()
.layoutWeight(1)
this.CardContent()
.layoutWeight(2)
}
.width('100%')
}
}
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: 2
})
}
@Builder CardImage() {
Image(this.imageUrl)
.width('100%')
.aspectRatio(this.aspectRatio)
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 12, topRight: 12 })
}
@Builder CardContent() {
Column() {
// 标题
Text(this.title)
.fontSize(this.getTitleFontSize())
.fontWeight(FontWeight.Bold)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
// 副标题
if (this.subtitle) {
Text(this.subtitle)
.fontSize(14)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
.margin({ top: 4 })
}
// 内容
if (this.content) {
Text(this.content)
.fontSize(14)
.fontColor('#333333')
.maxLines(this.getContentMaxLines())
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
.margin({ top: 8 })
}
// 标签
if (this.tags.length > 0) {
Row() {
ForEach(this.tags.slice(0, this.getMaxTags()), (tag: string, index) => {
Text(`#${tag}`)
.fontSize(12)
.fontColor('#007AFF')
.backgroundColor('rgba(0, 122, 255, 0.1)')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(12)
.margin({ right: index < this.getMaxTags() - 1 ? 8 : 0 })
})
}
.width('100%')
.margin({ top: 12 })
}
// 底部文本
if (this.footerText) {
Text(this.footerText)
.fontSize(12)
.fontColor('#999999')
.width('100%')
.margin({ top: 12 })
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(16)
}
// 根据断点获取标题字体大小
getTitleFontSize(): number {
switch (this.currentBreakpoint) {
case 'sm': return 16
case 'md': return 18
case 'lg': return 20
default: return 16
}
}
// 根据断点获取内容最大行数
getContentMaxLines(): number {
switch (this.currentBreakpoint) {
case 'sm': return 3
case 'md': return 4
case 'lg': return 5
default: return 3
}
}
// 根据断点获取最大标签数量
getMaxTags(): number {
switch (this.currentBreakpoint) {
case 'sm': return 2
case 'md': return 3
case 'lg': return 5
default: return 2
}
}
}
2.2 响应式网格容器组件
封装一个通用的响应式网格容器组件,简化网格布局的使用:
@Component
struct ResponsiveGridContainer {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
@BuilderParam content: () => void
@Prop columnsMap: Record<string, string> = { sm: '1fr', md: '1fr 1fr', lg: '1fr 1fr 1fr' }
@Prop gapMap: Record<string, number> = { sm: 12, md: 16, lg: 20 }
@Prop paddingMap: Record<string, number> = { sm: 12, md: 16, lg: 20 }
build() {
Grid() {
this.content()
}
.width('100%')
.columnsTemplate(this.getColumnsTemplate())
.rowsGap(this.getGap())
.columnsGap(this.getGap())
.padding(this.getPadding())
}
// 根据断点获取列模板
getColumnsTemplate(): string {
return this.columnsMap[this.currentBreakpoint] || this.columnsMap.md || '1fr 1fr'
}
// 根据断点获取间距
getGap(): number {
return this.gapMap[this.currentBreakpoint] || this.gapMap.md || 16
}
// 根据断点获取内边距
getPadding(): number {
return this.paddingMap[this.currentBreakpoint] || this.paddingMap.md || 16
}
}
使用示例:
ResponsiveGridContainer({
columnsMap: { sm: '1fr', md: '1fr 1fr', lg: '1fr 1fr 1fr 1fr' },
gapMap: { sm: 12, md: 16, lg: 20 },
paddingMap: { sm: 12, md: 16, lg: 24 }
}) {
ForEach(this.products, (product: Product) => {
GridItem() {
// 产品卡片内容
}
})
}
3. 高级性能优化
3.1 网格项复用策略
实现网格项复用,提高大量数据时的性能:
// 定义可复用的网格项ID生成器
function getItemId(item: NewsItem): string {
return `news-${item.id}`
}
// 在Grid中使用
Grid() {
LazyForEach(new NewsDataSource(this.getFilteredNews()), (item: NewsItem) => {
GridItem() {
// 网格项内容
}
.id(getItemId(item)) // 设置唯一ID用于复用
})
}
3.2 图片加载优化
优化网格中的图片加载,提高用户体验:
@Component
struct OptimizedImage {
@Prop src: string = ''
@Prop width: string | number = '100%'
@Prop height: string | number = '100%'
@Prop borderRadius: number | object = 0
@State isLoading: boolean = true
@State loadFailed: boolean = false
build() {
Stack({ alignContent: Alignment.Center }) {
// 加载中占位图
if (this.isLoading && !this.loadFailed) {
Column() {
LoadingProgress()
.width(24)
.height(24)
.color('#CCCCCC')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.borderRadius(this.borderRadius)
}
// 加载失败占位图
if (this.loadFailed) {
Column() {
Image($r('app.media.image_error'))
.width(32)
.height(32)
.fillColor('#CCCCCC')
Text('加载失败')
.fontSize(12)
.fontColor('#999999')
.margin({ top: 8 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.borderRadius(this.borderRadius)
}
// 实际图片
Image(this.src)
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
.borderRadius(this.borderRadius)
.opacity(this.isLoading || this.loadFailed ? 0 : 1)
.onComplete((event: { width: number, height: number, componentWidth: number, componentHeight: number }) => {
if (event.width > 0 && event.height > 0) {
this.isLoading = false
} else {
this.loadFailed = true
this.isLoading = false
}
})
.onError(() => {
this.loadFailed = true
this.isLoading = false
})
}
.width(this.width)
.height(this.height)
}
}
3.3 渲染优化
使用条件渲染和延迟加载优化网格渲染性能:
@Component
struct OptimizedGrid {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
@State items: any[] = []
@State visibleItems: any[] = []
@State isInitialRender: boolean = true
aboutToAppear() {
// 初始只加载部分数据
this.visibleItems = this.items.slice(0, this.getInitialLoadCount())
// 延迟加载剩余数据
setTimeout(() => {
this.isInitialRender = false
this.visibleItems = this.items
}, 300)
}
build() {
Grid() {
ForEach(this.visibleItems, (item: any) => {
GridItem() {
// 网格项内容
}
})
}
.columnsTemplate(this.getColumnsTemplate())
.rowsGap(16)
.columnsGap(16)
.width('100%')
}
// 根据断点获取初始加载数量
getInitialLoadCount(): number {
switch (this.currentBreakpoint) {
case 'sm': return 4
case 'md': return 6
case 'lg': return 9
default: return 4
}
}
// 根据断点获取列模板
getColumnsTemplate(): string {
switch (this.currentBreakpoint) {
case 'sm': return '1fr 1fr'
case 'md': return '1fr 1fr 1fr'
case 'lg': return '1fr 1fr 1fr 1fr'
default: return '1fr 1fr'
}
}
}
4. 实际案例:新闻资讯应用
4.1 新闻资讯应用架构
基于ResponsiveGrid.ets实现的新闻资讯应用架构:
// 应用主结构
@Entry
@Component
struct NewsApp {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
@State currentTab: string = 'home'
build() {
Column() {
// 根据当前标签显示不同页面
if (this.currentTab === 'home') {
NewsHomePage()
} else if (this.currentTab === 'video') {
VideoPage()
} else if (this.currentTab === 'live') {
LivePage()
} else if (this.currentTab === 'profile') {
ProfilePage()
}
// 底部导航栏
this.BottomNavigationBar()
}
.width('100%')
.height('100%')
}
@Builder BottomNavigationBar() {
Row() {
// 新闻标签
Column() {
Image($r('app.media.03'))
.width(24)
.height(24)
.fillColor(this.currentTab === 'home' ? '#007AFF' : '#8E8E93')
Text('新闻')
.fontSize(10)
.fontColor(this.currentTab === 'home' ? '#007AFF' : '#8E8E93')
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => this.currentTab = 'home')
// 视频标签
Column() {
Image($r('app.media.04'))
.width(24)
.height(24)
.fillColor(this.currentTab === 'video' ? '#007AFF' : '#8E8E93')
Text('视频')
.fontSize(10)
.fontColor(this.currentTab === 'video' ? '#007AFF' : '#8E8E93')
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => this.currentTab = 'video')
// 直播标签
Column() {
Image($r('app.media.big30'))
.width(24)
.height(24)
.fillColor(this.currentTab === 'live' ? '#007AFF' : '#8E8E93')
Text('直播')
.fontSize(10)
.fontColor(this.currentTab === 'live' ? '#007AFF' : '#8E8E93')
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => this.currentTab = 'live')
// 我的标签
Column() {
Image($r('app.media.profile_icon'))
.width(24)
.height(24)
.fillColor(this.currentTab === 'profile' ? '#007AFF' : '#8E8E93')
Text('我的')
.fontSize(10)
.fontColor(this.currentTab === 'profile' ? '#007AFF' : '#8E8E93')
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => this.currentTab = 'profile')
}
.width('100%')
.height(60)
.backgroundColor('#FFFFFF')
.borderColor('#E5E5EA')
.borderWidth({ top: 1 })
}
}
4.2 新闻详情页面
新闻详情页面的响应式布局实现:
@Component
struct NewsDetailPage {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
@Prop newsId: number = 0
@State newsDetail: NewsDetail = null
@State relatedNews: NewsItem[] = []
aboutToAppear() {
// 模拟加载新闻详情和相关新闻
this.loadNewsDetail()
this.loadRelatedNews()
}
build() {
if (this.newsDetail) {
// 小屏和中屏使用垂直布局
if (this.currentBreakpoint === 'sm' || this.currentBreakpoint === 'md') {
Column() {
// 顶部导航栏
this.NavigationBar()
// 新闻内容
Scroll() {
Column() {
// 新闻标题和信息
this.NewsHeader()
// 新闻内容
this.NewsContent()
// 相关新闻
this.RelatedNews()
}
.width('100%')
.padding(16)
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
} else {
// 大屏使用水平布局
Column() {
// 顶部导航栏
this.NavigationBar()
// 内容区域
Row() {
// 新闻内容
Scroll() {
Column() {
// 新闻标题和信息
this.NewsHeader()
// 新闻内容
this.NewsContent()
}
.width('100%')
.padding(24)
}
.width('70%')
.height('100%')
// 相关新闻
Scroll() {
Column() {
this.RelatedNews()
}
.width('100%')
.padding(16)
}
.width('30%')
.height('100%')
.backgroundColor('#F5F5F5')
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
} else {
// 加载中状态
Column() {
LoadingProgress()
.width(48)
.height(48)
Text('加载中...')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
// 组件定义...
@Builder NavigationBar() {
// 导航栏实现
}
@Builder NewsHeader() {
// 新闻标题和信息实现
}
@Builder NewsContent() {
// 新闻内容实现
}
@Builder RelatedNews() {
// 相关新闻实现
Text('相关推荐')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width('100%')
.margin({ top: 24, bottom: 16 })
Grid() {
ForEach(this.relatedNews, (item: NewsItem) => {
GridItem() {
// 相关新闻卡片
}
})
}
.columnsTemplate(this.getRelatedNewsColumns())
.rowsGap(16)
.columnsGap(16)
.width('100%')
}
// 根据断点获取相关新闻列数
getRelatedNewsColumns(): string {
switch (this.currentBreakpoint) {
case 'sm': return '1fr'
case 'md': return '1fr 1fr'
case 'lg': return '1fr'
default: return '1fr'
}
}
// 加载新闻详情
loadNewsDetail() {
// 模拟网络请求
}
// 加载相关新闻
loadRelatedNews() {
// 模拟网络请求
}
}
5. 最佳实践与注意事项
5.1 响应式设计原则
原则 | 描述 |
---|---|
内容优先 | 确定哪些内容在不同屏幕尺寸下必须显示,哪些可以隐藏 |
流式布局 | 使用相对单位和弹性布局,避免固定尺寸 |
断点设计 | 合理设置断点,确保在断点切换时布局变化自然 |
渐进增强 | 从小屏幕开始设计,逐步增强大屏幕的体验 |
一致性 | 保持不同屏幕尺寸下的视觉和交互一致性 |
5.2 性能优化建议
- 懒加载与分页:对于大量数据,使用懒加载和分页技术
- 图片优化:根据屏幕尺寸加载不同分辨率的图片
- 条件渲染:只渲染当前可见的内容
- 复用组件:使用ID标识可复用的组件
- 减少状态变化:避免频繁的状态变化导致不必要的重渲染
5.3 调试技巧
- 使用预览器:利用HarmonyOS开发工具的预览器测试不同屏幕尺寸
- 断点日志:在断点变化时添加日志,帮助调试
- 边界测试:测试断点边界值附近的布局变化
- 设备旋转测试:测试设备旋转时的布局变化
- 真机测试:在实际设备上测试,确保体验一致
6. 总结
本文深入探讨了HarmonyOS NEXT响应式网格布局的高级应用,包括复杂布局案例、高级组件封装、性能优化策略以及实际项目中的最佳实践。通过掌握这些高级技巧,开发者可以构建出适应各种设备的高质量应用,实现真正的"一次开发,多端部署"。
00
- 0回答
- 4粉丝
- 0关注
相关话题
- 172.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 基础篇
- 173.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 进阶篇
- 171.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局高级篇
- 162.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:高级篇
- 168.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局高级篇
- 180.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局高级篇
- 165.[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局高级篇:复杂布局与高级技巧
- 176.[HarmonyOS NEXT 实战案例七:Grid] 嵌套网格布局进阶篇:高级布局与交互技巧
- 177.[HarmonyOS NEXT 实战案例七:Grid] 嵌套网格布局高级篇:复杂业务场景与高级定制
- [HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(上)
- [HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(下)
- 183.[HarmonyOS NEXT 实战案例九:Grid] 电商网格布局高级篇:复杂场景与性能优化
- 164.[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局进阶篇:新闻应用高级布局与交互
- 160.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:基础篇
- 166.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局基础篇