本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
一、手势冲突的常见场景
场景 | 冲突表现 | 示例 |
---|---|---|
嵌套滑动 | 父容器和子组件同时响应滑动(如Scroll内嵌List) | 商品详情页(外层下拉刷新,内层列表滚动) |
多手势竞争 | 多个手势识别器同时触发(如点击和长按) | 图片列表(点击进入详情,长按选择) |
区域重叠 | 重叠区域的组件同时响应事件 | 悬浮按钮覆盖列表项 |
二、解决方案与核心API
1. 手势拦截:gesture
和 onTouch
通过组合手势识别器和手动控制事件流解决冲突。
API | 作用 |
---|---|
PanGesture | 滑动手势识别器 |
TapGesture | 点击手势识别器 |
LongPressGesture | 长按手势识别器 |
onTouch(event: TouchEvent) | 手动控制触摸事件(TouchEvent 包含坐标和动作类型) |
event.stopPropagation() | 阻止事件冒泡 |
2. 优先级控制:gesture
链式调用
通过链式调用设置手势识别器的优先级顺序。
三、实战案例与代码详解
案例1:嵌套滑动冲突(Scroll + List)
需求:外层Scroll
下拉刷新,内层List
垂直滚动,避免两者同时触发。
解决方案:判断滑动方向,动态拦截事件
@Entry
@Component
struct NestedScrollExample {
@State isRefreshing: boolean = false;
private scrollController: ScrollController = new ScrollController();
build() {
// 外层Scroll(支持下拉刷新)
Scroll(this.scrollController) {
Column() {
// 内层List
List({ space: 10 }) {
ForEach(Array(20).fill(0), (_, index) => {
ListItem() {
Text(`Item ${index}`)
.height(100)
.width('100%')
.backgroundColor('#f0f0f0')
}
})
}
.height('80%')
.width('100%')
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Move) {
// 判断滑动方向:Y轴位移大于X轴则为垂直滑动
const isVertical = Math.abs(event.touches[0].y - event.touches[0].startY) >
Math.abs(event.touches[0].x - event.touches[0].startX);
if (isVertical) {
event.stopPropagation(); // 阻止事件冒泡到外层Scroll
}
}
})
}
}
.onScrollEdge((side: ScrollEdge) => {
if (side === ScrollEdge.Top) {
this.isRefreshing = true;
simulateRefresh().then(() => this.isRefreshing = false);
}
})
.width('100%')
.height('100%')
}
}
// 模拟刷新
async function simulateRefresh(): Promise<void> {
return new Promise(resolve => setTimeout(resolve, 2000));
}
关键点:
- 通过
onTouch
手动判断滑动方向,垂直滑动时调用event.stopPropagation()
阻止事件冒泡。 - 外层
Scroll
通过onScrollEdge
监听顶部下拉动作。
案例2:点击与长按竞争
需求:列表项支持点击(进入详情)和长按(选择),避免误触发。
解决方案:使用gesture
链式调用设置优先级
@Entry
@Component
struct GestureCompetitionExample {
@State selectedIndex: number = -1;
build() {
List({ space: 10 }) {
ForEach(Array(10).fill(0), (_, index) => {
ListItem() {
Text(`Item ${index}`)
.height(80)
.width('90%')
.backgroundColor(this.selectedIndex === index ? '#dddddd' : '#ffffff')
}
// 手势链:长按优先于点击
.gesture(
LongPressGesture({ repeat: false })
.onAction(() => {
console.log(`长按选择项 ${index}`);
this.selectedIndex = index;
})
.onActionEnd(() => {
console.log('长按结束');
})
)
.gesture(
TapGesture()
.onAction(() => {
if (this.selectedIndex === -1) { // 未长按时才触发点击
console.log(`点击项 ${index}`);
router.pushUrl({ url: 'pages/DetailPage' });
}
})
)
})
}
.width('100%')
.height('100%')
}
}
关键点:
- 长按优先:先注册
LongPressGesture
,后注册TapGesture
,确保长按触发时不会同时触发点击。 - 状态控制:通过
selectedIndex
标记长按选中状态,点击时检查该状态。
案例3:悬浮按钮覆盖列表
需求:悬浮按钮覆盖列表区域,点击按钮时不触发列表项。
解决方案:使用hitTestBehavior
控制事件穿透
@Entry
@Component
struct FloatingButtonExample {
build() {
Stack() {
// 底层列表
List({ space: 10 }) {
ForEach(Array(20).fill(0), (_, index) => {
ListItem() {
Text(`List Item ${index}`)
.height(60)
.width('100%')
}
.onClick(() => {
console.log(`点击列表项 ${index}`);
})
})
}
.width('100%')
.height('100%')
// 悬浮按钮(阻止事件穿透)
Button('悬浮按钮')
.width(60)
.height(60)
.backgroundColor('#ff0000')
.position({ x: '80%', y: '80%' })
.hitTestBehavior(HitTestMode.Block) // 阻止事件向下传递
.onClick(() => {
console.log('点击悬浮按钮');
})
}
.width('100%')
.height('100%')
}
}
关键点:
HitTestMode.Block
:设置悬浮按钮完全拦截事件,底层列表不会响应。- 其他模式:
HitTestMode.Transparent
:允许事件穿透。HitTestMode.Default
:系统自动判断。
四、手势冲突解决策略
冲突类型 | 解决方案 | 核心API |
---|---|---|
嵌套滑动 | 方向判断 + 事件拦截 | onTouch + stopPropagation |
多手势竞争 | 链式注册 + 优先级控制 | gesture() 顺序 |
区域重叠 | 控制事件穿透行为 | hitTestBehavior |