HarmonyOS ArkUI训练营入门-组件掌握系列-List 列表组件开发技巧-PC版本

概述

列表是移动应用中最常用的组件之一,用于展示大量数据集合。HarmonyOS ArkUI 提供的 List 组件功能强大,支持懒加载、滑动操作、分组展示等特性。本文将从组件基础、数据渲染、样式定制、交互处理、性能优化等多个维度,深入讲解 List 组件的开发技巧。


一、List 组件基础

1.1 组件定义与作用

List 组件用于展示数据列表,支持垂直和水平滚动,是构建列表页面、消息列表、商品列表等场景的核心组件。

typescript 复制代码
@Entry
@Component
struct ListBasic {
  @State items: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5'];

  build() {
    Column() {
      List() {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .padding(16)
          }
        })
      }
      .width('100%')
      .height('100%')
    }
  }
}

1.2 核心属性

属性 类型 说明 默认值
listDirection Axis 滚动方向 Axis.Vertical
initialIndex number 初始滚动位置 0
scroller Scroller 滚动控制器 -
divider Divider 分割线配置 -
edgeEffect EdgeEffect 边缘效果 EdgeEffect.Spring

1.3 基础使用示例

typescript 复制代码
@Entry
@Component
struct BasicList {
  @State fruits: string[] = ['苹果', '香蕉', '橘子', '葡萄', '西瓜', '草莓', '蓝莓', '芒果'];

  build() {
    Column() {
      Text('水果列表')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 16 })
      
      List() {
        ForEach(this.fruits, (fruit: string) => {
          ListItem() {
            Text(fruit)
              .fontSize(16)
              .padding(16)
              .width('100%')
          }
        })
      }
      .width('90%')
      .height(300)
      .backgroundColor('#FFFFFF')
      .borderRadius(8)
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

二、数据渲染

2.1 使用 ForEach 渲染列表

ForEach 是 ArkUI 框架提供的循环渲染组件,用于遍历数据数组:

typescript 复制代码
@Entry
@Component
struct ForEachList {
  @State items: string[] = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];

  build() {
    Column() {
      List() {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .padding(16)
          }
        })
      }
      .width('100%')
      .height('100%')
    }
  }
}

2.2 使用自定义数据类

对于复杂数据,使用自定义类来管理数据结构:

typescript 复制代码
class ItemData {
  id: number = 0;
  title: string = '';
  desc: string = '';
  icon: string = '';

  constructor(id: number, title: string, desc: string, icon: string) {
    this.id = id;
    this.title = title;
    this.desc = desc;
    this.icon = icon;
  }
}

@Entry
@Component
struct CustomDataList {
  @State listData: ItemData[] = [
    new ItemData(1, '标题1', '描述信息1', '📱'),
    new ItemData(2, '标题2', '描述信息2', '💻'),
    new ItemData(3, '标题3', '描述信息3', '🎨'),
    new ItemData(4, '标题4', '描述信息4', '🚀'),
    new ItemData(5, '标题5', '描述信息5', '⭐')
  ];

  build() {
    Column() {
      List() {
        ForEach(this.listData, (item: ItemData) => {
          ListItem() {
            Row() {
              Text(item.icon)
                .fontSize(24)
              Column() {
                Text(item.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                Text(item.desc)
                  .fontSize(12)
                  .fontColor('#999999')
                  .margin({ top: 4 })
              }
              .margin({ left: 12 })
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
            }
            .padding(16)
          }
        })
      }
      .width('90%')
      .height(300)
    }
    .padding(20)
  }
}

2.3 动态数据更新

通过状态变量驱动列表更新:

typescript 复制代码
@Entry
@Component
struct DynamicList {
  @State items: string[] = ['项目1', '项目2', '项目3'];
  @State newItem: string = '';

  addItem() {
    if (this.newItem.trim()) {
      this.items = [...this.items, this.newItem];
      this.newItem = '';
    }
  }

  removeItem(index: number) {
    const newItems: string[] = [];
    for (let i: number = 0; i < this.items.length; i++) {
      if (i !== index) {
        newItems.push(this.items[i]);
      }
    }
    this.items = newItems;
  }

