ArkUI作为鸿蒙系统的UI框架,提供了丰富的布局组件,帮助开发者快速构建美观且响应式的界面。本文将介绍ArkUI中最常用的五种布局组件:栅格布局列表网格轮播选项卡,适合初学者循序渐进地学习和掌握。

一、栅格布局 (GridRow/GridCol)

栅格布局是一种通用的辅助定位工具,特别适合在不同尺寸设备上实现自适应布局。它通过将页面划分为等宽的列和行,为界面元素提供统一的定位标准。

核心优势

  • 规律性定位:通过行列划分,使界面元素排列有序
  • 多设备适配:统一的定位标注确保不同设备上布局一致性
  • 灵活间距调整:支持行列间距的精细控制
  • 自动换行与自适应:内容超出时自动换行,适应不同屏幕尺寸
功能 说明 示例
断点系统 响应不同屏幕尺寸 breakpoints: {value: ['320vp', '520vp']}
自定义列数 灵活控制布局结构 columns: { md: 8, lg: 12 }
元素排序 控制显示顺序 order: { xs: 2, sm: 1 }
嵌套布局 构建复杂界面 GridRow内嵌套GridRow

基本使用

栅格布局由GridRow(栅格容器)和GridCol(栅格子组件)组成:

GridRow() {
  GridCol() {
    Text('第一列内容')
      .width('100%')
      .height(80)
      .backgroundColor('#FF4081')
  }
  GridCol() {
    Text('第二列内容')
      .width('100%')
      .height(80)
      .backgroundColor('#3F51B5')
  }
}
.columns(2) // 设置总列数为2
.gutter(10) // 设置列间距为10vp
断点

栅格系统默认将设备宽度分为四类断点,可根据不同屏幕尺寸调整布局:

// 自定义断点
GridRow({ 
  breakpoints: { value: ['320vp', '520vp', '840vp', '1080vp'] }
}) {
  GridCol({ 
    span: { xs: 12, sm: 6, md: 4, lg: 3 } 
  }) {
    // 子组件内容
  }
}

上述代码表示:在最小屏幕(xs)上占12列(全屏),小屏(sm)占6列(半屏),中屏(md)占4列,大屏(lg)占3列。

示例

排列方向控制
// 默认从左到右排列
GridRow({ direction: GridRowDirection.Row }) { ... }

// 从右到左排列
GridRow({ direction: GridRowDirection.RowReverse }) { ... }
子组件偏移与排序
// 偏移2列
GridCol({ offset: 2 }) { ... }

// 自定义排序
GridCol({ order: { xs: 3, sm: 2, md: 1 } }) { ... }
嵌套使用

栅格布局支持嵌套,实现复杂界面:

GridRow() {
  GridCol({ span: 8 }) {
    // 左侧主内容区
    GridRow() {
      GridCol({ span: 12 }) { /* 标题区 */ }
      GridCol({ span: 6 }) { /* 左侧内容 */ }
      GridCol({ span: 6 }) { /* 右侧内容 */ }
    }
  }
  GridCol({ span: 4 }) {
    // 右侧边栏
  }
}

二、列表 (List)

  • 列表是用于展示结构化、可滚动信息的高效组件,适合呈现同类数据集合。
功能 实现方式 应用场景
分组列表 ListItemGroup 联系人分组
粘性标题 sticky: StickyStyle.Header 分类列表
侧滑操作 swipeAction 消息删除
懒加载 LazyForEach 长列表优化

基本使用

List() {
  ListItem() {
    Text('列表项1')
      .width('100%')
      .height(60)
      .padding(16)
  }
  ListItem() {
    Text('列表项2')
      .width('100%')
      .height(60)
      .padding(16)
  }
}
.width('100%')
.divider({ strokeWidth: 2, color: '#EEEEEE' }) // 添加分隔线

循环渲染列表

使用ForEach实现动态列表:

@Entry
@Component
struct ListDemo {
  private items: string[] = ['苹果', '香蕉', '橙子', '草莓', '葡萄']
  
  build() {
    List() {
      ForEach(this.items, (item) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(60)
            .padding(16)
        }
      }, (item) => item)
    }
    .width('100%')
    .space(8) // 列表项间距
  }
}

列表分类与分组

使用ListItemGroup实现分组列表:

List() {
  ListItemGroup({ header: Text('水果').fontSize(16).fontWeight(FontWeight.Bold) }) {
    ListItem() { Text('苹果') }
    ListItem() { Text('香蕉') }
  }
  
  ListItemGroup({ header: Text('蔬菜').fontSize(16).fontWeight(FontWeight.Bold) }) {
    ListItem() { Text('西红柿') }
    ListItem() { Text('黄瓜') }
  }
}
.sticky(StickyStyle.Header) // 粘性标题(吸顶效果)

长列表懒加载

对于大量数据,使用LazyForEach实现懒加载:

class MyDataSource implements IDataSource {
  private data: string[] = []
  
