165.[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局高级篇:复杂布局与高级技巧

2025-06-30 22:44:09
105次阅读
0个评论

[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局高级篇:复杂布局与高级技巧

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

效果演示

image.png

1. 复杂网格布局设计理念

在前两篇教程中,我们学习了Grid组件的基础用法和进阶技巧。在本篇高级教程中,我们将深入探讨Grid组件在复杂场景下的应用,以及一些高级布局技巧和优化方法。

1.1 复杂网格布局的设计原则

设计复杂网格布局时,应遵循以下原则:

原则 描述
内容优先 布局应服务于内容,而非相反
视觉层次 通过大小、位置、颜色等因素建立清晰的视觉层次
一致性 保持设计语言的一致性,提高用户理解和使用效率
响应式 布局应能适应不同屏幕尺寸和方向
可扩展性 布局应能轻松适应内容的增减变化
可访问性 考虑不同用户的使用需求,包括视力障碍用户

1.2 网格系统的高级概念

在复杂网格布局中,我们需要理解以下高级概念:

  1. 网格轨道:Grid布局中的行和列,可以使用固定尺寸、百分比或弹性单位(fr)定义
  2. 网格线:定义网格轨道边界的线,可以用于定位GridItem
  3. 网格单元:网格中的最小单位,由相邻的两条行线和两条列线围成
  4. 网格区域:由多个网格单元组成的矩形区域,可以通过网格线或区域名称定义
  5. 隐式网格:当GridItem放置在显式定义的网格范围之外时,Grid会自动创建隐式轨道

2. 高级网格布局技术

2.1 网格自动布局算法

在复杂的新闻应用中,我们可能需要根据内容自动调整网格布局:

@State newsData: NewsData[] = [];
@State layoutMode: string = 'auto'; // 'auto', 'dense', 'manual'

build() {
  Grid() {
    ForEach(this.newsData, (news: NewsData, index: number) => {
      GridItem() {
        NewsCard({ news: news })
      }
      // 自动布局模式下不指定位置
      .if(this.layoutMode === 'manual', item => {
        // 手动布局模式下指定位置
        if (index === 0) {
          item.rowStart(0).rowEnd(1).columnStart(0).columnEnd(1);
        } else if (index === 1) {
          item.rowStart(0).columnStart(2);
        } else {
          // 其他项的位置...
        }
      })
    })
  }
  .columnsTemplate('1fr 1fr 1fr')
  .rowsTemplate('200px 200px 100px 100px')
  .layoutDirection(GridDirection.Row) // 布局方向
  .maxCount(20) // 最大子组件数量
  .layoutWeight(1)
  .width('100%')
  // 自动放置算法
  .autoFlow(this.layoutMode === 'dense' ? GridAutoFlow.RowDense : GridAutoFlow.Row)
}

这段代码展示了如何根据不同的布局模式(自动、密集、手动)调整网格的自动布局算法。

2.2 复杂网格线命名

在复杂布局中,我们可以为网格线命名,使GridItem的定位更加直观:

Grid() {
  // 网格项内容...
}
.columnsTemplate('[start] 1fr [content-start] 2fr [content-end] 1fr [end]')
.rowsTemplate('[top] 200px [header-end] 200px [main] 100px [footer] 100px [bottom]')

// 使用命名的网格线定位GridItem
GridItem() {
  // 头条新闻内容...
}
.rowStart('top')
.rowEnd('header-end')
.columnStart('start')
.columnEnd('content-end')

通过为网格线命名,我们可以更直观地定义GridItem的位置,而不必依赖数字索引。

2.3 嵌套网格布局

在复杂的新闻应用中,我们可以使用嵌套网格实现更灵活的布局:

// 外层网格
Grid() {
  // 头条新闻区域
  GridItem() {
    // 头条新闻内容...
  }
  .rowStart(0)
  .rowEnd(1)
  .columnStart(0)
  .columnEnd(2)
  
  // 侧边栏区域(包含嵌套网格)
  GridItem() {
    // 嵌套网格
    Grid() {
      // 热门新闻
      GridItem() {
        Text('热门新闻')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .width('100%')
      }
      .rowStart(0)
      .columnSpan(2)
      
      // 热门新闻项
      ForEach(this.hotNews, (news: NewsData, index: number) => {
        GridItem() {
          SmallNewsCard({ news: news })
        }
        .rowStart(Math.floor(index / 2) + 1)
        .columnStart(index % 2)
      })
    }
    .columnsTemplate('1fr 1fr')
    .rowsTemplate('40px repeat(3, 120px)')
    .width('100%')
    .height('100%')
  }
  .rowStart(0)
  .rowEnd(2)
  .columnStart(2)
  
  // 底部新闻列表区域
  GridItem() {
    // 底部新闻列表内容...
  }
  .rowStart(1)
  .columnStart(0)
  .columnEnd(1)
}
.columnsTemplate('2fr 1fr 1fr')
.rowsTemplate('300px 200px')

通过嵌套网格,我们可以在一个网格单元内创建更复杂的布局结构,实现更灵活的内容组织。

3. 动态与自适应布局

3.1 基于内容的动态网格

在新闻应用中,我们可能需要根据内容动态调整网格布局:

@State newsData: NewsData[] = [];

getNewsImportance(news: NewsData): number {
  // 根据新闻属性计算重要性分数
  let score = 0;
  if (news.isTop) score += 3;
  if (news.isHot) score += 2;
  if (news.summary) score += 1;
  return score;
}

build() {
  Grid() {
    ForEach(this.newsData, (news: NewsData, index: number) => {
      GridItem() {
        NewsCard({ news: news })
      }
      // 根据新闻重要性动态设置网格项大小
      .if(this.getNewsImportance(news) >= 5, item => {
        // 重要新闻占据2行2列
        item.rowSpan(2).columnSpan(2);
      })
      .if(this.getNewsImportance(news) >= 3 && this.getNewsImportance(news) < 5, item => {
        // 次重要新闻占据1行2列
        item.columnSpan(2);
      })
      // 普通新闻占据1格
    })
  }
  .columnsTemplate('repeat(4, 1fr)')
  .autoFlow(GridAutoFlow.RowDense) // 使用密集放置算法填充空隙
}

这段代码展示了如何根据新闻的重要性动态调整其在网格中占据的空间,重要新闻会占据更大的区域。

3.2 响应式布局策略

为了适应不同屏幕尺寸,我们可以实现复杂的响应式布局策略:

@State screenClass: string = 'medium'; // 'small', 'medium', 'large', 'xlarge'

onPageShow() {
  this.updateScreenClass();
}

updateScreenClass() {
  const screenWidth = px2vp(window.getWindowWidth());
  if (screenWidth < 600) {
    this.screenClass = 'small';
  } else if (screenWidth < 840) {
    this.screenClass = 'medium';
  } else if (screenWidth < 1080) {
    this.screenClass = 'large';
  } else {
    this.screenClass = 'xlarge';
  }
}

build() {
  Column() {
    // 根据屏幕类别选择不同的布局
    if (this.screenClass === 'small') {
      this.SmallScreenLayout();
    } else if (this.screenClass === 'medium') {
      this.MediumScreenLayout();
    } else {
      this.LargeScreenLayout();
    }
  }
  .width('100%')
  .height('100%')
}

@Builder
SmallScreenLayout() {
  Grid() {
    // 小屏幕布局(2列)
    // 网格项内容...
  }
  .columnsTemplate('1fr 1fr')
  .rowsTemplate('repeat(auto-fill, 150px)')
}

@Builder
MediumScreenLayout() {
  Grid() {
    // 中等屏幕布局(3列)
    // 网格项内容...
  }
  .columnsTemplate('1fr 1fr 1fr')
  .rowsTemplate('200px 200px 100px 100px')
}

@Builder
LargeScreenLayout() {
  Grid() {
    // 大屏幕布局(4列)
    // 网格项内容...
  }
  .columnsTemplate('1fr 1fr 1fr 1fr')
  .rowsTemplate('250px 250px 150px 150px')
}

这段代码展示了如何根据屏幕尺寸动态选择不同的网格布局,为不同设备提供最佳的用户体验。

3.3 动态行高与列宽

在复杂布局中,我们可能需要动态调整行高和列宽:

@State newsData: NewsData[] = [];
@State expandedNews: number | null = null;

build() {
  Grid() {
    ForEach(this.newsData, (news: NewsData, index: number) => {
      GridItem() {
        NewsCard({
          news: news,
          expanded: this.expandedNews === news.id,
          onToggleExpand: () => {
            if (this.expandedNews === news.id) {
              this.expandedNews = null;
            } else {
              this.expandedNews = news.id;
            }
          }
        })
      }
      // 展开的新闻占据更多行
      .if(this.expandedNews === news.id, item => {
        item.rowSpan(2);
      })
    })
  }
  .columnsTemplate('repeat(3, 1fr)')
  // 动态行模板
  .rowsTemplate(this.expandedNews !== null ? '200px 300px 150px 150px' : '200px 200px 150px 150px')
  .onBreakpointChange((breakpoint) => {
    // 响应断点变化
    console.log(`Grid breakpoint changed: ${breakpoint}`);
  })
}

这段代码展示了如何根据用户交互动态调整网格的行高,当用户展开某条新闻时,该新闻会占据更多的垂直空间。

4. 高级交互与动画

4.1 网格项拖拽排序

在新闻应用中,我们可以实现网格项的拖拽排序功能:

@State newsData: NewsData[] = [];
@State dragIndex: number = -1;
@State targetIndex: number = -1;

build() {
  Grid() {
    ForEach(this.newsData, (news: NewsData, index: number) => {
      GridItem() {
        NewsCard({ news: news })
          .opacity(this.dragIndex === index ? 0.6 : 1.0)
          .border(this.targetIndex === index ? {
            width: 2,
            color: '#FF6B35',
            style: BorderStyle.Dashed
          } : null)
          .gesture(
            PanGesture()
              .onActionStart(() => {
                this.dragIndex = index;
              })
              .onActionUpdate((event) => {
                // 计算目标位置
                const targetRow = Math.floor((event.globalY - this.gridTop) / this.rowHeight);
                const targetCol = Math.floor((event.globalX - this.gridLeft) / this.colWidth);
                const newTargetIndex = targetRow * this.columnCount + targetCol;
                
                if (newTargetIndex >= 0 && newTargetIndex < this.newsData.length) {
                  this.targetIndex = newTargetIndex;
                }
              })
              .onActionEnd(() => {
                if (this.dragIndex !== -1 && this.targetIndex !== -1 && this.dragIndex !== this.targetIndex) {
                  // 交换位置
                  const temp = this.newsData[this.dragIndex];
                  this.newsData.splice(this.dragIndex, 1);
                  this.newsData.splice(this.targetIndex, 0, temp);
                }
                this.dragIndex = -1;
                this.targetIndex = -1;
              })
          )
      }
    })
  }
  .columnsTemplate('repeat(3, 1fr)')
  .rowsTemplate('repeat(auto-fill, 200px)')
  .onAreaChange((oldArea, newArea) => {
    // 记录网格位置和尺寸
    this.gridLeft = newArea.globalPosition.x;
    this.gridTop = newArea.globalPosition.y;
    this.gridWidth = newArea.width;
    this.gridHeight = newArea.height;
    this.rowHeight = this.gridHeight / (this.gridHeight / 200);
    this.colWidth = this.gridWidth / 3;
  })
}

这段代码实现了网格项的拖拽排序功能,用户可以通过拖拽调整新闻的显示顺序。

4.2 网格过渡动画

我们可以为网格布局添加流畅的过渡动画:

@State layoutMode: string = 'grid'; // 'grid', 'list'
@State animationConfig: AnimationOption = {
  duration: 300,
  curve: Curve.EaseInOut,
  delay: 0,
  iterations: 1,
  playMode: PlayMode.Normal
};

build() {
  Column() {
    // 布局切换按钮
    Row() {
      Button('网格视图')
        .onClick(() => {
          this.layoutMode = 'grid';
        })
        .backgroundColor(this.layoutMode === 'grid' ? '#FF6B35' : '#F5F5F5')
        .fontColor(this.layoutMode === 'grid' ? '#FFFFFF' : '#666666')
      
      Button('列表视图')
        .onClick(() => {
          this.layoutMode = 'list';
        })
        .backgroundColor(this.layoutMode === 'list' ? '#FF6B35' : '#F5F5F5')
        .fontColor(this.layoutMode === 'list' ? '#FFFFFF' : '#666666')
        .margin({ left: 16 })
    }
    .width('100%')
    .padding(16)
    
    // 动态切换布局
    if (this.layoutMode === 'grid') {
      Grid() {
        ForEach(this.newsData, (news: NewsData, index: number) => {
          GridItem() {
            NewsCard({ news: news, mode: 'grid' })
          }
          .transition(this.animationConfig) // 添加过渡动画
        })
      }
      .columnsTemplate('repeat(3, 1fr)')
      .rowsTemplate('repeat(auto-fill, 200px)')
      .width('100%')
      .layoutWeight(1)
    } else {
      List() {
        ForEach(this.newsData, (news: NewsData) => {
          ListItem() {
            NewsCard({ news: news, mode: 'list' })
          }
          .transition(this.animationConfig) // 添加过渡动画
        })
      }
      .width('100%')
      .layoutWeight(1)
    }
  }
  .width('100%')
  .height('100%')
}

这段代码实现了网格视图和列表视图之间的平滑切换,通过添加过渡动画提升用户体验。

4.3 网格项动画效果

我们可以为网格项添加丰富的动画效果:

@State animatedItems: boolean[] = [];

aboutToAppear() {
  // 初始化动画状态
  this.animatedItems = new Array(this.newsData.length).fill(false);
  
  // 错开动画时间,实现瀑布流动画效果
  for (let i = 0; i < this.newsData.length; i++) {
    setTimeout(() => {
      this.animatedItems[i] = true;
    }, i * 100);
  }
}

build() {
  Grid() {
    ForEach(this.newsData, (news: NewsData, index: number) => {
      GridItem() {
        NewsCard({ news: news })
          .opacity(this.animatedItems[index] ? 1.0 : 0.0)
          .translate({ y: this.animatedItems[index] ? 0 : 50 })
          .scale({ x: this.animatedItems[index] ? 1.0 : 0.9, y: this.animatedItems[index] ? 1.0 : 0.9 })
          .animation({
            duration: 300,
            curve: Curve.EaseOut,
            delay: 0,
            iterations: 1,
            playMode: PlayMode.Normal
          })
      }
    })
  }
  .columnsTemplate('repeat(3, 1fr)')
  .rowsTemplate('repeat(auto-fill, 200px)')
}

这段代码实现了网格项的入场动画效果,新闻卡片会以错开的时间依次淡入并上移,创造瀑布流动画效果。

5. 高级布局案例

5.1 杂志风格布局

我们可以使用Grid组件创建杂志风格的新闻布局:

@Component
struct MagazineLayout {
  @State newsData: NewsData[] = [];
  
  build() {
    Grid() {
      // 头条新闻(大图)
      GridItem() {
        Stack({ alignContent: Alignment.BottomStart }) {
          Image(this.newsData[0].image)
            .width('100%')
            .height('100%')
            .objectFit(ImageFit.Cover)
          
          // 渐变遮罩
          Column()
            .width('100%')
            .height('40%')
            .linearGradient({
              direction: GradientDirection.Bottom,
              colors: [['rgba(0, 0, 0, 0)', 0.0], ['rgba(0, 0, 0, 0.8)', 1.0]]
            })
          
          // 标题
          Text(this.newsData[0].title)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .padding(16)
        }
        .width('100%')
        .height('100%')
        .borderRadius(16)
      }
      .rowStart(0)
      .rowEnd(2)
      .columnStart(0)
      .columnEnd(1)
      
      // 右上角新闻(中图)
      GridItem() {
        Column() {
          Image(this.newsData[1].image)
            .width('100%')
            .aspectRatio(1.5)
            .objectFit(ImageFit.Cover)
            .borderRadius(12)
          
          Text(this.newsData[1].title)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin({ top: 8 })
          
          Text(this.newsData[1].summary || '')
            .fontSize(14)
            .fontColor('#666666')
            .margin({ top: 4 })
            .maxLines(2)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')
        .height('100%')
      }
      .rowStart(0)
      .columnStart(2)
      .columnEnd(3)
      
      // 右下角新闻(小图文)
      GridItem() {
        Row() {
          Image(this.newsData[2].image)
            .width(80)
            .height(80)
            .objectFit(ImageFit.Cover)
            .borderRadius(8)
          
          Column() {
            Text(this.newsData[2].title)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            
            Text(this.newsData[2].time)
              .fontSize(12)
              .fontColor('#999999')
              .margin({ top: 8 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
        }
        .width('100%')
        .height('100%')
      }
      .rowStart(1)
      .columnStart(2)
      .columnEnd(3)
      
      // 底部横向新闻列表
      ForEach(this.newsData.slice(3, 6), (news: NewsData, index: number) => {
        GridItem() {
          Column() {
            Image(news.image)
              .width('100%')
              .aspectRatio(1.2)
              .objectFit(ImageFit.Cover)
              .borderRadius(12)
            
            Text(news.title)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .margin({ top: 8 })
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .height('100%')
        }
        .rowStart(2)
        .columnStart(index)
      })
    }
    .columnsTemplate('2fr 1fr 1fr')
    .rowsTemplate('300px 150px 250px')
    .rowsGap(16)
    .columnsGap(16)
    .width('100%')
    .padding(16)
  }
}

这段代码实现了杂志风格的新闻布局,包括大图头条、中图新闻、小图文新闻和底部横向新闻列表,创造出丰富的视觉层次。

5.2 瀑布流布局

我们可以使用Grid组件实现瀑布流布局:

@Component
struct WaterfallLayout {
  @State newsData: NewsData[] = [];
  @State columnHeights: number[] = [0, 0, 0]; // 跟踪每列的当前高度
  
  aboutToAppear() {
    // 初始化列高度
    this.columnHeights = [0, 0, 0];
  }
  
  getNextColumn(): number {
    // 找出当前高度最小的列
    let minHeight = Math.min(...this.columnHeights);
    return this.columnHeights.indexOf(minHeight);
  }
  
  build() {
    Grid() {
      ForEach(this.newsData, (news: NewsData, index: number) => {
        GridItem() {
          Column() {
            Image(news.image)
              .width('100%')
              .aspectRatio(Math.random() * 0.5 + 0.8) // 随机高宽比
              .objectFit(ImageFit.Cover)
              .borderRadius(12)
            
            Text(news.title)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .margin({ top: 8 })
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            
            if (news.summary) {
              Text(news.summary)
                .fontSize(14)
                .fontColor('#666666')
                .margin({ top: 4 })
                .maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            
            Row() {
              Text(news.time)
                .fontSize(12)
                .fontColor('#999999')
              
              Blank()
              
              Text(`${news.readCount || 0}阅读`)
                .fontSize(12)
                .fontColor('#999999')
            }
            .width('100%')
            .margin({ top: 8 })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(16)
          .onAppear(() => {
            // 计算此项的高度(这里简化处理)
            const itemHeight = 200 + (news.summary ? 60 : 0);
            
            // 找出最短的列
            const column = this.getNextColumn();
            
            // 更新列高度
            this.columnHeights[column] += itemHeight + 16; // 加上间距
          })
        }
        // 自动放置到下一个位置
      })
    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsGap(16)
    .columnsGap(16)
    .width('100%')
    .padding(16)
    .backgroundColor('#F8F8F8')
  }
}

这段代码实现了瀑布流布局,通过跟踪每列的高度,将新的网格项放置在当前高度最小的列中,创造出参差不齐但视觉平衡的布局效果。

5.3 混合布局策略

在复杂的新闻应用中,我们可以结合多种布局策略:

@Component
struct HybridLayout {
  @State newsData: NewsData[] = [];
  @State featuredNews: NewsData[] = [];
  @State regularNews: NewsData[] = [];
  @State smallNews: NewsData[] = [];
  
  aboutToAppear() {
    // 根据新闻属性分类
    this.featuredNews = this.newsData.filter(news => news.isTop);
    this.regularNews = this.newsData.filter(news => !news.isTop && news.summary);
    this.smallNews = this.newsData.filter(news => !news.isTop && !news.summary);
  }
  
  build() {
    Column() {
      // 头条区域(杂志风格)
      if (this.featuredNews.length > 0) {
        this.MagazineSection(this.featuredNews)
      }
      
      // 常规新闻区域(网格布局)
      if (this.regularNews.length > 0) {
        Text('热门资讯')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .padding({ left: 16, top: 24, bottom: 16 })
        
        this.GridSection(this.regularNews)
      }
      
      // 小型新闻区域(列表布局)
      if (this.smallNews.length > 0) {
        Text('快讯')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .padding({ left: 16, top: 24, bottom: 16 })
        
        this.ListSection(this.smallNews)
      }
    }
    .width('100%')
  }
  
  @Builder
  MagazineSection(news: NewsData[]) {
    // 杂志风格布局实现...
  }
  
  @Builder
  GridSection(news: NewsData[]) {
    Grid() {
      ForEach(news, (item: NewsData, index: number) => {
        GridItem() {
          // 网格新闻卡片实现...
        }
      })
    }
    .columnsTemplate('1fr 1fr')
    .rowsGap(16)
    .columnsGap(16)
    .width('100%')
    .padding(16)
  }
  
  @Builder
  ListSection(news: NewsData[]) {
    List() {
      ForEach(news, (item: NewsData) => {
        ListItem() {
          // 列表新闻项实现...
        }
      })
    }
    .width('100%')
    .padding(16)
  }
}

这段代码展示了如何在一个页面中结合杂志风格布局、网格布局和列表布局,根据新闻的不同特性选择最合适的展示方式。

6. 总结

在本教程中,我们深入探讨了HarmonyOS NEXT中Grid组件的高级应用技巧, 通过掌握这些高级技巧,我们可以创建出更加复杂、灵活和富有吸引力的不规则网格布局,为用户提供卓越的新闻阅读体验。

收藏00

登录 后评论。没有帐号? 注册 一个。