  build() {
    Column() {
      Row() {
        TextInput({ placeholder: '输入新项目' })
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#FFFFFF')
          .border({ width: 1, color: '#E5E5E5' })
          .onChange((value: string) => {
            this.newItem = value;
          })
        Button('添加')
          .width(60)
          .height(40)
          .backgroundColor('#0A59F7')
          .fontColor('#FFFFFF')
          .margin({ left: 8 })
          .onClick(() => {
            this.addItem();
          })
      }
      .width('90%')
      .margin({ bottom: 16 })
      
      List() {
        ForEach(this.items, (item: string, index: number) => {
          ListItem() {
            Row() {
              Text(item)
                .fontSize(16)
                .layoutWeight(1)
              Button('删除')
                .width(50)
                .height(30)
                .backgroundColor('#FF3B30')
                .fontColor('#FFFFFF')
                .fontSize(12)
                .onClick(() => {
                  this.removeItem(index);
                })
            }
            .padding(12)
          }
        })
      }
      .width('90%')
      .height(300)
    }
    .padding(20)
  }
}

三、样式定制

3.1 列表项样式

定制列表项的外观:

typescript 复制代码
@Entry
@Component
struct StyledListItem {
  @State items: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5'];

  build() {
    Column() {
      List() {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .fontColor('#333333')
              .padding(16)
              .width('100%')
              .backgroundColor('#FFFFFF')
          }
        })
      }
      .width('90%')
      .height(300)
      .backgroundColor('#F5F5F5')
    }
    .padding(20)
  }
}

3.2 分割线样式

通过 divider 属性定制分割线:

typescript 复制代码
@Entry
@Component
struct DividerList {
  @State items: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5'];

  build() {
    Column() {
      List() {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .padding(16)
          }
        })
      }
      .width('90%')
      .height(300)
      .divider({
        strokeWidth: 1,
        color: '#E5E5E5',
        startMargin: 16,
        endMargin: 16
      })
    }
    .padding(20)
  }
}

3.3 列表背景与圆角

设置列表的背景色和圆角:

typescript 复制代码
@Entry
@Component
struct BackgroundList {
  @State items: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5'];

  build() {
    Column() {
      List() {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .padding(16)
          }
        })
      }
      .width('90%')
      .height(300)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .shadow({ radius: 4, color: 'rgba(0, 0, 0, 0.1)' })
    }
    .padding(20)
  }
}

3.4 完整样式示例

typescript 复制代码
@Entry
@Component
struct CompleteListStyle {
  @State listData: ItemData[] = [
    new ItemData(1, 'ArkTS 基础语法', '声明式 UI,状态驱动', '#0A59F7'),
    new ItemData(2, '组件与布局', 'Container、Flex、Grid', '#34C759'),
    new ItemData(3, '页面路由', 'router.pushUrl / back', '#FF9500'),
    new ItemData(4, '@State 状态', '数据变化自动刷新 UI', '#AF52DE'),
    new ItemData(5, '列表与循环', 'ForEach 数据渲染', '#FF2D55')
  ];

  build() {
    Column() {
      Text('学习目录')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 16 })
      
      List() {
        ForEach(this.listData, (item: ItemData) => {
          ListItem() {
            Row() {
              Text(String(item.id))
                .width(32)
                .height(32)
                .borderRadius(16)
                .backgroundColor(item.color)
                .fontColor('#FFFFFF')
                .textAlign(TextAlign.Center)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
              
              Column() {
                Text(item.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                  .fontColor('#333333')
                Text(item.desc)
                  .fontSize(12)
                  .fontColor('#999999')
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .margin({ left: 12 })
              .alignItems(HorizontalAlign.Start)
              
              Text('›')
                .fontSize(20)
                .fontColor('#CCCCCC')
            }
            .padding(16)
            .backgroundColor('#FFFFFF')
          }
        })
      }
      .width('90%')
      .height(300)
      .backgroundColor('#F5F5F5')
      .divider({
        strokeWidth: 1,
        color: '#E5E5E5',
        startMargin: 16,
        endMargin: 16
      })
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }
}

四、交互处理

4.1 点击事件

通过 ListItemonClick 事件处理点击:

typescript 复制代码
@Entry
@Component
struct ClickableList {
  @State items: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5'];
  @State selectedItem: string = '';

  build() {
    Column() {
      Text('选中:' + (this.selectedItem || '无'))
        .fontSize(14)
        .fontColor('#0A59F7')
        .margin({ bottom: 16 })
      
      List() {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .padding(16)
          }
          .onClick(() => {
            this.selectedItem = item;
          })
        })
      }
      .width('90%')
      .height(300)
    }
    .padding(20)
  }
}

4.2 选中状态

实现列表项的选中/未选中状态切换:

typescript 复制代码
@Entry
@Component
struct SelectableList {
  @State items: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5'];
  @State selectedIndex: number = -1;

  build() {
    Column() {
      List() {
        ForEach(this.items, (item: string, index: number) => {
          ListItem() {
            Row() {
              Text(item)
                .fontSize(16)
                .fontColor(this.selectedIndex === index ? '#0A59F7' : '#333333')
                .layoutWeight(1)
              if (this.selectedIndex === index) {
                Text('✓')
                  .fontSize(18)
                  .fontColor('#34C759')
              }
            }
            .padding(16)
            .backgroundColor(this.selectedIndex === index ? '#EAF4FF' : '#FFFFFF')
          }
          .onClick(() => {
            this.selectedIndex = index;
          })
        })
      }
      .width('90%')
      .height(300)
      .divider({ strokeWidth: 1, color: '#E5E5E5' })
    }
    .padding(20)
  }
}

4.3 滚动监听

通过 Scroller 监听滚动事件:

typescript 复制代码
@Entry
@Component
struct ScrollableList {
  @State items: string[] = [];
  @State scrollPosition: string = '顶部';
  private scroller: Scroller = new Scroller();

  aboutToAppear() {
    for (let i: number = 0; i < 50; i++) {
      this.items.push('项目' + (i + 1));
    }
  }

  build() {
    Column() {
      Text('滚动位置:' + this.scrollPosition)
        .fontSize(14)
        .fontColor('#999999')
        .margin({ bottom: 16 })
      
      List({ scroller: this.scroller }) {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .padding(16)
          }
        })
      }
      .width('90%')
      .height(300)
      .onScroll((scrollOffset: number) => {
        if (scrollOffset < 100) {
          this.scrollPosition = '顶部';
        } else if (scrollOffset > 1000) {
          this.scrollPosition = '底部';
        } else {
          this.scrollPosition = '中间';
        }
      })
    }
    .padding(20)
  }
}

五、高级用法

5.1 分组列表

实现带分组的列表展示:

typescript 复制代码
class GroupData {
  groupTitle: string = '';
  items: string[] = [];

  constructor(groupTitle: string, items: string[]) {
    this.groupTitle = groupTitle;
    this.items = items;
  }
}

@Entry
@Component
struct GroupedList {
  @State groups: GroupData[] = [
    new GroupData('水果', ['苹果', '香蕉', '橘子']),
    new GroupData('蔬菜', ['白菜', '胡萝卜', '西红柿']),
    new GroupData('肉类', ['猪肉', '牛肉', '鸡肉'])
  ];

  build() {
    Column() {
      List() {
        ForEach(this.groups, (group: GroupData) => {
          // 分组标题
          ListItem() {
            Text(group.groupTitle)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#666666')
              .padding(12)
              .backgroundColor('#F5F5F5')
          }
          
          // 分组内容
          ForEach(group.items, (item: string) => {
            ListItem() {
              Text(item)
                .fontSize(16)
                .padding(16)
            }
          })
        })
      }
      .width('90%')
      .height(300)
    }
    .padding(20)
  }
}

5.2 带图标的列表

创建包含图标的列表项:

typescript 复制代码
@Entry
@Component
struct IconList {
  @State menuItems: { icon: string; title: string; desc: string }[] = [
    { icon: '📱', title: '手机', desc: '设备管理' },
    { icon: '📧', title: '邮件', desc: '查看邮件' },
    { icon: '📷', title: '相机', desc: '拍摄照片' },
    { icon: '🎵', title: '音乐', desc: '播放音乐' },
    { icon: '⚙️', title: '设置', desc: '系统设置' }
  ];