  constructor(size: number) {
    // 模拟1000条数据
    for (let i = 0; i < size; i++) {
      this.data.push(`列表项 ${i + 1}`);
    }
  }
  
  totalCount(): number {
    return this.data.length;
  }
  
  getData(index: number): string {
    return this.data[index];
  }
  
  registerDataChangeListener(listener: DataChangeListener): void {}
  unregisterDataChangeListener(listener: DataChangeListener): void {}
}

@Entry
@Component
struct LazyListDemo {
  private dataSource: MyDataSource = new MyDataSource(1000);
  
  build() {
    List() {
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(60)
            .padding(16)
        }
      })
    }
    .cachedCount(5) // 预加载5项
  }
}
滑动删除
ListItem() {
  Text('可滑动删除项')
}
.swipeAction({
  end: this.deleteButton()
})

@Builder deleteButton() {
  Button('删除')
    .width(60)
    .height(60)
    .backgroundColor(Color.Red)
    .onClick(() => {
      // 处理删除逻辑
    })
}
下拉刷新与上拉加载
List() {
  // 列表内容
}
.onReachStart(() => {
  // 下拉刷新逻辑
})
.onReachEnd(() => {
  // 上拉加载更多逻辑
})

三、网格布局 (Grid/GridItem)

网格布局提供了强大的二维布局能力,适合实现九宫格、计算器、日历等界面。

基本使用

Grid() {
  GridItem() { Text('1').fontSize(24) }
  GridItem() { Text('2').fontSize(24) }
  GridItem() { Text('3').fontSize(24) }
  GridItem() { Text('4').fontSize(24) }
  GridItem() { Text('5').fontSize(24) }
  GridItem() { Text('6').fontSize(24) }
}
.columnsTemplate('1fr 1fr 1fr') // 3列等宽
.rowsTemplate('1fr 1fr') // 2行等高
.columnsGap(10) // 列间距
.rowsGap(10) // 行间距
.width('100%')
.height(300)

不规则网格布局

通过onGetRectByIndex实现跨行跨列效果:

