鸿蒙原生 ArkTS 布局之 List 的单选与多选模式实战指南

项目演示


目录


第一章:引言与背景

1.1 List 组件在移动开发中的地位

在移动应用开发中,列表(List)是最基础也最重要的 UI 组件之一。从通讯录到聊天记录,从购物车到设置页面,几乎每一个应用都离不开列表来展示结构化数据。

HarmonyOS NEXT 的 ArkUI 框架为 List 组件提供了强大的原生支持,不仅实现了流畅的滚动性能,还内置了丰富的交互模式,其中最常用也最实用的便是 单选多选 两种选择模式。

1.2 单选与多选的应用场景

单选模式场景

单选模式适用于用户需要从一组选项中选择唯一答案的场景:

  • 设置页面:选择主题(浅色/深色/跟随系统)、选择语言、选择默认支付方式
  • 表单填写:选择性别、选择出生日期、选择配送方式
  • 功能配置:选择定位精度、选择数据同步频率、选择消息提醒方式
  • 方案确认:选择配送地址、选择优惠券、选择支付渠道
多选模式场景

多选模式适用于用户需要同时选择多个选项的场景:

  • 批量操作:删除多条消息、移动多个文件、分享多张图片
  • 筛选过滤:多选标签筛选内容、多选条件组合搜索
  • 权限管理:批量授予权限、批量移除成员
  • 内容收藏:收藏多个项目、添加多个目标

1.3 为什么选择原生方案

在 HarmonyOS NEXT 开发中,我们有多种方式实现选择功能:

方案 优点 缺点
原生 List API 性能最优、体验一致、代码简洁 需要学习特定 API
自定义组件 灵活性高 开发量大、维护成本高
第三方库 开箱即用 依赖问题、版本适配

推荐选择原生方案的原因

  1. 性能保障:原生组件经过深度优化,滚动流畅、响应迅速
  2. 体验一致:与系统应用行为统一,用户无需额外学习
  3. 开发效率:声明式 API 简洁明了,代码量少
  4. 长期维护:官方持续更新,兼容性有保障

1.4 开发环境准备

开始之前,请确保开发环境满足以下要求:

项目 版本要求
操作系统 Windows 10/11 或 macOS 12+
DevEco Studio 5.0.0 Release 及以上
HarmonyOS SDK API 24 (HarmonyOS NEXT)
Node.js 18.0.0 及以上
JDK 17 及以上

创建新项目时,推荐选择 Empty Ability 模板,这样可以获得最精简的项目结构。


第二章:核心 API 深度解析

2.1 API 概览

HarmonyOS NEXT (API 24) 中,List 选择功能主要涉及以下几个核心 API:

API 所属组件 功能说明 API 版本
.editMode(boolean) List 进入/退出编辑模式 API 9 (已废弃但仍可用)
.multiSelectable(boolean) List 开启多选模式 API 8+
.selectable(boolean) ListItem 设置该项是否可选中 API 7+
.selected(boolean) ListItem 设置该项是否被选中 API 7+
.onSelect(callback) ListItem 监听选中状态变化 API 7+
.selectedState(state) ListItem 设置选中态样式 API 7+

2.2 List 组件属性详解

.editMode(boolean) - 编辑模式开关

这是控制列表是否进入编辑状态的总开关。当设置为 true 时,列表项会显示选择控件(如复选框、单选按钮),用户可以进行选择操作。

typescript 复制代码
// 进入编辑模式
List() {
  // ...
}
.editMode(true)

// 退出编辑模式
List() {
  // ...
}
.editMode(false)

注意事项

  • .editMode() 在 API 24 中已标记为 @deprecated,但官方仍保持兼容
  • 新推荐方式是使用 .multiSelectable(true) + 自定义选择控件
  • 如果项目需要长期维护,建议逐步迁移到新方案
.multiSelectable(boolean) - 多选模式

开启多选模式后,用户可以同时选中多个列表项:

typescript 复制代码
// 开启多选模式
List() {
  // ...
}
.multiSelectable(true)

.editMode(true) 的区别:

  • .editMode(true) 是传统方式,会自动显示选择控件
  • .multiSelectable(true) 更灵活,可以配合自定义 UI 使用

2.3 ListItem 组件属性详解

.selectable(boolean) - 是否可选中

设置单个列表项是否允许被选中:

typescript 复制代码
ListItem() {
  // 内容
}
.selectable(true)   // 可选中
// .selectable(false) // 不可选中

使用场景 :列表中某些项(如分组标题、功能按钮)不需要被选中时,设置为 false

.selected(boolean) - 选中状态

这是一个 受控属性,用于绑定列表项的选中状态:

typescript 复制代码
@State selectedId: number = 1

ListItem() {
  // 内容
}
.selected(this.selectedId === item.id)

.selected() 的参数值从 false 变为 true 时,该项会显示为选中状态;反之则显示为未选中状态。

.onSelect(callback) - 选中回调

监听列表项选中状态的变化:

typescript 复制代码
ListItem() {
  // 内容
}
.onSelect((isSelected: boolean) => {
  if (isSelected) {
    console.log('列表项被选中')
  } else {
    console.log('列表项被取消选中')
  }
})

回调参数说明

  • isSelected: boolean - 选中状态,true 表示被选中,false 表示取消选中
.selectedState(state) - 选中态样式

自定义选中状态下的视觉效果:

typescript 复制代码
ListItem() {
  // 内容
}
.selectedState({
  selected: {
    backgroundColor: '#E8F4FF',
    borderColor: '#007DFF'
  },
  unselected: {
    backgroundColor: '#FFFFFF',
    borderColor: '#CCCCCC'
  }
})

2.4 核心属性组合使用

单选模式组合
typescript 复制代码
List() {
  ForEach(this.itemList, (item: ItemData) => {
    ListItem() {
      this.ItemContent(item)
    }
    .selectable(true)
    .selected(this.selectedId === item.id)
    .onSelect((isSelected: boolean) => {
      if (isSelected) {
        this.selectedId = item.id
      }
    })
  }, (item: ItemData) => item.id.toString())
}
.editMode(this.isEditMode)
多选模式组合
typescript 复制代码
List() {
  ForEach(this.itemList, (item: ItemData) => {
    ListItem() {
      this.ItemContent(item)
    }
    .selectable(true)
    .selected(this.selectedIds.indexOf(item.id) !== -1)
    .onSelect((isSelected: boolean) => {
      if (isSelected) {
        this.selectedIds.push(item.id)
      } else {
        const index = this.selectedIds.indexOf(item.id)
        if (index !== -1) {
          this.selectedIds.splice(index, 1)
        }
      }
    })
  }, (item: ItemData) => item.id.toString())
}
.editMode(this.isEditMode)

2.5 受控 vs 非受控模式

非受控模式

使用组件内部状态管理选中:

typescript 复制代码
ListItem() {
  // 内容
}
.selectable(true)
.selected(true)  // 始终选中

缺点:无法在外部控制选中状态,实用性有限。

受控模式(推荐)

通过状态变量控制选中:

typescript 复制代码
@State selectedId: number = 1

ListItem() {
  // 内容
}
.selectable(true)
.selected(this.selectedId === item.id)
.onSelect((isSelected: boolean) => {
  this.selectedId = isSelected ? item.id : 0
})

优点:

  • 外部可以随时修改选中状态
  • 支持全选、清空等操作
  • 状态持久化方便

第三章:数据模型设计

3.1 ArkTS 类定义规范

ArkTS 对类定义有严格要求,必须遵循以下规则:

规则 1:所有字段必须在类体内声明,不能在构造函数参数中声明

typescript 复制代码
// ❌ 错误写法
class ItemData {
  constructor(public id: number, public name: string) {}
}

// ✅ 正确写法
class ItemData {
  id: number = 0
  name: string = ''
  
  constructor(id: number, name: string) {
    this.id = id
    this.name = name
  }
}

规则 2:字段必须有初始值

typescript 复制代码
// ❌ 错误写法
class ItemData {
  id: number  // 缺少初始值
  name: string  // 缺少初始值
}

// ✅ 正确写法
class ItemData {
  id: number = 0
  name: string = ''
}

规则 3:推荐使用明确的类型标注

typescript 复制代码
// ❌ 不推荐
let data = new ItemData(1, 'test')

// ✅ 推荐
let data: ItemData = new ItemData(1, 'test')

3.2 基础数据模型

单列表项模型
typescript 复制代码
class ListItemData {
  id: number = 0
  title: string = ''
  subtitle: string = ''
  icon: string = ''
  isEnabled: boolean = true

  constructor(
    id: number,
    title: string,
    subtitle: string,
    icon: string,
    isEnabled: boolean = true
  ) {
    this.id = id
    this.title = title
    this.subtitle = subtitle
    this.icon = icon
    this.isEnabled = isEnabled
  }
}
带分类的数据模型
typescript 复制代码
class CategoryItem {
  categoryId: string = ''
  categoryName: string = ''
  items: ListItemData[] = []

  constructor(categoryId: string, categoryName: string, items: ListItemData[]) {
    this.categoryId = categoryId
    this.categoryName = categoryName
    this.items = items
  }
}

3.3 初始化示例数据

typescript 复制代码
@State itemList: ListItemData[] = [
  new ListItemData(1, '消息通知', '接收应用推送消息', '✉️'),
  new ListItemData(2, '账户安全', '密码与身份验证', '🔒'),
  new ListItemData(3, '隐私设置', '个人信息保护', '🛡️'),
  new ListItemData(4, '主题模式', '深色/浅色切换', '🎨'),
  new ListItemData(5, '语言设置', '应用显示语言', '🌐'),
  new ListItemData(6, '清除缓存', '释放存储空间', '🗑️'),
  new ListItemData(7, '关于我们', '版本信息', 'ℹ️'),
  new ListItemData(8, '帮助中心', '常见问题解答', '❓'),
]

3.4 状态管理变量设计

单选模式状态
typescript 复制代码
// 编辑模式开关
@State isEditMode: boolean = false
// 当前选中项 ID,0 表示未选中
@State selectedId: number = 0
多选模式状态
typescript 复制代码
// 编辑模式开关
@State isEditMode: boolean = false
// 选中项 ID 列表(使用 Array 而非 Set,符合 ArkTS 规范)
@State selectedIds: number[] = []

3.5 选择逻辑辅助方法

typescript 复制代码
// 单选:检查某项是否被选中
private isItemSelected(id: number): boolean {
  return this.selectedId === id
}

// 多选:检查某项是否被选中
private isItemSelected(id: number): boolean {
  return this.selectedIds.indexOf(id) !== -1
}

// 获取选中数量
private getSelectedCount(): number {
  return this.selectedIds.length
}

// 清空所有选中
private clearAllSelections(): void {
  this.selectedId = 0
  this.selectedIds = []
}

// 全选(多选模式)
private selectAll(): void {
  this.selectedIds = []
  for (let i = 0; i < this.itemList.length; i++) {
    if (this.itemList[i].isEnabled) {
      this.selectedIds.push(this.itemList[i].id)
    }
  }
}

第四章:单选模式完整实现

4.1 单选模式特点

单选模式的核心特点是 互斥性:同时只能有一个列表项被选中。当用户选中新项时,之前的选中项会自动取消。

4.2 基础版实现

步骤 1:定义数据模型和状态
typescript 复制代码
class ListItemData {
  id: number = 0
  title: string = ''
  description: string = ''

  constructor(id: number, title: string, description: string) {
    this.id = id
    this.title = title
    this.description = description
  }
}

@Entry
@Component
struct SingleSelectDemo {
  // 数据源
  @State itemList: ListItemData[] = [
    new ListItemData(1, '选项一', '这是第一个选项'),
    new ListItemData(2, '选项二', '这是第二个选项'),
    new ListItemData(3, '选项三', '这是第三个选项'),
    new ListItemData(4, '选项四', '这是第四个选项'),
  ]
  