  build() {
    Column() {
      List() {
        ForEach(this.menuItems, (item) => {
          ListItem() {
            Row() {
              Text(item.icon)
                .fontSize(28)
              Column() {
                Text(item.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                Text(item.desc)
                  .fontSize(12)
                  .fontColor('#999999')
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .margin({ left: 12 })
              .alignItems(HorizontalAlign.Start)
              Text('›')
                .fontSize(20)
                .fontColor('#CCCCCC')
            }
            .padding(16)
          }
        })
      }
      .width('90%')
      .height(300)
      .divider({ strokeWidth: 1, color: '#E5E5E5', startMargin: 56 })
    }
    .padding(20)
  }
}

5.3 横向列表

创建横向滚动的列表:

typescript 复制代码
@Entry
@Component
struct HorizontalList {
  @State items: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5', '项目6', '项目7', '项目8'];

  build() {
    Column() {
      Text('横向列表')
        .fontSize(16)
        .margin({ bottom: 12 })
      
      List({ space: 12 }) {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(14)
              .padding({ left: 20, right: 20, top: 12, bottom: 12 })
              .backgroundColor('#FFFFFF')
              .borderRadius(20)
          }
        })
      }
      .width('100%')
      .height(60)
      .listDirection(Axis.Horizontal)
      .padding({ left: 20, right: 20 })
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

5.4 列表加载更多

实现列表的加载更多功能:

typescript 复制代码
@Entry
@Component
struct LoadMoreList {
  @State items: string[] = [];
  @State isLoading: boolean = false;
  @State hasMore: boolean = true;

  aboutToAppear() {
    this.loadData(0, 10);
  }

  loadData(start: number, count: number) {
    for (let i: number = start; i < start + count && i < 50; i++) {
      this.items.push('项目' + (i + 1));
    }
    this.hasMore = this.items.length < 50;
  }

  onReachEnd() {
    if (!this.isLoading && this.hasMore) {
      this.isLoading = true;
      setTimeout(() => {
        this.loadData(this.items.length, 10);
        this.isLoading = false;
      }, 1000);
    }
  }

  build() {
    Column() {
      List() {
        ForEach(this.items, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .padding(16)
          }
        })
        
        // 加载更多提示
        if (this.isLoading) {
          ListItem() {
            Row() {
              Progress({ value: 0, total: 100, type: ProgressType.Eclipse })
                .width(20)
                .height(20)
              Text('加载中...')
                .fontSize(14)
                .fontColor('#999999')
                .margin({ left: 8 })
            }
            .padding(16)
            .justifyContent(FlexAlign.Center)
          }
        } else if (!this.hasMore) {
          ListItem() {
            Text('已加载全部')
              .fontSize(14)
              .fontColor('#999999')
              .padding(16)
              .textAlign(TextAlign.Center)
          }
        }
      }
      .width('90%')
      .height(300)
      .onReachEnd(() => {
        this.onReachEnd();
      })
    }
    .padding(20)
  }
}

六、实际案例:联系人列表

6.1 需求分析

构建一个联系人列表页面,包含:

  • 联系人分组展示(按首字母)
  • 联系人搜索功能
  • 联系人点击跳转
  • 列表项包含头像、姓名、电话

6.2 代码实现

typescript 复制代码
import { router } from '@kit.ArkUI';

class Contact {
  id: number = 0;
  name: string = '';
  phone: string = '';
  avatar: string = '';

  constructor(id: number, name: string, phone: string, avatar: string) {
    this.id = id;
    this.name = name;
    this.phone = phone;
    this.avatar = avatar;
  }
}

class ContactGroup {
  letter: string = '';
  contacts: Contact[] = [];

  constructor(letter: string, contacts: Contact[]) {
    this.letter = letter;
    this.contacts = contacts;
  }
}

@Entry
@Component
struct ContactList {
  @State groups: ContactGroup[] = [
    new ContactGroup('A', [
      new Contact(1, '张三', '13800138000', '张'),
      new Contact(2, '李四', '13900139000', '李'),
      new Contact(3, '王五', '13700137000', '王')
    ]),
    new ContactGroup('B', [
      new Contact(4, '赵六', '13600136000', '赵'),
      new Contact(5, '钱七', '13500135000', '钱')
    ]),
    new ContactGroup('C', [
      new Contact(6, '孙八', '13400134000', '孙'),
      new Contact(7, '周九', '13300133000', '周')
    ])
  ];
  @State searchKeyword: string = '';
  @State filteredGroups: ContactGroup[] = [];

  onSearch() {
    if (!this.searchKeyword.trim()) {
      this.filteredGroups = this.groups;
      return;
    }
    
    this.filteredGroups = [];
    for (const group of this.groups) {
      const filteredContacts: Contact[] = [];
      for (const contact of group.contacts) {
        if (contact.name.includes(this.searchKeyword)) {
          filteredContacts.push(contact);
        }
      }
      if (filteredContacts.length > 0) {
        this.filteredGroups.push(new ContactGroup(group.letter, filteredContacts));
      }
    }
  }

  aboutToAppear() {
    this.filteredGroups = this.groups;
  }

  build() {
    Column() {
      Text('联系人')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 16 })
      