Grid({
  layoutOptions: {
    regularSize: [1, 1], // 常规项占1行1列
    onGetRectByIndex: (index: number) => {
      // 自定义特定项的位置和大小
      if (index === 0) {
        return [0, 0, 2, 2]; // [rowStart, columnStart, rowSpan, columnSpan]
      }
      return [0, 0, 1, 1]; // 默认占1行1列
    }
  }
}) {
  // 第一个项跨2行2列
  GridItem() {
    Text('大项')
      .width('100%')
      .height('100%')
      .backgroundColor('#FF4081')
  }
  
  // 其他常规项
  ForEach(Array(8).fill(0), (_, i) => {
    GridItem() {
      Text(`${i + 2}`)
        .width('100%')
        .height('100%')
        .backgroundColor('#3F51B5')
    }
  })
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.width('100%')
.height(400)

响应式网格

根据屏幕尺寸动态调整列数:

@Entry
@Component
struct ResponsiveGrid {
  @State columns: string = '1fr' // 默认1列
  
  // 监听屏幕尺寸变化
  private mediaQueryListener: MediaQueryListener = mediaquery.matchMediaSync('(min-width: 600vp)');
  
  aboutToAppear() {
    this.mediaQueryListener.on('change', (result) => {
      if (result.matches) {
        this.columns = '1fr 1fr'; // 大屏显示2列
      } else {
        this.columns = '1fr'; // 小屏显示1列
      }
    });
  }
  
  build() {
    Grid() {
      // 网格内容
      ForEach(Array(6).fill(0), (_, i) => {
        GridItem() {
          Text(`Item ${i + 1}`)
            .width('100%')
            .height(100)
            .backgroundColor('#3F51B5')
        }
      })
    }
    .columnsTemplate(this.columns)
    .columnsGap(10)
    .rowsGap(10)
    .padding(10)
  }
}

示例:计算器界面

Grid() {
  GridItem() { Text('C').fontSize(24) }
    .backgroundColor('#9E9E9E')
  GridItem() { Text('←').fontSize(24) }
    .backgroundColor('#9E9E9E')
  GridItem() { Text('%').fontSize(24) }
    .backgroundColor('#9E9E9E')
  GridItem() { Text('÷').fontSize(24) }
    .backgroundColor('#FF9800')
  
  // 数字按钮...
  
  // 0按钮跨2列
  GridItem({ columnStart: 0, columnEnd: 2 }) { 
    Text('0').fontSize(24) 
  }
    .backgroundColor('#333333')
  
  GridItem() { Text('.').fontSize(24) }
    .backgroundColor('#333333')
  GridItem() { Text('=').fontSize(24) }
    .backgroundColor('#FF9800')
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.width('100%')
.height(500)

四、轮播组件 (Swiper)

轮播组件用于实现内容的滑动切换,常见于广告轮播、图片预览等场景。

基本使用

Swiper() {
  Text('第一页')
    .width('100%')
    .height(200)
    .backgroundColor('#FF4081')
    .textAlign(TextAlign.Center)
    .fontSize(24)
    
  Text('第二页')
    .width('100%')
    .height(200)
    .backgroundColor('#3F51B5')
    .textAlign(TextAlign.Center)
    .fontSize(24)
    
  Text('第三页')
    .width('100%')
    .height(200)
    .backgroundColor('#4CAF50')
    .textAlign(TextAlign.Center)
    .fontSize(24)
}
.indicator(true) // 显示指示器
.autoPlay(true) // 自动播放
.interval(3000) // 切换间隔3秒
.loop(true) // 循环播放

图片轮播实现

@Entry
@Component
struct ImageSwiper {
  private images: Resource[] = [
    $r('app.media.image1'),
    $r('app.media.image2'),
    $r('app.media.image3')
  ]
  
  build() {
    Swiper() {
      ForEach(this.images, (image) => {
        Image(image)
          .width('100%')
          .height(200)
          .objectFit(ImageFit.Cover)
      })
    }
    .indicator(Indicator.Dot().selectedColor(Color.Red))
    .autoPlay(true)
    .loop(true)
  }
}

自定义指示器样式

Swiper() {
  // 轮播内容
}
.indicator(
  Indicator.Number()
    .fontColor(Color.White)
    .selectedFontColor(Color.Red)
    .fontSize(14)
)

垂直轮播

Swiper() {
  // 轮播内容
}
.vertical(true) // 垂直方向轮播
.height(300)

控制器控制

@Entry
@Component
struct ControlledSwiper {
  private swiperController: SwiperController = new SwiperController()
  
  build() {
    Column() {
      Swiper(this.swiperController) {
        // 轮播内容
      }
      .indicator(true)
      
      Row({ space: 10 }) {
        Button('上一页')
          .onClick(() => {
            this.swiperController.showPrevious()
          })
        Button('下一页')
          .onClick(() => {
            this.swiperController.showNext()
          })
      }
      .margin(10)
    }
  }
}

五、选项卡 (Tabs)

选项卡组件用于在一个页面内实现多个视图的快速切换,常见于分类内容展示。

基本使用

Tabs() {
  TabContent() {
    Text('首页内容')
      .width('100%')
      .height('100%')
      .textAlign(TextAlign.Center)
      .fontSize(24)
  }
  .tabBar('首页')
  
  TabContent() {
    Text('发现内容')
      .width('100%')
      .height('100%')
      .textAlign(TextAlign.Center)
      .fontSize(24)
  }
  .tabBar('发现')
  
  TabContent() {
    Text('我的内容')
      .width('100%')
      .height('100%')
      .textAlign(TextAlign.Center)
      .fontSize(24)
  }
  .tabBar('我的')
}
.width('100%')
.height(300)

底部导航栏

Tabs({ barPosition: BarPosition.End }) {
  TabContent() {
    // 首页内容
  }
  .tabBar(this.tabBuilder('首页', $r('app.media.home')))
  
  TabContent() {
    // 分类内容
  }
  .tabBar(this.tabBuilder('分类', $r('app.media.category')))
  
  TabContent() {
    // 我的内容
  }
  .tabBar(this.tabBuilder('我的', $r('app.media.mine')))
}
.barHeight(56) // 底部导航栏高度
.backgroundColor('#FFFFFF') // 背景色

@Builder tabBuilder(title: string, icon: Resource) {
  Column({ space: 4 }) {
    Image(icon)
      .size({ width: 24, height: 24 })
    Text(title)
      .fontSize(12)
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}

控制器与事件监听

@Entry
@Component
struct TabsWithController {
  private controller: TabsController = new TabsController()
  @State currentIndex: number = 0
  
  build() {
    Column() {
      Tabs({ 
        barPosition: BarPosition.Start,
        controller: this.controller,
        index: this.currentIndex
      }) {
        TabContent() { Text('页面1') }.tabBar('标签1')
        TabContent() { Text('页面2') }.tabBar('标签2')
        TabContent() { Text('页面3') }.tabBar('标签3')
      }
      .onChange((index: number) => {
        this.currentIndex = index
        console.log(`切换到标签 ${index + 1}`)
      })
      
      Button('切换到标签2')
        .margin(10)
        .onClick(() => {
          this.controller.changeIndex(1)
        })
    }
  }
}

自定义选项卡样式

Tabs() {
  // 选项卡内容
}
.barMode(BarMode.Fixed) // 固定模式
.barWidth(300) // 选项卡宽度
.animationDuration(300) // 切换动画时长

六、总结

布局选择原则:

  • 单列数据:List
  • 二维网格:Grid
  • 跨设备响应:栅格+媒体查询
  • 导航结构:Tabs

性能优化:

  • 长列表使用LazyForEach
  • 设置合理cachedCount
  • 避免深层次嵌套

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