  // 编辑模式
  @State isEditMode: boolean = false
  // 当前选中项 ID
  @State selectedId: number = 0
}
步骤 2:实现 UI 构建器
typescript 复制代码
@Builder
private ItemBuilder(item: ListItemData) {
  Row() {
    Column() {
      Text(item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.selectedId === item.id ? '#007DFF' : '#333333')
      
      Text(item.description)
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    
    // 选中指示器(圆形单选按钮样式)
    Circle()
      .width(20)
      .height(20)
      .fill(this.selectedId === item.id ? '#007DFF' : 'transparent')
      .strokeWidth(2)
      .stroke(this.selectedId === item.id ? '#007DFF' : '#CCCCCC')
  }
  .width('100%')
  .padding(16)
  .backgroundColor(this.selectedId === item.id ? '#F0F7FF' : '#FFFFFF')
  .borderRadius(8)
}
步骤 3:构建主布局
typescript 复制代码
build() {
  Column() {
    // 标题栏
    Row() {
      Text('单选模式示例')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
      
      Button({ type: ButtonType.Normal }) {
        Text(this.isEditMode ? '完成' : '选择')
      }
      .onClick(() => {
        this.isEditMode = !this.isEditMode
        if (!this.isEditMode) {
          this.selectedId = 0
        }
      })
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#F8F8F8')
    
    // 已选中提示
    if (this.isEditMode && this.selectedId > 0) {
      Text('已选中:' + this.itemList.find(item => item.id === this.selectedId)?.title)
        .fontSize(14)
        .fontColor('#007DFF')
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
    }
    
    // 列表
    List({ space: 8 }) {
      ForEach(this.itemList, (item: ListItemData) => {
        ListItem() {
          this.ItemBuilder(item)
        }
        .selectable(true)
        .selected(this.selectedId === item.id)
        .onSelect((isSelected: boolean) => {
          if (this.isEditMode && isSelected) {
            this.selectedId = item.id
          }
        })
      }, (item: ListItemData) => item.id.toString())
    }
    .width('100%')
    .layoutWeight(1)
    .padding(12)
    .editMode(this.isEditMode)
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F0F0F0')
}

4.3 完整版实现(带交互反馈)

完整代码
typescript 复制代码
// ListItemData 类定义
class ListItemData {
  id: number = 0
  title: string = ''
  description: string = ''
  icon: string = ''
  price: string = ''
  isRecommended: boolean = false

  constructor(
    id: number,
    title: string,
    description: string,
    icon: string,
    price: string = '',
    isRecommended: boolean = false
  ) {
    this.id = id
    this.title = title
    this.description = description
    this.icon = icon
    this.price = price
    this.isRecommended = isRecommended
  }
}

@Entry
@Component
struct SingleSelectCompleteDemo {
  // 数据源 - 支付方式选择
  @State paymentList: ListItemData[] = [
    new ListItemData(1, '微信支付', '推荐使用', '💚', '免费', true),
    new ListItemData(2, '支付宝', '安全便捷', '💙', '免费'),
    new ListItemData(3, '银行卡', '支持主流银行', '💳', '免费'),
    new ListItemData(4, '话费支付', '小额便捷', '📱', '收取手续费'),
  ]
  
  // 编辑模式
  @State isEditMode: boolean = false
  // 选中的支付方式 ID
  @State selectedPaymentId: number = 1
  // 确认对话框显示状态
  @State showConfirmDialog: boolean = false

  // 切换编辑模式
  private toggleEditMode(): void {
    this.isEditMode = !this.isEditMode
  }

  // 确认选择
  private confirmSelection(): void {
    this.showConfirmDialog = true
  }

  // 获取选中项
  private getSelectedItem(): ListItemData | null {
    for (let i = 0; i < this.paymentList.length; i++) {
      if (this.paymentList[i].id === this.selectedPaymentId) {
        return this.paymentList[i]
      }
    }
    return null
  }

  @Builder
  private PaymentItemBuilder(item: ListItemData) {
    Row() {
      // 图标
      Text(item.icon)
        .fontSize(32)
        .margin({ right: 12 })

      // 中间信息
      Column() {
        Row() {
          Text(item.title)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .fontColor(this.selectedPaymentId === item.id ? '#007DFF' : '#333333')
          
          if (item.isRecommended) {
            Text('推荐')
              .fontSize(11)
              .fontColor('#FF6B00')
              .backgroundColor('#FFF3E0')
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .margin({ left: 8 })
              .borderRadius(4)
          }
        }

        Text(item.description)
          .fontSize(13)
          .fontColor('#999999')
          .margin({ top: 4 })
        
        Text(item.price)
          .fontSize(12)
          .fontColor('#07C160')
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      // 单选指示器
      Row() {
        if (this.selectedPaymentId === item.id) {
          Circle()
            .width(22)
            .height(22)
            .fill('#007DFF')
          
          Circle()
            .width(10)
            .height(10)
            .fill(Color.White)
        } else {
          Circle()
            .width(22)
            .height(22)
            .fill('transparent')
            .strokeWidth(2)
            .stroke('#CCCCCC')
        }
      }
      .width(24)
      .height(24)
      .justifyContent(FlexAlign.Center)
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(this.selectedPaymentId === item.id ? '#F0F7FF' : '#FFFFFF')
    .borderRadius(12)
    .border({
      width: this.selectedPaymentId === item.id ? 2 : 1,
      color: this.selectedPaymentId === item.id ? '#007DFF' : '#EEEEEE'
    })
  }

  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Text('选择支付方式')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 24, bottom: 16 })
      .backgroundColor(Color.White)

      // 当前选择提示
      Row() {
        Text('已选择:')
          .fontSize(14)
          .fontColor('#666666')
        
        Text(this.getSelectedItem()?.title ?? '未选择')
          .fontSize(14)
          .fontColor('#007DFF')
          .fontWeight(FontWeight.Medium)
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 8, bottom: 12 })

      // 列表区域
      List({ space: 12 }) {
        ForEach(this.paymentList, (item: ListItemData) => {
          ListItem() {
            this.PaymentItemBuilder(item)
          }
          .selectable(true)
          .selected(this.selectedPaymentId === item.id)
          .onSelect((isSelected: boolean) => {
            if (isSelected) {
              this.selectedPaymentId = item.id
            }
          })
        }, (item: ListItemData) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 20, right: 20 })

      // 底部确认按钮
      Button() {
        Text('确认支付方式')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
      }
      .width('90%')
      .height(48)
      .backgroundColor('#007DFF')
      .fontColor(Color.White)
      .borderRadius(24)
      .margin({ bottom: 24 })
      .onClick(() => {
        this.confirmSelection()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .bindSheet({
      show: this.showConfirmDialog,
      builder: this.ConfirmSheet,
      height: 300
    })
  }

  @Builder
  private ConfirmSheet() {
    Column() {
      Text('确认选择')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      Text('您选择的支付方式是:')
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 8 })

      Text(this.getSelectedItem()?.title ?? '')
        .fontSize(20)
        .fontColor('#007DFF')
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 24 })

      Row() {
        Button() {
          Text('取消')
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor('#F0F0F0')
        .fontColor('#666666')
        .borderRadius(22)
        .margin({ right: 12 })
        .onClick(() => {
          this.showConfirmDialog = false
        })

        Button() {
          Text('确认')
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor('#007DFF')
        .fontColor(Color.White)
        .borderRadius(22)
        .onClick(() => {
          this.showConfirmDialog = false
        })
      }
      .width('100%')
    }
    .width('100%')
    .padding(24)
    .backgroundColor(Color.White)
  }
}

4.4 关键实现要点

1. 选中状态更新

单选模式下,选中状态通过单个 @State 变量管理:

typescript 复制代码
.onSelect((isSelected: boolean) => {
  if (isSelected) {
    this.selectedId = item.id  // 直接赋值即可,之前的选中自动取消
  }
})
2. UI 反馈

选中状态需要在多个地方体现:

  • 选中指示器(圆形、勾选框等)
  • 背景色变化
  • 文字颜色变化
  • 边框高亮
3. 确认流程

重要选择操作建议增加确认步骤:

  • 显示选中项信息
  • 提供确认/取消按钮
  • 防止误操作

第五章:多选模式完整实现

5.1 多选模式特点

多选模式的核心特点是 可叠加性:用户可以同时选中多个列表项,实现批量操作。

5.2 基础版实现

typescript 复制代码
@Entry
@Component
struct MultiSelectBasicDemo {
  @State itemList: ListItemData[] = [
    new ListItemData(1, '文件 1', '1.2 MB'),
    new ListItemData(2, '文件 2', '3.5 MB'),
    new ListItemData(3, '文件 3', '856 KB'),
    new ListItemData(4, '文件 4', '2.1 MB'),
    new ListItemData(5, '文件 5', '4.8 MB'),
  ]

  @State isEditMode: boolean = false
  @State selectedIds: number[] = []

  // 检查是否选中
  private isSelected(id: number): boolean {
    return this.selectedIds.indexOf(id) !== -1
  }

  // 处理选中
  private handleSelect(id: number, isSelected: boolean): void {
    if (isSelected) {
      if (this.selectedIds.indexOf(id) === -1) {
        this.selectedIds.push(id)
      }
    } else {
      const index = this.selectedIds.indexOf(id)
      if (index !== -1) {
        this.selectedIds.splice(index, 1)
      }
    }
  }

  // 全选
  private selectAll(): void {
    this.selectedIds = []
    for (let i = 0; i < this.itemList.length; i++) {
      this.selectedIds.push(this.itemList[i].id)
    }
  }

  // 删除选中
  private deleteSelected(): void {
    const newList: ListItemData[] = []
    for (let i = 0; i < this.itemList.length; i++) {
      if (!this.isSelected(this.itemList[i].id)) {
        newList.push(this.itemList[i])
      }
    }
    this.itemList = newList
    this.selectedIds = []
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('文件管理')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        Button({ type: ButtonType.Normal }) {
          Text(this.isEditMode ? '完成' : '管理')
        }
        .onClick(() => {
          this.isEditMode = !this.isEditMode
          if (!this.isEditMode) {
            this.selectedIds = []
          }
        })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#F8F8F8')

      // 操作栏
      if (this.isEditMode) {
        Row() {
          Text('已选 ' + this.selectedIds.length + ' 项')
            .fontSize(14)
            .fontColor('#666666')

          Blank()

          Button() {
            Text('全选')
          }
          .fontSize(12)
          .backgroundColor('#E8F4FF')
          .fontColor('#007DFF')
          .margin({ right: 8 })
          .onClick(() => {
            this.selectAll()
          })

          Button() {
            Text('删除')
          }
          .fontSize(12)
          .backgroundColor('#FF6B6B')
          .fontColor(Color.White)
          .enabled(this.selectedIds.length > 0)
          .onClick(() => {
            this.deleteSelected()
          })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor(Color.White)
      }

      // 列表
      List({ space: 1 }) {
        ForEach(this.itemList, (item: ListItemData) => {
          ListItem() {
            Row() {
              if (this.isEditMode) {
                // 多选复选框
                Row() {
                  if (this.isSelected(item.id)) {
                    Icon('check', 20)
                      .fillColor(Color.White)
                  }
                }
                .width(22)
                .height(22)
                .backgroundColor(this.isSelected(item.id) ? '#007DFF' : '#FFFFFF')
                .border({
                  width: 2,
                  color: this.isSelected(item.id) ? '#007DFF' : '#CCCCCC'
                })
                .borderRadius(4)
                .margin({ right: 12 })
                .justifyContent(FlexAlign.Center)
                .alignItems(VerticalAlign.Center)
              }

              Column() {
                Text(item.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                  .fontColor(this.isSelected(item.id) ? '#007DFF' : '#333333')

                Text(item.description)
                  .fontSize(13)
                  .fontColor('#999999')
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .width('100%')
            .padding(16)
            .backgroundColor(this.isSelected(item.id) ? '#F0F7FF' : '#FFFFFF')
          }
          .selectable(true)
          .selected(this.isSelected(item.id))
          .onSelect((isSelected: boolean) => {
            if (this.isEditMode) {
              this.handleSelect(item.id, isSelected)
            }
          })
        }, (item: ListItemData) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .editMode(this.isEditMode)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

5.3 完整版实现(带拖拽排序和更多操作)

完整代码结构
typescript 复制代码
// 消息列表数据模型
class MessageItem {
  id: number = 0
  senderName: string = ''
  avatar: string = ''
  content: string = ''
  time: string = ''
  isRead: boolean = false
  isPinned: boolean = false

  constructor(
    id: number,
    senderName: string,
    avatar: string,
    content: string,
    time: string,
    isRead: boolean = false,
    isPinned: boolean = false
  ) {
    this.id = id
    this.senderName = senderName
    this.avatar = avatar
    this.content = content
    this.time = time
    this.isRead = isRead
    this.isPinned = isPinned
  }
}

@Entry
@Component
struct MessageListDemo {
  @State messageList: MessageItem[] = [
    new MessageItem(1, '张三', '👨', '你好,最近怎么样?', '10:30', false, true),
    new MessageItem(2, '李四', '👩', '明天有空一起吃饭吗?', '09:45', false),
    new MessageItem(3, '王五', '🧑', '项目进度汇报,请查收', '昨天', true),
    new MessageItem(4, '赵六', '👨‍💼', '会议改到下午三点', '昨天', true),
    new MessageItem(5, '系统通知', '📢', '您的订单已发货', '2天前', true),
    new MessageItem(6, '陈七', '👴', '周末去钓鱼吗?', '3天前', true),
  ]

  @State isEditMode: boolean = false
  @State selectedIds: number[] = []
  @State showActionSheet: boolean = false

  private isSelected(id: number): boolean {
    return this.selectedIds.indexOf(id) !== -1
  }

  private toggleSelect(id: number): void {
    const index = this.selectedIds.indexOf(id)
    if (index !== -1) {
      this.selectedIds.splice(index, 1)
    } else {
      this.selectedIds.push(id)
    }
  }

  private selectAll(): void {
    this.selectedIds = []
    for (let i = 0; i < this.messageList.length; i++) {
      this.selectedIds.push(this.messageList[i].id)
    }
  }

  private clearSelection(): void {
    this.selectedIds = []
  }

  private deleteMessages(): void {
    const newList: MessageItem[] = []
    for (let i = 0; i < this.messageList.length; i++) {
      if (!this.isSelected(this.messageList[i].id)) {
        newList.push(this.messageList[i])
      }
    }
    this.messageList = newList
    this.selectedIds = []
    if (this.messageList.length === 0) {
      this.isEditMode = false
    }
  }

  private markAsRead(): void {
    for (let i = 0; i < this.messageList.length; i++) {
      if (this.isSelected(this.messageList[i].id)) {
        this.messageList[i].isRead = true
      }
    }
    this.selectedIds = []
  }

  private pinMessages(): void {
    for (let i = 0; i < this.messageList.length; i++) {
      if (this.isSelected(this.messageList[i].id)) {
        this.messageList[i].isPinned = true
      }
    }
    this.selectedIds = []
  }

  @Builder
  private MessageItemBuilder(item: MessageItem) {
    Row() {
      if (this.isEditMode) {
        // 多选复选框
        Row() {
          if (this.isSelected(item.id)) {
            Icon('checkmark', 16)
              .fillColor(Color.White)
          }
        }
        .width(22)
        .height(22)
        .backgroundColor(this.isSelected(item.id) ? '#007DFF' : '#FFFFFF')
        .border({
          width: 2,
          color: this.isSelected(item.id) ? '#007DFF' : '#CCCCCC'
        })
        .borderRadius(6)
        .margin({ right: 12 })
        .justifyContent(FlexAlign.Center)
        .alignItems(VerticalAlign.Center)
      }

      // 头像
      Text(item.avatar)
        .fontSize(40)
        .margin({ right: 12 })

      // 消息内容
      Column() {
        Row() {
          Text(item.senderName)
            .fontSize(16)
            .fontWeight(item.isRead ? FontWeight.Normal : FontWeight.Bold)
            .fontColor('#333333')
            .layoutWeight(1)

          Text(item.time)
            .fontSize(12)
            .fontColor('#999999')
        }
        .width('100%')

        Row() {
          Text(item.content)
            .fontSize(14)
            .fontColor(item.isRead ? '#999999' : '#666666')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .layoutWeight(1)

          if (!item.isRead) {
            Circle()
              .width(8)
              .height(8)
              .fill('#FF4D4F')
              .margin({ left: 8 })
          }
        }
        .width('100%')
        .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      if (item.isPinned) {
        Text('📌')
          .fontSize(16)
          .margin({ left: 8 })
      }
    }
    .width('100%')
    .padding(12)
    .backgroundColor(this.isSelected(item.id) ? '#F0F7FF' : '#FFFFFF')
    .borderRadius(8)
  }

  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Text('消息')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        if (this.isEditMode) {
          Button() {
            Text('取消')
          }
          .fontSize(14)
          .backgroundColor(Color.Transparent)
          .fontColor('#007DFF')
          .onClick(() => {
            this.isEditMode = false
            this.selectedIds = []
          })
        } else {
          Button() {
            Text('编辑')
          }
          .fontSize(14)
          .backgroundColor(Color.Transparent)
          .fontColor('#007DFF')
          .onClick(() => {
            this.isEditMode = true
          })
        }
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 24, bottom: 16 })
      .backgroundColor(Color.White)

      // 编辑模式下的操作栏
      if (this.isEditMode) {
        Row() {
          Text('已选 ' + this.selectedIds.length + ' 项')
            .fontSize(14)
            .fontColor('#666666')

          Blank()

          Button() {
            Text('全选')
          }
          .fontSize(12)
          .backgroundColor(Color.Transparent)
          .fontColor('#007DFF')
          .margin({ right: 12 })
          .onClick(() => {
            if (this.selectedIds.length === this.messageList.length) {
              this.clearSelection()
            } else {
              this.selectAll()
            }
          })

          Button() {
            Text('清空')
          }
          .fontSize(12)
          .backgroundColor(Color.Transparent)
          .fontColor('#999999')
          .onClick(() => {
            this.clearSelection()
          })
        }
        .width('100%')
        .padding({ left: 20, right: 20, top: 8, bottom: 8 })
        .backgroundColor(Color.White)
      }

      // 消息列表
      List({ space: 2 }) {
        ForEach(this.messageList, (item: MessageItem) => {
          ListItem() {
            this.MessageItemBuilder(item)
          }
          .selectable(true)
          .selected(this.isSelected(item.id))
          .onSelect((isSelected: boolean) => {
            if (this.isEditMode) {
              this.toggleSelect(item.id)
            }
          })
        }, (item: MessageItem) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 12, right: 12 })
      .editMode(this.isEditMode)

      // 底部批量操作栏
      if (this.isEditMode && this.selectedIds.length > 0) {
        Row() {
          // 标为已读
          Button() {
            Column() {
              Text('✓')
                .fontSize(20)
              Text('已读')
                .fontSize(11)
                .margin({ top: 4 })
            }
          }
          .layoutWeight(1)
          .height(56)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.markAsRead()
          })

          // 置顶
          Button() {
            Column() {
              Text('📌')
                .fontSize(20)
              Text('置顶')
                .fontSize(11)
                .margin({ top: 4 })
            }
          }
          .layoutWeight(1)
          .height(56)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.pinMessages()
          })

          // 删除
          Button() {
            Column() {
              Text('🗑️')
                .fontSize(20)
              Text('删除')
                .fontSize(11)
                .margin({ top: 4 })
            }
          }
          .layoutWeight(1)
          .height(56)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.deleteMessages()
          })
        }
        .width('100%')
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#EEEEEE' })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

5.4 多选模式核心逻辑

添加选中项
typescript 复制代码
if (this.selectedIds.indexOf(id) === -1) {
  this.selectedIds.push(id)
}
移除选中项
typescript 复制代码
const index = this.selectedIds.indexOf(id)
if (index !== -1) {
  this.selectedIds.splice(index, 1)
}
判断是否全选
typescript 复制代码
const isAllSelected: boolean = this.selectedIds.length === this.itemList.length
全选/取消全选
typescript 复制代码
if (this.selectedIds.length === this.itemList.length) {
  // 已全选,执行取消全选
  this.selectedIds = []
} else {
  // 未全选,执行全选
  this.selectAll()
}

第六章:综合实战案例

6.1 案例概述

本案例将实现一个完整的 文件管理器 应用,综合运用单选和多选两种模式:

  • 单选模式:选择文件、进入文件夹、打开文件
  • 多选模式:批量删除、移动、复制、分享

6.2 完整代码实现

由于篇幅限制,这里提供核心代码结构和关键实现点:

步骤 1:定义数据模型
typescript 复制代码
class FileItem {
  id: number = 0
  name: string = ''
  type: string = ''  // 'folder', 'image', 'document', 'video', 'audio', 'other'
  size: string = ''
  modifiedDate: string = ''
  isFolder: boolean = false
  parentId: number = 0  // 父文件夹 ID,0 表示根目录

  constructor(
    id: number,
    name: string,
    type: string,
    size: string,
    modifiedDate: string,
    isFolder: boolean,
    parentId: number = 0
  ) {
    this.id = id
    this.name = name
    this.type = type
    this.size = size
    this.modifiedDate = modifiedDate
    this.isFolder = isFolder
    this.parentId = parentId
  }
}
步骤 2:文件类型图标映射
typescript 复制代码
function getFileIcon(type: string, isFolder: boolean): string {
  if (isFolder) {
    return '📁'
  }
  switch (type) {
    case 'image': return '🖼️'
    case 'document': return '📄'
    case 'video': return '🎬'
    case 'audio': return '🎵'
    case 'archive': return '📦'
    default: return '📎'
  }
}
步骤 3:核心状态管理
typescript 复制代码
@Entry
@Component
struct FileManager {
  // 文件列表(扁平化存储,通过 parentId 构建层级)
  @State allFiles: FileItem[] = []
  // 当前目录下的文件
  @State currentFiles: FileItem[] = []
  // 当前路径
  @State currentPath: string = '/我的文件'
  @State currentFolderId: number = 0
  
  // 模式控制
  @State isEditMode: boolean = false
  @State selectedIds: number[] = []
  
  // 视图模式
  @State viewMode: 'list' | 'grid' = 'list'
  @State sortBy: 'name' | 'date' | 'size' = 'name'
}
步骤 4:文件过滤与排序
typescript 复制代码
// 获取当前目录下的文件
private getCurrentFiles(): void {
  const filtered: FileItem[] = []
  for (let i = 0; i < this.allFiles.length; i++) {
    if (this.allFiles[i].parentId === this.currentFolderId) {
      filtered.push(this.allFiles[i])
    }
  }
  this.currentFiles = this.sortFiles(filtered)
}

// 排序文件
private sortFiles(files: FileItem[]): FileItem[] {
  const sorted = files.slice()
  
  // 文件夹始终排在前面
  sorted.sort((a: FileItem, b: FileItem) => {
    if (a.isFolder !== b.isFolder) {
      return a.isFolder ? -1 : 1
    }
    return 0
  })
  
  // 按选定的排序方式排序
  sorted.sort((a: FileItem, b: FileItem) => {
    switch (this.sortBy) {
      case 'name':
        return a.name.localeCompare(b.name)
      case 'date':
        return new Date(b.modifiedDate).getTime() - new Date(a.modifiedDate).getTime()
      case 'size':
        return this.parseSize(b.size) - this.parseSize(a.size)
      default:
        return 0
    }
  })
  
  return sorted
}

// 解析文件大小为字节数
private parseSize(size: string): number {
  const parts = size.split(' ')
  if (parts.length !== 2) return 0
  
  const value = parseFloat(parts[0])
  const unit = parts[1].toUpperCase()
  
  switch (unit) {
    case 'B': return value
    case 'KB': return value * 1024
    case 'MB': return value * 1024 * 1024
    case 'GB': return value * 1024 * 1024 * 1024
    default: return value
  }
}
步骤 5:文件操作方法
typescript 复制代码
// 进入文件夹(单选模式)
private openFolder(file: FileItem): void {
  if (file.isFolder && !this.isEditMode) {
    this.currentPath += '/' + file.name
    this.currentFolderId = file.id
    this.getCurrentFiles()
  }
}

// 返回上级目录
private goBack(): void {
  if (this.currentFolderId === 0) return
  
  // 找到父文件夹
  for (let i = 0; i < this.allFiles.length; i++) {
    if (this.allFiles[i].id === this.currentFolderId) {
      const parentId = this.allFiles[i].parentId
      
      // 更新路径
      const pathParts = this.currentPath.split('/')
      pathParts.pop()
      this.currentPath = pathParts.join('/') || '/我的文件'
      
      this.currentFolderId = parentId
      this.getCurrentFiles()
      return
    }
  }
}

// 批量删除(多选模式)
private deleteFiles(): void {
  const newAllFiles: FileItem[] = []
  const idsToDelete: number[] = []
  
  // 收集要删除的 ID(包括子文件)
  for (let i = 0; i < this.selectedIds.length; i++) {
    idsToDelete.push(this.selectedIds[i])
    this.collectChildIds(this.selectedIds[i], idsToDelete)
  }
  
  // 过滤
  for (let i = 0; i < this.allFiles.length; i++) {
    if (idsToDelete.indexOf(this.allFiles[i].id) === -1) {
      newAllFiles.push(this.allFiles[i])
    }
  }
  
  this.allFiles = newAllFiles
  this.selectedIds = []
  this.isEditMode = false
  this.getCurrentFiles()
}

// 递归收集子文件 ID
private collectChildIds(folderId: number, result: number[]): void {
  for (let i = 0; i < this.allFiles.length; i++) {
    if (this.allFiles[i].parentId === folderId) {
      result.push(this.allFiles[i].id)
      if (this.allFiles[i].isFolder) {
        this.collectChildIds(this.allFiles[i].id, result)
      }
    }
  }
}

6.3 UI 布局实现

typescript 复制代码
@Builder
private FileItemBuilder(file: FileItem) {
  Row() {
    // 多选模式下的复选框
    if (this.isEditMode) {
      Row() {
        if (this.selectedIds.indexOf(file.id) !== -1) {
          Icon('checkmark', 16)
            .fillColor(Color.White)
        }
      }
      .width(22)
      .height(22)
      .backgroundColor(
        this.selectedIds.indexOf(file.id) !== -1 ? '#007DFF' : '#FFFFFF'
      )
      .border({
        width: 2,
        color: this.selectedIds.indexOf(file.id) !== -1 ? '#007DFF' : '#CCCCCC'
      })
      .borderRadius(4)
      .margin({ right: 12 })
    }

    // 文件图标
    Text(getFileIcon(file.type, file.isFolder))
      .fontSize(36)
      .margin({ right: 12 })

    // 文件信息
    Column() {
      Text(file.name)
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
        .fontColor('#333333')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Row() {
        Text(file.isFolder ? '文件夹' : file.size)
          .fontSize(12)
          .fontColor('#999999')
          .layoutWeight(1)

        Text(file.modifiedDate)
          .fontSize(12)
          .fontColor('#CCCCCC')
      }
      .width('100%')
      .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    // 文件夹箭头
    if (file.isFolder && !this.isEditMode) {
      Icon('chevron-right', 20)
        .fillColor('#CCCCCC')
    }
  }
  .width('100%')
  .padding(12)
  .backgroundColor(
    this.selectedIds.indexOf(file.id) !== -1 ? '#F0F7FF' : '#FFFFFF'
  )
  .borderRadius(8)
}

build() {
  Column() {
    // 顶部导航栏
    Row() {
      if (this.currentFolderId !== 0 && !this.isEditMode) {
        Button() {
          Icon('arrow-left', 20)
        }
        .backgroundColor(Color.Transparent)
        .onClick(() => {
          this.goBack()
        })
      } else {
        Blank()
      }

      Column() {
        Text(this.currentPath)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(this.currentFiles.length + ' 项')
          .fontSize(12)
          .fontColor('#999999')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 12 })

      if (this.isEditMode) {
        Button() {
          Text('取消')
        }
        .fontSize(14)
        .backgroundColor(Color.Transparent)
        .fontColor('#007DFF')
        .onClick(() => {
          this.isEditMode = false
          this.selectedIds = []
        })
      } else {
        Button() {
          Text('选择')
        }
        .fontSize(14)
        .backgroundColor(Color.Transparent)
        .fontColor('#007DFF')
        .onClick(() => {
          this.isEditMode = true
        })
      }
    }
    .width('100%')
    .padding({ left: 12, right: 12, top: 16, bottom: 12 })
    .backgroundColor(Color.White)

    // 文件列表
    List({ space: 8 }) {
      ForEach(this.currentFiles, (file: FileItem) => {
        ListItem() {
          this.FileItemBuilder(file)
        }
        .selectable(true)
        .selected(this.selectedIds.indexOf(file.id) !== -1)
        .onSelect((isSelected: boolean) => {
          if (this.isEditMode) {
            if (isSelected) {
              this.selectedIds.push(file.id)
            } else {
              const index = this.selectedIds.indexOf(file.id)
              if (index !== -1) {
                this.selectedIds.splice(index, 1)
              }
            }
          } else if (isSelected && file.isFolder) {
            this.openFolder(file)
          }
        })
      }, (file: FileItem) => file.id.toString())
    }
    .width('100%')
    .layoutWeight(1)
    .padding(12)
    .editMode(this.isEditMode)

    // 底部操作栏(多选模式)
    if (this.isEditMode && this.selectedIds.length > 0) {
      Row() {
        Button() {
          Text('移动')
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor('#F5F5F5')
        .fontColor('#666666')
        .borderRadius(8)
        .margin({ right: 8 })

        Button() {
          Text('复制')
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor('#F5F5F5')
        .fontColor('#666666')
        .borderRadius(8)
        .margin({ right: 8 })

        Button() {
          Text('分享')
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor('#F5F5F5')
        .fontColor('#666666')
        .borderRadius(8)
        .margin({ right: 8 })

        Button() {
          Text('删除')
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor('#FF4D4F')
        .fontColor(Color.White)
        .borderRadius(8)
        .onClick(() => {
          this.deleteFiles()
        })
      }
      .width('100%')
      .padding(12)
      .backgroundColor(Color.White)
      .border({ width: 1, color: '#EEEEEE' })
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F5F5F5')
}

第七章:进阶技巧与性能优化

7.1 使用 LazyForEach 优化性能

当列表数据量较大(超过 50 项)时,推荐使用 LazyForEach 替代 ForEach

typescript 复制代码
// 1. 创建数据源类
class FileDataSource implements IDataSource {
  private dataList: FileItem[] = []
  private listener: DataChangeListener | null = null

  constructor(dataList: FileItem[]) {
    this.dataList = dataList
  }

  totalCount(): number {
    return this.dataList.length
  }

  getData(index: number): FileItem {
    return this.dataList[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = null
  }

  addData(index: number, data: FileItem): void {
    this.dataList.splice(index, 0, data)
    this.listener?.onDataReloaded()
  }

  removeData(index: number): void {
    this.dataList.splice(index, 1)
    this.listener?.onDataReloaded()
  }

  reloadData(dataList: FileItem[]): void {
    this.dataList = dataList
    this.listener?.onDataReloaded()
  }
}

// 2. 在组件中使用
@Component
struct LazyListDemo {
  private dataSource: FileDataSource = new FileDataSource([])

  aboutToAppear() {
    // 初始化数据
    const initialData: FileItem[] = []
    for (let i = 0; i < 1000; i++) {
      initialData.push(new FileItem(i, '文件 ' + i, 'document', '1.2 MB', '2024-01-01', false))
    }
    this.dataSource.reloadData(initialData)
  }

  build() {
    List() {
      LazyForEach(this.dataSource, (item: FileItem) => {
        ListItem() {
          Text(item.name)
            .fontSize(16)
        }
      }, (item: FileItem) => item.id.toString())
    }
    .width('100%')
    .height('100%')
  }
}

7.2 长列表缓存优化

typescript 复制代码
List() {
  // ...
}
.cachedCount(5)  // 预加载前后 5 项

7.3 防抖处理

避免频繁触发选中回调导致性能问题:

typescript 复制代码
private debounceTimer: number = 0

private handleSelectWithDebounce(id: number, isSelected: boolean): void {
  clearTimeout(this.debounceTimer)
  this.debounceTimer = setTimeout(() => {
    this.handleSelect(id, isSelected)
  }, 100) as unknown as number
}

7.4 虚拟滚动

对于超大数据集(10,000+ 项),使用虚拟滚动技术:

typescript 复制代码
List() {
  LazyForEach(this.virtualDataSource, (item) => {
    // 仅渲染可见项
  }, (item) => item.id.toString())
}
.virtualScroll(true)  // 开启虚拟滚动

7.5 动画效果

选中动画
typescript 复制代码
ListItem() {
  // ...
}
.onSelect((isSelected: boolean) => {
  // 使用 animateTo 添加过渡动画
  animateTo({ duration: 200 }, () => {
    this.handleSelect(id, isSelected)
  })
})
列表更新动画
typescript 复制代码
List() {
  // ...
}
.chainAnimation(true)  // 链式动画

7.6 数据持久化

使用 Preferences 保存选择状态
typescript 复制代码
import preferences from '@ohos.data.preferences'

private async saveSelection(): Promise<void> {
  try {
    const context = getContext(this)
    const prefs = await preferences.getPreferences(context, 'list_prefs')
    await prefs.put('selectedIds', this.selectedIds)
    await prefs.put('selectedId', this.selectedId)
    await prefs.flush()
  } catch (e) {
    console.error('保存失败:', e)
  }
}

private async loadSelection(): Promise<void> {
  try {
    const context = getContext(this)
    const prefs = await preferences.getPreferences(context, 'list_prefs')
    this.selectedIds = await prefs.get('selectedIds', []) as number[]
    this.selectedId = await prefs.get('selectedId', 0) as number
  } catch (e) {
    console.error('加载失败:', e)
  }
}

第八章:最佳实践与避坑指南

8.1 架构设计建议

1. 数据与 UI 分离
typescript 复制代码
// ✅ 推荐:数据模型独立,UI 只负责展示
class UserData {
  id: number
  name: string
  email: string
}

// ❌ 不推荐:在数据模型中混合 UI 状态
class UserData {
  id: number
  name: string
  email: string
  isSelected: boolean  // UI 状态不应在数据模型中
}
2. 状态集中管理
typescript 复制代码
// ✅ 推荐:集中管理所有状态
@State listState: {
  isEditMode: boolean
  selectedId: number
  selectedIds: number[]
  sortBy: string
} = { ... }

// ❌ 不推荐:分散定义多个状态
@State isEditMode: boolean = false
@State selectedId: number = 0
@State selectedIds: number[] = []
@State sortBy: string = 'name'
3. 业务逻辑提取
typescript 复制代码
// ✅ 推荐:使用 Service 类封装业务逻辑
class ListService {
  static selectItem(
    currentSelected: number[],
    id: number,
    isSelected: boolean
  ): number[] {
    const newList = currentSelected.slice()
    if (isSelected && newList.indexOf(id) === -1) {
      newList.push(id)
    } else if (!isSelected) {
      const index = newList.indexOf(id)
      if (index !== -1) {
        newList.splice(index, 1)
      }
    }
    return newList
  }
}

8.2 常见错误与解决方案

错误 1:选中状态不更新

问题 :调用 .onSelect() 后 UI 没有响应。

原因 :ListItem 的 .selected() 属性是受控的,需要在回调中手动更新状态。

解决方案

typescript 复制代码
// ❌ 错误:只设置了 selected 属性
ListItem() {
  // ...
}
.selected(this.isSelected)
// 缺少 onSelect 回调来更新状态

// ✅ 正确:同时设置 selected 和 onSelect
ListItem() {
  // ...
}
.selected(this.isSelected)
.onSelect((isSelected: boolean) => {
  this.isSelected = isSelected  // 必须更新状态
})
错误 2:多选状态混乱

问题:选中多个项后,部分项状态不正确。

原因 :在异步操作中直接修改 @State 数组。

解决方案

typescript 复制代码
// ❌ 错误:直接 push 到 @State 数组
@State selectedIds: number[] = []

this.selectedIds.push(id)  // 直接修改可能导致状态不一致

// ✅ 正确:创建新数组后重新赋值
const newIds = this.selectedIds.slice()  // 复制数组
if (newIds.indexOf(id) === -1) {
  newIds.push(id)
}
this.selectedIds = newIds  // 重新赋值触发状态更新
错误 3:UI 无响应

问题:修改了状态变量,但界面没有更新。

原因 :使用 if-else 条件时,状态更新被跳过。

解决方案

typescript 复制代码
// ❌ 错误:条件不满足时不更新
if (this.isEditMode) {
  this.selectedIds.push(id)
}

// ✅ 正确:确保所有路径都能触发更新
const newIds = this.selectedIds.slice()
if (this.isEditMode) {
  if (newIds.indexOf(id) === -1) {
    newIds.push(id)
  }
}
this.selectedIds = newIds

8.3 性能优化建议

1. 避免不必要的重渲染
typescript 复制代码
// ❌ 不推荐:每次渲染都创建新对象
@Builder
private ItemBuilder(item: ItemData) {
  Text(item.name)
    .fontSize(16)
    .fontColor({ red: 255, green: 0, blue: 0 })  // 每次创建新对象
}

// ✅ 推荐:提取常量
const SELECTED_COLOR: string = '#007DFF'
const NORMAL_COLOR: string = '#333333'

@Builder
private ItemBuilder(item: ItemData) {
  Text(item.name)
    .fontSize(16)
    .fontColor(this.isSelected(item.id) ? SELECTED_COLOR : NORMAL_COLOR)
}
2. 使用 key 优化列表
typescript 复制代码
// ✅ 正确:提供稳定且唯一的 key
ForEach(this.itemList, (item: ItemData) => {
  ListItem() {
    // ...
  }
}, (item: ItemData) => item.id.toString())  // 使用数据的唯一标识

// ❌ 错误:使用索引作为 key
ForEach(this.itemList, (item: ItemData, index: number) => {
  ListItem() {
    // ...
  }
}, (item: ItemData, index: number) => index.toString())  // 列表变动时 key 会变
3. 避免在渲染方法中执行重计算
typescript 复制代码
// ❌ 不推荐:每次渲染都计算
@Builder
private ItemBuilder(item: ItemData) {
  const formattedSize = this.formatFileSize(item.size)  // 重计算
  Text(formattedSize)
}

// ✅ 推荐:预计算或缓存结果
@State formattedSizes: Map<number, string> = new Map()

aboutToAppear() {
  // 预先计算所有格式化后的值
  for (let item of this.itemList) {
    this.formattedSizes.set(item.id, this.formatFileSize(item.size))
  }
}

@Builder
private ItemBuilder(item: ItemData) {
  const formattedSize = this.formattedSizes.get(item.id) ?? ''
  Text(formattedSize)
}

8.4 无障碍设计

1. 添加语义化标签
typescript 复制代码
// ✅ 添加 accessibilityLabel 帮助残障用户
ListItem() {
  // ...
}
.accessibilityLabel(item.name + ',' + item.description)
.accessibilityRole('button')
2. 确保足够的点击区域
typescript 复制代码
// ✅ 确保最小点击区域为 48x48
ListItem() {
  Row() {
    // 内容
  }
  .width('100%')
  .height(56)  // 至少 48
}
3. 支持键盘导航
typescript 复制代码
// 在支持键盘的设备上(如平板),确保焦点顺序正确
List() {
  ForEach(this.items, (item: ItemData, index: number) => {
    ListItem() {
      // ...
    }
    .tabIndex(index + 1)  // 设置焦点顺序
  }, (item: ItemData) => item.id.toString())
}

8.5 深色模式适配

1. 使用主题色资源
typescript 复制代码
// ✅ 使用 $r 引用主题色
Text(item.name)
  .fontColor($r('app.color.text_primary'))  // 自动适配深浅色模式

// ❌ 不要硬编码颜色
Text(item.name)
  .fontColor('#333333')  // 在深色模式下可能看不清
2. 提供明暗两套样式
typescript 复制代码
// 在 resources/base/element/color.json 中定义
{
  "name": "selected_background",
  "value": "#E8F4FF"
}

// 在 resources/dark/element/color.json 中定义
{
  "name": "selected_background",
  "value": "#1A3A5C"
}

// 使用
.backgroundColor($r('app.color.selected_background'))

第九章:总结与展望

9.1 核心要点回顾

本文详细介绍了 HarmonyOS NEXT (API 24) 中 List 组件的单选与多选功能,主要内容包括:

  1. 核心 API 掌握

    • .editMode() - 控制编辑模式
    • .selected() - 受控选中状态
    • .onSelect() - 选中事件回调
    • .selectable() - 是否允许选中
  2. 两种选择模式

    • 单选模式:适用于唯一选择场景,如支付方式、语言设置
    • 多选模式:适用于批量操作场景,如删除、移动、分享
  3. 状态管理技巧

    • 使用 @State 管理选中状态
    • 单选用单个 ID,多选用 ID 数组
    • 善用辅助方法简化逻辑
  4. 性能优化策略

    • 大数据量使用 LazyForEach
    • 避免不必要的重渲染
    • 提供稳定的 key 值
  5. 用户体验设计

    • 清晰的视觉反馈
    • 直观的操作流程
    • 深色模式适配

9.2 进阶学习路径

1. 学习更多 List 特性
  • 分组列表 :使用 ListItemGroup 实现分区
  • 粘性标题 :使用 .sticky() 固定表头
  • 侧滑操作 :使用 .swipeAction() 实现滑动删除
  • 拖拽排序 :使用 .onMove() 实现重排
2. 状态管理进阶
  • @Observed / @ObjectLink:复杂对象状态管理
  • 状态提升:将状态提升到父组件
  • 全局状态:使用 AppStorage 或 LocalStorage
3. 动画与手势
  • 属性动画:选中状态过渡效果
  • 手势识别:长按进入编辑模式
  • 转场动画:列表增删动画

9.3 实际应用场景

场景 1:设置页面
typescript 复制代码
// 单选示例:主题设置
@State theme: string = 'light'

Row() {
  Text('主题模式')
    .fontSize(16)
  Blank()
  Text(this.theme === 'light' ? '浅色' : '深色')
    .fontColor('#007DFF')
  Icon('chevron-right', 20)
}
.onClick(() => {
  // 打开选择弹窗
})
场景 2:邮件客户端
typescript 复制代码
// 多选示例:批量操作
@State selectedEmailIds: number[] = []

// 长按进入编辑模式
ListItem() {
  // 邮件内容
}
.gesture(
  LongPressGesture()
    .onAction(() => {
      this.isEditMode = true
      this.selectedEmailIds.push(email.id)
    })
)
场景 3:购物车
typescript 复制代码
// 混合示例:单选规格 + 多选删除
// 规格选择(单选)
@State selectedSpecId: number = 1

// 商品管理(多选)
@State selectedProductIds: number[] = []

9.4 未来发展方向

随着 HarmonyOS 的不断发展,List 组件也在持续进化:

  1. 更好的跨设备体验

    • 手机/平板/桌面自适应
    • 折叠屏适配
    • 多窗口支持
  2. 更强的性能

    • 更高效的渲染引擎
    • 更智能的内存管理
    • 更好的启动速度
  3. 更丰富的交互

    • 3D Touch 支持
    • 笔势输入优化
    • 语音操作集成
  4. 更智能的 UI

    • AI 辅助布局
    • 智能滚动
    • 自动分类

9.5 学习资源推荐

官方资源
社区资源
  • HarmonyOS 开发者论坛
  • 掘金/HarmonyOS 专区
  • CSDN/HarmonyOS 分类
  • GitHub/HarmonyOS 项目
实践建议
  1. 动手实践:本文所有代码都可以直接运行调试
  2. 修改尝试:修改颜色、样式、逻辑等参数观察效果
  3. 功能扩展:在现有基础上添加新功能
  4. 性能测试:使用 DevEco Studio 的性能分析工具

附录:完整 API 参考

List 组件属性

属性 类型 默认值 说明
.editMode(editing) boolean false 是否进入编辑模式
.multiSelectable(multiSelect) boolean false 是否开启多选
.divider(options) DividerOptions - 分隔线样式
.scrollBar(barState) BarState Auto 滚动条状态
.scrollBarColor(color) ResourceColor - 滚动条颜色
.scrollBarWidth(width) Length - 滚动条宽度
.cachedCount(count) number 1 预缓存数量
.lanes(lanes) number 1 显示列数
.alignListItem(align) ListItemAlign Center 列表项对齐方式

ListItem 组件属性

属性 类型 默认值 说明
.selectable(selectable) boolean false 是否可选中
.selected(selected) boolean false 是否被选中
.selectedState(state) SelectedState - 选中态样式
.onSelect(callback) (isSelected: boolean) => void - 选中回调
.swipeAction(options) SwipeActionOptions - 侧滑操作
.editable(editable) boolean false 是否可编辑
.editMode(mode) EditMode None 编辑模式

事件回调

事件 参数 说明
.onScroll(callback) (scrollOffset: number, scrollState: ScrollState) => void 滚动事件
.onReachStart(callback) () => void 滚动到顶部
.onReachEnd(callback) () => void 滚动到底部
.onScrollIndex(callback) (start: number, end: number) => void 可见项索引变化
.onSelect(callback) (isSelected: boolean) => void 选中状态变化

结语

感谢您阅读完本文!List 的单选与多选功能看似简单,但背后涉及的知识体系非常丰富。希望本文能够帮助您更好地理解和掌握 HarmonyOS NEXT 的列表组件。

在实际开发中,请根据具体需求灵活运用这些知识,不断实践和探索。祝您开发顺利,创造出优秀的 HarmonyOS 应用!

相关推荐
胡琦博客2 小时前
HarmonyOS 智能工具箱(三):语音交互工具
交互·xcode·harmonyos
国服第二切图仔2 小时前
HarmonyOS7新特性之沉浸光感:新一代材质渲染技术解析
harmonyos
●VON2 小时前
鸿蒙 PC Markdown 编辑器链接补全与离线校验:中文路径、标题锚点与授权边界
华为·编辑器·harmonyos·鸿蒙
kiros_wang2 小时前
鸿蒙性能优化全维度实战(启动速度 + 内存治理 + 帧率稳定 + 包体积瘦身)
华为·性能优化·harmonyos
星恒随风2 小时前
C++ STL 详解:list 的使用、迭代器失效、模拟实现与 vector 对比
开发语言·数据结构·c++·笔记·学习·list
●VON2 小时前
鸿蒙 PC Markdown 编辑器快速打开:模糊排序、最近权重与纯键盘路径
华为·编辑器·计算机外设·harmonyos·鸿蒙
●VON2 小时前
鸿蒙 PC Markdown 编辑器命令面板:把桌面高频操作收束到一个入口
服务器·华为·编辑器·harmonyos·鸿蒙
爱写代码的森2 小时前
鸿蒙三方库 | harmony-utils之FileUtil文件拷贝与移动详解
华为·harmonyos·鸿蒙·huawei
xd1855785553 小时前
落枕缓解指南 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