      // 搜索框
      TextInput({ placeholder: '搜索联系人' })
        .width('85%')
        .height(44)
        .backgroundColor('#FFFFFF')
        .border({ width: 1, color: '#E5E5E5', radius: 22 })
        .padding({ left: 20, right: 20 })
        .margin({ bottom: 16 })
        .onChange((value: string) => {
          this.searchKeyword = value;
          this.onSearch();
        })
      
      // 联系人列表
      List() {
        ForEach(this.filteredGroups, (group: ContactGroup) => {
          // 分组标题
          ListItem() {
            Text(group.letter)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#666666')
              .padding(12)
              .backgroundColor('#F5F5F5')
          }
          
          // 联系人列表项
          ForEach(group.contacts, (contact: Contact) => {
            ListItem() {
              Row() {
                // 头像
                Text(contact.avatar)
                  .width(44)
                  .height(44)
                  .borderRadius(22)
                  .backgroundColor('#0A59F7')
                  .fontColor('#FFFFFF')
                  .fontSize(18)
                  .fontWeight(FontWeight.Bold)
                  .textAlign(TextAlign.Center)
                
                // 信息
                Column() {
                  Text(contact.name)
                    .fontSize(16)
                    .fontWeight(FontWeight.Medium)
                  Text(contact.phone)
                    .fontSize(12)
                    .fontColor('#999999')
                    .margin({ top: 4 })
                }
                .layoutWeight(1)
                .margin({ left: 12 })
                .alignItems(HorizontalAlign.Start)
                
                // 箭头
                Text('›')
                  .fontSize(20)
                  .fontColor('#CCCCCC')
              }
              .padding(12)
            }
            .onClick(() => {
              console.info('点击联系人:' + contact.name);
            })
          })
        })
      }
      .width('85%')
      .layoutWeight(1)
      .divider({ strokeWidth: 1, color: '#E5E5E5', startMargin: 68 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }
}

七、常见问题与解决方案

7.1 列表渲染异常

问题描述:列表项显示不完整或布局错乱。

解决方案

  1. 确保每个 ListItem 都有明确的尺寸约束
  2. 检查 ForEach 的第三个参数(key)是否唯一
  3. 确认数据数组的结构正确

7.2 列表滚动不流畅

问题描述:列表滚动时有卡顿现象。

解决方案

  1. 避免在列表项中使用复杂的嵌套布局
  2. 使用懒加载减少初始渲染量
  3. 避免在滚动时触发频繁的状态更新

7.3 列表项点击无响应

问题描述 :点击列表项后,onClick 事件没有触发。

解决方案

  1. 检查 ListItem 是否有 enabled 属性设置为 false
  2. 确认列表项有足够的尺寸
  3. 检查是否有其他组件拦截了触摸事件

7.4 动态更新数据不生效

问题描述:更新数据后,列表没有刷新。

解决方案

  1. 确保使用状态变量(@State@Observed 等)
  2. 检查是否正确更新了数组引用
  3. 确认 ForEach 的数据源正确

八、性能优化建议

8.1 使用懒加载

对于大量数据,使用懒加载减少内存占用:

typescript 复制代码
List() {
  ForEach(this.items, (item: string) => {
    ListItem() {
      // 只渲染可见的列表项
    }
  })
}

8.2 避免复杂布局

简化列表项的布局结构:

typescript 复制代码
// 避免
ListItem() {
  Column() {
    Row() {
      // 多层嵌套
    }
  }
}

// 推荐
ListItem() {
  Row() {
    // 扁平化布局
  }
}

8.3 合理使用 key

ForEach 提供唯一的 key,提高渲染效率:

typescript 复制代码
ForEach(this.items, (item: ItemData) => {
  ListItem() {
    // ...
  }
}, (item: ItemData) => item.id)

8.4 避免频繁状态更新

在滚动过程中,避免触发状态更新:

typescript 复制代码
.onScroll((scrollOffset: number) => {
  // 避免频繁更新状态
  if (scrollOffset % 100 === 0) {
    // 更新状态
  }
})

九、总结

List 组件是 HarmonyOS ArkUI 中最常用的组件之一,掌握其使用方法对于构建高质量的用户界面至关重要。

核心要点

  1. 使用 ForEach 渲染列表数据
  2. 通过 ListItem 定义列表项内容
  3. 使用 divider 属性添加分割线
  4. 通过事件处理实现交互功能
  5. 注意性能优化,避免卡顿

希望本文能帮助你更好地理解和使用 List 组件,构建出优秀的 HarmonyOS 应用。


参考资料