本章将介绍如何在 ArkUI 中使用 Scroll(滚动容器)组件和 List(列表容器)组件来构建动态视图。当页面内容超出屏幕范围时,Scroll 组件允许用户通过滑动操作查看更多内容,在 HarmonyOS 中有很多相关的案例,比如应用商店的首页、新闻应用中的信息流列表等等。
List 组件则与 ListItem 子组件配合,提供结构化的项目样式,供开发者构建列表。电商应用中的商品列表页面,是 List 组件最常见的应用场景之一。
2.1 使用滚动组件
创建一个名为 MyScroll 的新的 HarmonyOS 项目,并打开工程开发面板。
使用 private 关键字声明一个私有的字符串数组来存储图标资源名称,代码如下:
private symbols :string[] = ['calendar','clock','worldclock','camera','capture_smiles','livephoto','picture','paintpalette','doc_text','music','ellipsis_message','mic_circle','icloud','wifi','bolt_filled_on_circle','crop_rotate']
声明一个Scroller 类型的状态变量作为控制器来管理 Scroll 组件,该控制器用于与可滚动组件进行绑定,代码如下:
scroller: Scroller = new Scroller()
创建一个子组件SymbolItemView,用于渲染一个包含图标和文本的方块视图,并通过 symbol 参数动态设置图标和文本内容,代码如下:
//第2章/Index.ets
@Component
struct SymbolItemView {
@Prop symbol: string
build() {
Flex({
direction: FlexDirection.Column,
justifyContent: FlexAlign.Center,
alignItems: ItemAlign.Center
}) {
SymbolGlyph($r(`sys.symbol.${this.symbol}`))
.fontSize(23)
Text(this.symbol)
.fontSize(14)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({top:10})
}
.width(140)
.height(80)
.backgroundColor('#F5F5F5')
.border({ radius: 8 })
}
}
symbol 参数通过@Prop 装饰器进行定义,使其可以从父组件接收参数。Flex 组件用于构建弹性布局,通过设置direction 参数来控制组件内元素的排列方向,通过设置justifyContent 和alignItems 来控制组件内元素的对齐方向。Flex 组件的闭包中,使用SymbolGlyph 组件和Text 组件来显示方块视图的图标和文本。
回到 Index 视图中,将 build()函数中的主体内容替换为 Scroll 组件和Row 组件,在 Scroll 组件中绑定控制器参数scroller,来实现页面的滚动逻辑。并为Scroll 组件添加scrollable (滚动方向)修饰器、scrollBar(滚动条状态)修饰器,来设置滚动视图的滚动方向和滚动条的显示状态。Row 组件中使用ForEach()函数来遍历symbols 数组的数据来创建视图,并将遍历的数据赋值给SymbolItemView 组件,代码如下:
build() {
Scroll(this.scroller) {
Row({space:10}) {
ForEach(this.symbols, (item: string) => {
SymbolItemView({symbol:item})
})
}
.margin({left:10})
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.On)
.height('100%')
}
Row 组件闭包中,当使用ForEach()函数循环处理symbols 数组的数据时,每次循环都会将当前元素 item 作为参数传递给 SymbolItemView 子组件。 SymbolItemView 子组件在接收到 item 后,将会显示对应的符号图标和文本。如图 2-1 所示。
图 2-1 Scroll 组件实现横向滚动视图
2.2 使用列表组件
List 组件与Scroll 组件功能类似,用于构建包含一系列相同宽度的列表项,适合连续、多行呈现同类数据。
创建一个名为 MyList 的新的 HarmonyOS 项目,并打开工程开发面板。
定义一个名为NoteModel 的接口,用于描述笔记对象的结构和参数,每个笔记项都有一个唯一的标识符 id、笔记内容content、创建时间createdAt 和背景颜色bgColor 参数,代码如下:
//第2章/Index.ets
interface NoteModel {
id: number
content: string
createdAt: Date
bgColor: string
}
通过@State 装饰器声明一个NoteModel 类型的数组notes 用于存储初始的笔记内容,并创建 5 条示例笔记,代码如下:
//第2章/Index.ets
@State notes: NoteModel[] = [
{
id: 1,
content: 'Discuss team progress and tasks for the day.',
createdAt: new Date('2025-03-01T09:00:00'),
bgColor:'#CDFDC8'
},
{
id: 2,
content: 'Review pull requests and provide feedback.',
createdAt: new Date('2025-03-01T11:00:00'),
bgColor:'#B1EDFD'
},
{
id: 3,
content: 'Take a break and enjoy a meal.',
createdAt: new Date('2025-03-01T12:30:00'),
bgColor:'#F9DDA5'
},
{
id: 4,
content: 'Discuss project updates and next steps.',
createdAt: new Date('2025-03-01T15:00:00'),
bgColor:'#F6C6C4'
},
{
id: 5,
content: 'Summarize completed tasks and plan for tomorrow.',
createdAt: new Date('2025-03-01T18:00:00'),
bgColor:'#ACC8AB'
},
]
notes 数组中包含了多个NoteModel 类型的对象, 由于每个对象的 createdAt 参数类型为Date 类型,因此在赋值时使用new Date()函数来对日期进行初始化,时间格式为 ISO 8601 标准。
创建一个子组件NoteItemView,用于渲染单个笔记项视图,并通过 noteItem 参数动态设置图标和文本内容,代码如下:
//第2章/Index.ets
@Component
struct NoteItemView {
@Prop noteItem: NoteModel
build() {
Column({ space: 20 }) {
Text(this.noteItem.content)
.fontSize(17)
.fontColor('#333333')
.maxLines(4)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Blank()
Text(this.noteItem.createdAt.toLocaleString())
.fontSize(14)
.fontColor('#666666')
}
.alignItems(HorizontalAlign.Start)
.padding(20)
.width('100%')
.height(160)
.backgroundColor(this.noteItem.bgColor)
.borderRadius(8)
}
}
noteItem 参数类型为NoteModel,表示传入的参数可以从NoteModel 接口中获取。在build()函数中构建 UI 时,Text 组件可以NoteModel 接口中获取其属性的数据来显示内容。由于Text 组件只能接收 string 类型的数据,因此在显示noteItem.createdAt 的值时,需要使用toLocaleString()方法对日期进行本地时间的格式化处理。
回到 Index 视图,在Index 视图的build()函数中使用List 组件和ForEach()函数根据notes 数组的数据来渲染笔记项,生成笔记列表。代码如下:
//第2章/Index.ets
@Entry
@Component
struct Index {
@State notes: NoteModel[] = [...]
build() {
List({space:10}){
ForEach(this.notes,(item:NoteModel) => {
ListItem(){
NoteItemView({noteItem:item})
.margin({ left: 10, right: 10 })
}
})
}
.listDirection(Axis.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
}
与Scroll 组件不同的是,List 组件的子组件必须为ListItem 子组件,因此需要将NoteItemView 组件作为ListItem 子组件的孙组件进行使用。
listDirection 修饰器用于设置 List 组件的排列方向,scrollBar 修饰器用于设置 List 组件的滚动条状态。List 组件作容器使用时,还需要开发者设置容器的高度,否则可能会导致滚动行为异常或布局问题。
NoteItemView 组件在接收到NoteModel 类型的item 参数后,将其传递给noteItem 参数,用于显示笔记项的内容。如图 2-2 所示。
图 2-2 List 组件实现垂直列表视图
2.3 实现列表的常见功能
新增、删除和查询功能是数据操作的高频功能,新增功能允许用户创建新的数据项,例如在笔记应用中添加一条新的笔记,或者在任务管理应用中创建一个新的任务。删除功能为每个数据项提供一个清晰的删除按钮或操作选项,让用户能够轻松移除不再需要的数据项。查询功能允许用户根据关键字搜索数据项,从而快速定位想要操作的数据。
2.3.1 实现新增功能
创建一个名为 ListManager 的新的 HarmonyOS 项目,并打开工程开发面板。
使用@State 装饰器声明一个 string 类型的数组fruits,并初始化一些默认值,代码如下:
@State fruits: string[] = ['Apple', 'Banana', 'Orange','Watermelon', 'Cherry']
使用自定义函数装饰器@Builder 构建一个列表视图listView 和输入框视图addItemInputView,并通过Navigation路由导航组件作为根容器进行页面排版,代码如下:
//第2章/Index.ets
@Entry
@Component
struct Index {
@State fruits: string[] = ['Apple', 'Banana', 'Orange','Watermelon', 'Cherry']
@State inputText: string = ''
build() {
Navigation() {
Column() {
this.listView()
Blank()
this.addItemInputView()
}
.height('100%')
}
.title('Fruits')
}
@Builder listView() {
List({space:10}) {
ForEach(this.fruits,(item:string)=>{
ListItem(){
Text(item)
.padding(20)
.width('100%')
.backgroundColor('#F5F5F5')
.border({ radius: 8 })
}
.margin({left:10,right:10})
})
}
.width('100%')
.height('80%')
}
@Builder addItemInputView() {
Row({space:10}) {
TextInput({ text: this.inputText, placeholder: '请输入' })
.width('80%')
.onChange((value: string) => {
this.inputText = value
})
Button('+', { type: ButtonType.Capsule, stateEffect: true })
.onClick(() => {
})
}
}
}
@Builder 装饰器与@Component 装饰器功能类似,用于构建自定义组件的对象实例。@Component 装饰器常用于构建一个可复用的自定义子组件,该子组件经常被用于动态渲染数据和实现复杂的交互逻辑。而@Builder 装饰器则更适用于简化复杂对象的创建过程,使代码更加简洁和易维护。
listView()函数中,使用List 组件、ForEach()函数、ListItem 子组件和Text 组件来循环渲染列表项来得到一个列表视图。addItemInputView()函数中,使用Row 组件、TextInput 组件、Button 组件来构建一个带+符号按钮的输入框视图。
build()函数中,通过 Navigation 组件、Column 组件和title 修饰器,并调用listView()函数和addItemInputView()函数来显示一个带导航标题的首页 UI。
打开预览器,可以看到列表页显示效果,如图 2-3 所示。
图 2-3 列表页
创建 addItem()函数来实现添加数据的逻辑,并在addItemInputView()函数中 Button 组件的点击事件中调用该方法,代码如下:
//第2章/Index.ets
@Entry
@Component
struct Index {
...
build() {...}
@Builder listView() {...}
@Builder addItemInputView() {
Row({space:10}) {
...
Button('+', { type: ButtonType.Capsule, stateEffect: true })
.onClick(() => {
this.addItem()
})
}
}
addItem() {
if (this.inputText.trim() !== '') {
this.fruits.push(this.inputText)
this.inputText = ''
}
}
}
addItem()函数用于实现添加内容到fruits 数组的功能,通过 if 语句对inputText 参数值进行判断,当inputText 的值不为空时,调用push()函数将inputText 的值添加到fruits 数组中,添加新数据后通过赋值的方式清空inputText 的值。
在预览器的输入框输入Strawberry,点击+按钮,查看数据添加交互效果,如图 2-4 所示。
图 2-4 新增数据效果预览
2.3.2 实现侧滑功能
列表交互的左右滑动功能允许用户对单条数据项进行复杂操作,比如删除操作。首先,开发者需要创建数据项滑动时的操作组件,代码如下:
//第2章/Index.ets
@Builder itemEnd(item:string,index: number) {
Button({ type: ButtonType.Circle }) {
SymbolGlyph($r('sys.symbol.trash'))
.fontSize(23)
.fontColor([Color.White])
}
.size({width:40,height:40})
.backgroundColor(Color.Red)
.margin({left:10,right:10})
.onClick(() => {
})
}
itemEnd 组件用于显示一个“删除”按钮,使用Button 组件显示一个基础按钮组件,通过SymbolGlyph 组件来显示“删除”按钮的 icon。itemEnd 组件预先声明需要操作的数据项的名称item 和数据项索引index,便于实现后续删除的操作。
在listView 组件中,为ListItem 子组件添加swipeAction 修饰器,用于实现滑动操作功能,代码如下:
//第2章/Index.ets
@Builder listView() {
List({space:10}) {
ForEach(this.filteredFruits(),(item:string,index:number)=>{
ListItem(){...}
.margin({left:10,right:10})
.swipeAction({
end: {
builder: () => { this.itemEnd(item,index) },
}
})
})
}
.width('100%')
.height('80%')
}
swipeAction 修饰器的参数中,start 参数用于设置列表数据项在右滑时的响应事件,end 参数用于设置列表数据项在左滑时的响应事件。当前在 end 参数的响应事件中使用builder 函数构建组件的实例,该实例指向itemEnd 组件。
在预览器中,向右拖拽单条数据项,预览侧滑效果,如图 2-5 所示。
图 2-5 侧滑交互预览
2.3.3 实现删除功能
和addItem()方法类型,创建deleteItem()函数来实现删除数据的逻辑,代码如下:
//第2章/Index.ets
deleteItem(index: number) {
if (index != undefined) {
this.fruits.splice(index, 1)
}
}
deleteItem 函数中传入被删除数据的索引index,使用 if 语句对index 的值进行非空判断,当index 不为空时,调用splice()函数来根据索引删除fruits 数组中的指定元素。
为 itemEnd 组件添加点击事件,为避免删除操作的误操作,使用AlertDialog.show()方法来实现警告弹窗,在警告弹窗点击主按钮时调用deleteItem 函数来删除数据,代码如下:
//第2章/Index.ets
@Builder itemEnd(item:string,index: number) {
Button({ type: ButtonType.Circle }) {...}
.size({width:40,height:40})
.backgroundColor(Color.Red)
.margin({left:10,right:10})
.onClick(() => {
AlertDialog.show({
title: 'Confirm Deletion',
message: `Are you sure you want to delete ${item} ? `,
confirm: {
value: 'Delete',
action: () => {
this.deleteItem(index)
}
}
})
})
}
AlertDialog 组件用于显示一个警告弹窗,show()方法用于定义警告弹窗并弹出。在AlertDialog 组件中定义弹窗的标题和描述内容,其中message 的值使用模板字符串(``)将 item 的值插入到描述内容中。
通过confirm 参数设置警告弹窗的按钮文字和回调事件,在AlertDialog的confirm回调中,调用deleteItem方法时传递当前点击的笔记的索引,如此deleteItem方法就可以根据索引值来删除指定笔记数据。
在预览器中侧滑 Banana 笔记项,查看弹窗弹出与数据删除效果,如图 2-6 所示。
图 2-6 删除数据效果预览
2.2.4 实现搜索功能
搜索功能较为复杂,首先创建一个搜索框组件作为搜索交互的 UI,代码如下:
//第2章/Index.ets
@Entry
@Component
struct Index {
...
@State searchText: string = ''
build() {
Navigation() {
Column() {
this.searchInputView()
this.listView()
Blank()
this.addItemInputView()
}
.height('100%')
}
.title('Fruits')
}
@Builder searchInputView() {
TextInput({ text: this.searchText, placeholder: 'Search...' })
.width('95%')
.margin({bottom:10})
.onChange((value: string) => {
this.searchText = value
})
}
...
}
通过@State声明searchText 参数用于绑定TextInput 组件中输入的值,使用@Builder 装饰器构建自定义函数searchInputView(),用于显示一个搜索框。在build()函数中的Column 组件中调用searchInputView()函数,将其排列在listView()前。
创建 filteredFruits()函数来实现搜索数据的逻辑,代码如下:
//第2章/Index.ets
filteredFruits(): string[] {
if (!this.searchText.trim()) {
return this.fruits
}
return this.fruits.filter(item => item.toLowerCase().includes(this.searchText.toLowerCase()))
}
filteredFruits()函数用于根据用户输入的搜索关键字来过滤fruits 数组的数据,并返回一个过滤后的数组。filteredFruits()函数通过 if 语句判断用户输入的搜索关键字是否为空,当用户输入的值为空时,则返回原始的fruits 数组的数据。当用户输入值不为空时,则使用filter()函数遍历fruits数组中的每个元素,通过元素名称 item 和searchText 的值进行一一匹配,将匹配的结果返回一个新的数组。
其中toLowerCase()方法的作用是将item 和searchText 的值都转换为小写,来忽略大小写之间的差异。
将filteredFruits()函数替换 List 组件中ForEach()函数遍历的数组fruits,代码如下:
List({space:10}) {
ForEach(this.filteredFruits(),(item:string,index:number)=>{...}
}
List 组件使用filteredFruits()函数返回的数据作为数据源后,在预览器中,开发者可以尝试在搜索框中输入关键字,来预览搜索功能的交互效果,如图 2-7 所示。
图 2-7 搜索数据效果预览
2.2.5 实现拖拽排序功能
拖拽排序允许用户通过长按拖拽数据项的方式来改变数据项的排列顺序,ArkUI 为 ForEach()函数提供了onMove 修饰器,当ForEach在List组件下使用时,ForEach每次迭代都会生成一个ListItem,onMove 修饰器可以使ListItem 实现拖拽动画和更新数据索引,从而实现拖拽排序功能。
为listView 组件中的ForEach()函数添加onMove 修饰器,并实现更新索引逻辑,代码如下:
@Builder listView() {
List({space:10}) {
ForEach(this.filteredFruits(),(item:string,index:number)=>{...})
.onMove((from:number, to:number) => {
let fruit = this.fruits.splice(from, 1);
this.fruits.splice(to, 0, fruit[0])
})
}
.width('100%')
.height('80%')
}
在onMove事件中,定义数据移动原始索引号和目标索引号,当数据发生更改时,从fruits 数组中移出当前索引的数据项,并将其生成一个新的数据项,当拖拽完成时,将新的数据项插入到fruits 数组新的索引中,以此来实现列表项的拖拽排序功能。
在预览器中,长按并拖拽Cherry数据项,将其拖动至 Apple 数据项前面,如图 2-8 所示。
图 2-8 拖拽排序效果预览
2.4 实现分组列表
分组列表是 List 组件的一种展示方式,支持列表中的数据项按照分组的方式进行展示,来提高用户的数据查找效率。
创建一个名为 ListSection 的新的 HarmonyOS 项目,并打开工程开发面板。
创建一个组件itemHead,用于渲染列表分组的头部标题视图,代码如下:
@Builder itemHead(text: string) {
Text(text)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#999999')
.backgroundColor('#F5F5F5')
.width('100%')
.padding(10)
}
itemHead 组件传入一个string 类型的参数text,并将其赋值给Text 组件,以此来实现从父级组件定义itemHead 组件的标题的功能。
使用 private 关键字声明 2 个私有的字符串数组,来分别存储 2 个不同地区的国家名称,代码如下:
private asianCountries: string[] = ['中国', '日本', '韩国', '印度', '泰国', '越南', '新加坡', '马来西亚', '印度尼西亚', '菲律宾']
private europeanCountries: string[] = ['德国', '法国', '英国', '意大利', '西班牙', '荷兰', '瑞士', '瑞典', '挪威', '芬兰']
在 Build()函数中,使用 List 组件和ListItemGroup 组件来实现列表的分组显示,并通过 ForEach 函数来对多个分组进行循环渲染。代码如下:
@Entry
@Component
struct Index {
private asianCountries: string[] = ['中国', '日本', '韩国', '印度', '泰国', '越南', '新加坡', '马来西亚', '印度尼西亚', '菲律宾']
private europeanCountries: string[] = ['德国', '法国', '英国', '意大利', '西班牙', '荷兰', '瑞士', '瑞典', '挪威', '芬兰']
build() {
List() {
ListItemGroup({ header: this.itemHead('亚洲') }) {
ForEach(this.asianCountries,(item:string)=>{
ListItem() {
Text(item)
.fontSize(17)
.padding(10)
}
})
}
ListItemGroup({ header: this.itemHead('欧洲') }) {
ForEach(this.europeanCountries,(item:string)=>{
ListItem() {
Text(item)
.fontSize(17)
.padding(10)
}
})
}
}
.width('100%')
.height('100%')
}
@Builder itemHead(text: string) {...}
}
ListItemGroup 组件中,header 参数用于设置列表分组的头部组件,设置分组的头部组件为itemHead 组件,并设置itemHead 组件传入的参数值为大洲名称。
在不同的ListItemGroup 组件中使用ForEach()函数根据asianCountries 数组和europeanCountries 数组来渲染国家名称数据,并在ListItem 子组件中构建列表项的内容。
打开预览器,可以看到分组列表的显示效果,如图 2-9 所示。
图 2-9 分组列表效果预览
2.5 本章小结
本章介绍了 ArkUI 中的滚动视图和列表视图的基础使用,并进一步探讨了如何为列表添加增删查功能,包括新增列表项、实现侧滑交互、删除功能以及搜索功能。此外,本章还拓展介绍了如何创建分组列表,以实现更清晰的层级展示。
通过这些示例,开发者可以深入理解 ArkUI 在列表处理方面的强大能力,无论是基本的滚动与列表展示,还是复杂的交互操作,ArkUI 都能提供良好的支持。学习并使用ArkUI,开发者可以轻松地创建功能丰富、交互顺畅的列表视图。