项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
在现代移动应用中,可滑动列表项是一种常见且高效的交互方式,它允许用户通过水平滑动列表项来显示隐藏的操作按钮,如删除、置顶、归档等。本教程将详细讲解如何使用HarmonyOS NEXT的Row组件结合手势和动画创建一个流畅的可滑动列表项,实现滑动操作按钮的高级交互效果。
在设计可滑动列表项时,需要考虑以下几个关键原则:
本案例展示了如何创建一个可滑动的列表项,通过水平滑动显示隐藏的操作按钮(置顶和删除)。
@Component
export struct SwipeableListItem {
@State isSwiped: boolean = false
@State swipeOffset: number = 0
private swipeThreshold: number = 20
private maxSwipeDistance: number = 120
private actionButtonWidth: number = 60
private name: string = '张三'
private avatar: Resource = $r('app.media.avatar')
private lastMessage: string = '你好,最近怎么样?'
private onPin?: () => void
private onDelete?: () => void
build() {
Row() {
// 左侧内容(头像、姓名、最后消息)
Row() {
Image(this.avatar)
.width(50)
.height(50)
.borderRadius(25)
.margin({ right: 12 })
Column() {
Text(this.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 4 })
Text(this.lastMessage)
.fontSize(14)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.flexGrow(1)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.translate({ x: this.swipeOffset })
.gesture(
PanGesture({ direction: PanDirection.Horizontal })
.onActionStart(() => {
// 开始滑动
})
.onActionUpdate((event: GestureEvent) => {
// 更新滑动位置
if (event.offsetX < 0) {
// 只允许向左滑动(负值)
this.swipeOffset = Math.max(-this.maxSwipeDistance, event.offsetX)
} else if (this.isSwiped) {
// 如果已经处于滑开状态,允许向右滑动恢复
this.swipeOffset = Math.min(0, -this.maxSwipeDistance + event.offsetX)
}
})
.onActionEnd(() => {
// 结束滑动,根据滑动距离决定是否显示操作按钮
if (this.swipeOffset < -this.swipeThreshold) {
// 滑动距离超过阈值,显示操作按钮
this.swipeOffset = -this.maxSwipeDistance
this.isSwiped = true
} else {
// 滑动距离未超过阈值,恢复原位
this.swipeOffset = 0
this.isSwiped = false
}
})
)
// 右侧操作按钮(置顶、删除)
if (this.isSwiped) {
Row() {
Button({ type: ButtonType.Capsule }) {
Text('置顶')
.fontSize(14)
.fontColor('#FFFFFF')
}
.width(this.actionButtonWidth)
.height(50)
.backgroundColor('#2196F3')
.onClick(() => {
if (this.onPin) {
this.onPin()
}
// 操作完成后恢复原位
this.swipeOffset = 0
this.isSwiped = false
})
Button({ type: ButtonType.Capsule }) {
Text('删除')
.fontSize(14)
.fontColor('#FFFFFF')
}
.width(this.actionButtonWidth)
.height(50)
.backgroundColor('#FF5252')
.onClick(() => {
if (this.onDelete) {
this.onDelete()
}
// 操作完成后恢复原位
this.swipeOffset = 0
this.isSwiped = false
})
}
.width(this.maxSwipeDistance)
.height('100%')
.position({ x: '100%' })
.translate({ x: -this.maxSwipeDistance })
}
}
.width('100%')
.height(82)
.clip(true)
}
}
@Component
export struct SwipeableListItem {
@State isSwiped: boolean = false
@State swipeOffset: number = 0
private swipeThreshold: number = 20
private maxSwipeDistance: number = 120
private actionButtonWidth: number = 60
private name: string = '张三'
private avatar: Resource = $r('app.media.avatar')
private lastMessage: string = '你好,最近怎么样?'
private onPin?: () => void
private onDelete?: () => void
这部分代码声明了一个名为SwipeableListItem
的自定义组件,并定义了以下状态和属性:
属性/状态 | 类型 | 说明 | 默认值 |
---|---|---|---|
isSwiped | boolean | 是否处于滑开状态 | false |
swipeOffset | number | 滑动偏移量 | 0 |
swipeThreshold | number | 触发滑开状态的阈值 | 20 |
maxSwipeDistance | number | 最大滑动距离 | 120 |
actionButtonWidth | number | 操作按钮宽度 | 60 |
name | string | 联系人姓名 | ‘张三’ |
avatar | Resource | 联系人头像 | $r(‘app.media.avatar’) |
lastMessage | string | 最后一条消息 | ‘你好,最近怎么样?’ |
onPin | () => void | 置顶回调函数 | undefined |
onDelete | () => void | 删除回调函数 | undefined |
使用@State
装饰器定义的状态变量会在值变化时自动触发UI更新,这对于实现滑动效果非常重要。
Row() {
// 子组件
}
.width('100%')
.height(82)
.clip(true)
这部分代码创建了一个Row容器,作为可滑动列表项的根容器。Row容器的属性设置如下:
属性 | 值 | 说明 |
---|---|---|
width | ‘100%’ | 容器宽度为父容器的100% |
height | 82 | 容器高度为82vp |
clip | true | 裁剪超出容器范围的内容 |
clip(true)
设置非常重要,它确保滑动时超出容器范围的内容会被裁剪,避免影响其他UI元素。
Row() {
Image(this.avatar)
.width(50)
.height(50)
.borderRadius(25)
.margin({ right: 12 })
Column() {
Text(this.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 4 })
Text(this.lastMessage)
.fontSize(14)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.flexGrow(1)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.translate({ x: this.swipeOffset })
.gesture(
PanGesture({ direction: PanDirection.Horizontal })
// 手势处理...
)
这部分代码创建了一个Row容器,用于显示左侧内容(头像、姓名、最后消息)。Row容器的属性设置如下:
属性 | 值 | 说明 |
---|---|---|
width | ‘100%’ | 容器宽度为父容器的100% |
padding | 16 | 设置内边距为16vp |
backgroundColor | ‘#FFFFFF’ | 设置背景色为白色 |
translate | { x: this.swipeOffset } | 设置水平平移距离为swipeOffset |
内部包含两个子组件:
最关键的是translate({ x: this.swipeOffset })
设置,它根据swipeOffset
状态变量动态调整Row容器的水平位置,实现滑动效果。
.gesture(
PanGesture({ direction: PanDirection.Horizontal })
.onActionStart(() => {
// 开始滑动
})
.onActionUpdate((event: GestureEvent) => {
// 更新滑动位置
if (event.offsetX < 0) {
// 只允许向左滑动(负值)
this.swipeOffset = Math.max(-this.maxSwipeDistance, event.offsetX)
} else if (this.isSwiped) {
// 如果已经处于滑开状态,允许向右滑动恢复
this.swipeOffset = Math.min(0, -this.maxSwipeDistance + event.offsetX)
}
})
.onActionEnd(() => {
// 结束滑动,根据滑动距离决定是否显示操作按钮
if (this.swipeOffset < -this.swipeThreshold) {
// 滑动距离超过阈值,显示操作按钮
this.swipeOffset = -this.maxSwipeDistance
this.isSwiped = true
} else {
// 滑动距离未超过阈值,恢复原位
this.swipeOffset = 0
this.isSwiped = false
}
})
)
这部分代码为Row容器添加了一个水平方向的平移手势(PanGesture),用于处理滑动操作。手势处理包含三个阶段:
swipeOffset
状态变量。
swipeOffset
为手指移动距离和最大滑动距离的较大值(负值),确保不会超过最大滑动距离。swipeOffset
设置为最大滑动距离的负值,并将isSwiped
设置为true,表示处于滑开状态。swipeOffset
设置为0,并将isSwiped
设置为false,表示恢复原位。这种手势处理方式使列表项能够根据用户的滑动操作自然地显示或隐藏操作按钮,提供流畅的交互体验。
if (this.isSwiped) {
Row() {
Button({ type: ButtonType.Capsule }) {
Text('置顶')
.fontSize(14)
.fontColor('#FFFFFF')
}
.width(this.actionButtonWidth)
.height(50)
.backgroundColor('#2196F3')
.onClick(() => {
if (this.onPin) {
this.onPin()
}
// 操作完成后恢复原位
this.swipeOffset = 0
this.isSwiped = false
})
Button({ type: ButtonType.Capsule }) {
Text('删除')
.fontSize(14)
.fontColor('#FFFFFF')
}
.width(this.actionButtonWidth)
.height(50)
.backgroundColor('#FF5252')
.onClick(() => {
if (this.onDelete) {
this.onDelete()
}
// 操作完成后恢复原位
this.swipeOffset = 0
this.isSwiped = false
})
}
.width(this.maxSwipeDistance)
.height('100%')
.position({ x: '100%' })
.translate({ x: -this.maxSwipeDistance })
}
这部分代码使用条件渲染,只有在isSwiped
为true时才显示右侧操作按钮。操作按钮包含在一个Row容器中,Row容器的属性设置如下:
属性 | 值 | 说明 |
---|---|---|
width | this.maxSwipeDistance | 容器宽度为最大滑动距离 |
height | ‘100%’ | 容器高度为父容器的100% |
position | { x: ‘100%’ } | 设置水平位置为父容器的100%(右侧) |
translate | { x: -this.maxSwipeDistance } | 设置水平平移距离为最大滑动距离的负值 |
内部包含两个Button组件:
position({ x: '100%' })
和translate({ x: -this.maxSwipeDistance })
的组合使操作按钮定位在列表项的右侧,并向左平移最大滑动距离,使其在列表项滑开时正好显示在右侧。
在HarmonyOS NEXT中,滑动交互主要通过手势和状态管理实现。
HarmonyOS NEXT提供了丰富的手势识别功能,在本案例中,我们使用PanGesture
识别水平滑动手势:
PanGesture({ direction: PanDirection.Horizontal })
PanGesture
是一种平移手势,可以识别用户的拖动操作。通过设置direction
参数为PanDirection.Horizontal
,我们限制了只识别水平方向的滑动,忽略垂直方向的滑动。
在滑动交互中,状态管理非常重要。本案例使用两个状态变量管理滑动状态:
这两个状态变量使用@State
装饰器定义,确保它们的变化会自动触发UI更新。
通过translate
属性,我们可以动态调整组件的位置:
.translate({ x: this.swipeOffset })
当swipeOffset
状态变量变化时,组件的水平位置会随之变化,实现滑动效果。
通过条件渲染,我们可以根据状态动态显示或隐藏操作按钮:
if (this.isSwiped) {
// 显示操作按钮
}
当isSwiped
状态变量为true时,操作按钮会显示;当为false时,操作按钮会隐藏。
为了提升滑动交互的用户体验,我们需要设置适当的滑动阈值和添加动画效果。
滑动阈值是指触发滑开状态的最小滑动距离。在本案例中,我们设置了20vp的滑动阈值:
private swipeThreshold: number = 20
当滑动距离超过这个阈值时,列表项会自动滑到最大滑动距离,显示操作按钮;当滑动距离小于这个阈值时,列表项会自动恢复原位。
这种设计使用户可以通过小幅度的滑动来取消操作,提高了交互的容错性。
为了使滑动交互更加流畅,我们可以添加动画效果:
.translate({ x: this.swipeOffset })
.animation({
duration: 200,
curve: Curve.Ease
})
通过添加animation
属性,我们可以为translate
属性的变化添加动画效果,使列表项的滑动更加平滑。
duration
参数指定了动画的持续时间,curve
参数指定了动画的缓动曲线。在本例中,我们使用了200毫秒的持续时间和Curve.Ease
缓动曲线,使动画既不会太快也不会太慢,提供自然的过渡效果。
为了提升可滑动列表项的视觉效果和用户体验,我们可以进行以下优化:
添加分割线可以清晰地区分不同的列表项:
Row() {
// 现有内容
}
.width('100%')
.height(82)
.clip(true)
.border({
width: { bottom: 1 },
color: { bottom: '#EEEEEE' },
style: { bottom: BorderStyle.Solid }
})
这些设置在列表项底部添加了一条浅灰色的分割线,使不同列表项之间的边界更加清晰。
添加点击效果可以提升用户交互体验:
Row() {
// 左侧内容
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.translate({ x: this.swipeOffset })
.stateStyles({
pressed: {
opacity: 0.8,
backgroundColor: '#F8F8F8'
}
})
.gesture(
// 手势处理
)
.onClick(() => {
// 处理点击事件
if (this.isSwiped) {
// 如果处于滑开状态,点击时恢复原位
this.swipeOffset = 0
this.isSwiped = false
} else {
// 如果未处于滑开状态,点击时执行其他操作
}
})
这些设置使列表项在被点击时有明显的视觉反馈,提示用户点击操作已被识别。同时,我们添加了点击事件处理,使用户可以通过点击列表项来恢复滑开状态。
优化操作按钮的样式可以提升视觉效果:
Button({ type: ButtonType.Capsule }) {
Column() {
Image($r('app.media.ic_pin'))
.width(24)
.height(24)
.margin({ bottom: 4 })
Text('置顶')
.fontSize(12)
.fontColor('#FFFFFF')
}
.alignItems(HorizontalAlign.Center)
}
.width(this.actionButtonWidth)
.height(70)
.backgroundColor('#2196F3')
这些设置使操作按钮包含图标和文本,提供更丰富的视觉信息,帮助用户理解按钮的功能。
为了提升可滑动列表项的交互体验,我们可以添加以下功能:
添加触觉反馈可以增强滑动交互的体验:
.onActionEnd(() => {
// 结束滑动,根据滑动距离决定是否显示操作按钮
if (this.swipeOffset < -this.swipeThreshold) {
// 滑动距离超过阈值,显示操作按钮
this.swipeOffset = -this.maxSwipeDistance
this.isSwiped = true
vibrate(VibrationType.Light) // 触发轻微振动
} else {
// 滑动距离未超过阈值,恢复原位
this.swipeOffset = 0
this.isSwiped = false
}
})
通过调用vibrate
函数,我们可以在列表项滑开时触发轻微振动,提供触觉反馈,增强用户的操作感知。
添加自动关闭功能,使用户点击其他区域时自动关闭已滑开的列表项:
@Component
export struct SwipeableList {
@State activeItem: number = -1 // 当前滑开的列表项索引
build() {
List() {
ForEach(this.items, (item, index) => {
ListItem() {
SwipeableListItem({
name: item.name,
avatar: item.avatar,
lastMessage: item.lastMessage,
isActive: this.activeItem === index,
onSwipeStateChange: (isSwiped) => {
if (isSwiped) {
// 如果当前列表项被滑开,记录其索引
this.activeItem = index
} else if (this.activeItem === index) {
// 如果当前列表项被关闭,重置活动索引
this.activeItem = -1
}
},
onPin: () => {
// 处理置顶操作
},
onDelete: () => {
// 处理删除操作
}
})
}
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.onClick(() => {
// 点击列表空白区域时,关闭所有滑开的列表项
if (this.activeItem !== -1) {
this.activeItem = -1
}
})
}
}
在这个优化版本中,我们使用activeItem
状态变量记录当前滑开的列表项索引,并通过onSwipeStateChange
回调函数更新这个状态。当用户点击列表空白区域时,我们将activeItem
重置为-1,关闭所有滑开的列表项。
基于本案例的基本结构,我们可以扩展更多功能:
在某些场景下,可能需要显示更多的操作按钮:
Row() {
Button({ type: ButtonType.Capsule }) {
Text('标记')
.fontSize(14)
.fontColor('#FFFFFF')
}
.width(this.actionButtonWidth)
.height(50)
.backgroundColor('#4CAF50')
.onClick(() => {
// 处理标记操作
})
Button({ type: ButtonType.Capsule }) {
Text('置顶')
.fontSize(14)
.fontColor('#FFFFFF')
}
.width(this.actionButtonWidth)
.height(50)
.backgroundColor('#2196F3')
.onClick(() => {
// 处理置顶操作
})
Button({ type: ButtonType.Capsule }) {
Text('删除')
.fontSize(14)
.fontColor('#FFFFFF')
}
.width(this.actionButtonWidth)
.height(50)
.backgroundColor('#FF5252')
.onClick(() => {
// 处理删除操作
})
}
.width(this.actionButtonWidth * 3)
.height('100%')
在这个扩展功能中,我们添加了一个"标记"按钮,使用户可以标记重要的列表项。同时,我们将Row容器的宽度调整为操作按钮宽度的3倍,确保有足够的空间显示所有按钮。
在某些场景下,可能需要支持从右向左滑动显示操作按钮:
@Component
export struct BiDirectionalSwipeableListItem {
@State leftSwipeOffset: number = 0
@State rightSwipeOffset: number = 0
@State isLeftSwiped: boolean = false
@State isRightSwiped: boolean = false
private swipeThreshold: number = 20
private maxSwipeDistance: number = 120
build() {
Row() {
// 左侧操作按钮(收藏、转发)
if (this.isLeftSwiped) {
Row() {
// 左侧操作按钮
}
.width(this.maxSwipeDistance)
.height('100%')
.position({ x: 0 })
.translate({ x: this.leftSwipeOffset - this.maxSwipeDistance })
}
// 中间内容
Row() {
// 列表项内容
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.translate({ x: this.leftSwipeOffset + this.rightSwipeOffset })
.gesture(
PanGesture({ direction: PanDirection.Horizontal })
.onActionUpdate((event: GestureEvent) => {
// 处理水平滑动
if (event.offsetX > 0 && !this.isRightSwiped) {
// 向右滑动,显示左侧操作按钮
this.leftSwipeOffset = Math.min(this.maxSwipeDistance, event.offsetX)
this.rightSwipeOffset = 0
} else if (event.offsetX < 0 && !this.isLeftSwiped) {
// 向左滑动,显示右侧操作按钮
this.rightSwipeOffset = Math.max(-this.maxSwipeDistance, event.offsetX)
this.leftSwipeOffset = 0
}
})
.onActionEnd(() => {
// 处理滑动结束
if (this.leftSwipeOffset > this.swipeThreshold) {
// 显示左侧操作按钮
this.leftSwipeOffset = this.maxSwipeDistance
this.isLeftSwiped = true
this.isRightSwiped = false
} else if (this.rightSwipeOffset < -this.swipeThreshold) {
// 显示右侧操作按钮
this.rightSwipeOffset = -this.maxSwipeDistance
this.isRightSwiped = true
this.isLeftSwiped = false
} else {
// 恢复原位
this.leftSwipeOffset = 0
this.rightSwipeOffset = 0
this.isLeftSwiped = false
this.isRightSwiped = false
}
})
)
// 右侧操作按钮(置顶、删除)
if (this.isRightSwiped) {
Row() {
// 右侧操作按钮
}
.width(this.maxSwipeDistance)
.height('100%')
.position({ x: '100%' })
.translate({ x: this.rightSwipeOffset })
}
}
.width('100%')
.height(82)
.clip(true)
}
}
在这个扩展功能中,我们支持双向滑动:向左滑动显示右侧操作按钮,向右滑动显示左侧操作按钮。我们使用四个状态变量管理滑动状态:leftSwipeOffset
、rightSwipeOffset
、isLeftSwiped
和isRightSwiped
。
为了提高代码复用性,可以将可滑动列表项封装为一个独立的组件:
@Component
export struct SwipeableListItem {
@Prop name: string = ''
@Prop avatar: Resource = $r('app.media.default_avatar')
@Prop lastMessage: string = ''
@Prop isActive: boolean = false
@State isSwiped: boolean = false
@State swipeOffset: number = 0
private swipeThreshold: number = 20
private maxSwipeDistance: number = 120
private actionButtonWidth: number = 60
onSwipeStateChange?: (isSwiped: boolean) => void
onPin?: () => void
onDelete?: () => void
aboutToAppear() {
// 监听isActive属性变化
if (!this.isActive && this.isSwiped) {
// 如果不是活动项但处于滑开状态,恢复原位
this.swipeOffset = 0
this.isSwiped = false
if (this.onSwipeStateChange) {
this.onSwipeStateChange(false)
}
}
}
build() {
// 组件内容
}
}
然后在应用中使用这个组件:
@Entry
@Component
struct ChatListPage {
@State contacts: Array<{
name: string,
avatar: Resource,
lastMessage: string
}> = [
{
name: '张三',
avatar: $r('app.media.avatar_1'),
lastMessage: '你好,最近怎么样?'
},
{
name: '李四',
avatar: $r('app.media.avatar_2'),
lastMessage: '周末有空吗?一起打球吧。'
}
// 更多联系人...
]
@State activeItem: number = -1
build() {
Column() {
// 标题
Text('消息')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ left: 16, top: 16, bottom: 8 })
// 联系人列表
List() {
ForEach(this.contacts, (contact, index) => {
ListItem() {
SwipeableListItem({
name: contact.name,
avatar: contact.avatar,
lastMessage: contact.lastMessage,
isActive: this.activeItem === index,
onSwipeStateChange: (isSwiped) => {
if (isSwiped) {
this.activeItem = index
} else if (this.activeItem === index) {
this.activeItem = -1
}
},
onPin: () => {
// 处理置顶操作
console.info('置顶联系人:' + contact.name)
},
onDelete: () => {
// 处理删除操作
console.info('删除联系人:' + contact.name)
}
})
}
})
}
.width('100%')
.layoutWeight(1)
.backgroundColor('#F5F5F5')
.onClick(() => {
// 点击列表空白区域时,关闭所有滑开的列表项
if (this.activeItem !== -1) {
this.activeItem = -1
}
})
}
.width('100%')
.height('100%')
}
}
本教程详细讲解了如何使用HarmonyOS NEXT的Row组件结合手势和动画创建流畅的可滑动列表项,实现滑动操作按钮的高级交互效果。