HarmonyOS6 ArkUI 触摸拦截(onTouchIntercept)全面解析与实战演示

文章目录


一、onTouchIntercept 核心概念

1.1 触摸事件分发流程

理解 onTouchIntercept 之前,需要先了解 ArkUI 触摸事件的分发流程:

复制代码
用户手指触碰屏幕
       ↓
  HitTest 命中测试(从根节点向叶节点遍历)
       ↓
  onTouchIntercept 回调触发(API 12+)
  ↓(返回 HitTestMode)
  Default  → 自身响应,子组件继续测试
  Block    → 自身拦截,子组件不再接收
  Transparent → 跳过自身,子组件正常测试
  None     → 自身和子组件均不参与
       ↓
  确定事件消费组件
       ↓
  onTouch / onClick / 手势回调触发

1.2 API 定义

onTouchIntercept 是 ArkUI 组件通用属性(Universal Attribute),可链式调用在任意组件上:

typescript 复制代码
// 接口签名
onTouchIntercept(callback: (event: TouchEvent) => HitTestMode): T
参数 类型 说明
callback (event: TouchEvent) => HitTestMode 触摸命中测试前的拦截回调,必须同步返回 HitTestMode
返回值 当前组件实例 T 支持链式调用

版本说明onTouchInterceptAPI version 12 开始支持;在 attributeModifier 中调用需 API version 20

1.3 HitTestMode 枚举详解

HitTestModeonTouchIntercept 回调的返回值类型,共四种模式:

枚举值 数值 含义 子组件是否受影响
HitTestMode.Default 0 默认:组件自身可响应,子组件正常参与命中测试 不受影响(正常)
HitTestMode.Block 1 拦截 :组件自身响应,且阻止子组件参与命中测试 子组件不可响应
HitTestMode.Transparent 2 透明 :组件自身不响应,子组件正常参与命中测试 不受影响(正常)
HitTestMode.None 3 禁用 :组件自身和子组件均不响应 子组件也不响应

核心区别Default vs Block 的差异在于子组件是否受阻Transparent vs None 的差异在于子组件是否仍可响应

1.4 四种模式对比速查

复制代码
触点
  │
  ▼ 命中 Column(绑定了 onTouchIntercept)
  
  返回 Default    → ✅ Column 自身响应  ✅ 子组件正常响应
  返回 Block      → ✅ Column 自身响应  ❌ 子组件不响应
  返回 Transparent→ ❌ Column 自身不响应 ✅ 子组件正常响应
  返回 None       → ❌ Column 自身不响应 ❌ 子组件不响应

1.5 使用限制速查

限制项 说明
必须同步返回 回调必须同步 返回 HitTestMode,禁止异步操作,否则按 Default 处理
避免耗时操作 回调在 HitTest 阶段执行,耗时会导致触摸响应延迟/卡顿
版本要求 API 12+;attributeModifier 中使用需 API 20+
事件流顺序 与高阶手势(拖拽、多指)同用时需注意事件流顺序,避免互相干扰

二、基础用法:四种模式演示

2.1 Default ------ 默认行为

typescript 复制代码
// Default:自身可响应,子组件也可响应(正常行为)
Column() {
  Text('子组件(可点击)')
    .onClick(() => console.info('子组件点击'))
}
.backgroundColor('#E3F2FD')
.onTouchIntercept((event: TouchEvent) => {
  console.info('HitTest: 返回 Default')
  return HitTestMode.Default  // 自身和子组件均正常
})
.onClick(() => console.info('Column 点击'))

运行效果如图,当点击组件控制台打印:

2.2 Block ------ 拦截模式

typescript 复制代码
// Block:Column 拦截,子组件 Text 点击失效
Column() {
  Text('子组件(被拦截,点击无效)')
    .onClick(() => console.info('这行不会触发'))
}
.backgroundColor('#FFEBEE')
.onTouchIntercept((event: TouchEvent) => {
  console.info('HitTest: 返回 Block,子组件被拦截')
  return HitTestMode.Block  // 拦截,子组件不可响应
})
.onClick(() => console.info('Column 拦截了所有触摸'))

当点击元素,控制台打印:

2.3 Transparent ------ 透明穿透

typescript 复制代码
// Transparent:Column 自身不响应,子组件正常响应
Column() {
  Text('子组件(可点击,Column 透明)')
    .onClick(() => console.info('子组件正常触发'))
}
.backgroundColor('#E8F5E9')
.onTouchIntercept((event: TouchEvent) => {
  console.info('HitTest: 返回 Transparent,Column 透明')
  return HitTestMode.Transparent  // 自身透明,子组件正常
})
.onClick(() => console.info('这行不会触发'))

点击组件,控制台输出如图所示:

2.4 None ------ 完全禁用

typescript 复制代码
// None:Column 和子组件全部不响应
Column() {
  Text('子组件(也无法点击)')
    .onClick(() => console.info('不会触发'))
}
.backgroundColor('#FFF9C4')
.onTouchIntercept((event: TouchEvent) => {
  console.info('HitTest: 返回 None,全部禁用')
  return HitTestMode.None  // 自身和子组件均不响应
})
.onClick(() => console.info('不会触发'))

三、可运行完整示例:四模式交互对比

3.1 功能说明

以下示例提供四个可切换的演示卡片,分别展示 HitTestMode 四种模式的实际效果,并实时记录事件触发日志:

typescript 复制代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct HitTestModeDemo {
  @State selectedMode: number = 0  // 0=Default, 1=Block, 2=Transparent, 3=None
  @State eventLog: string[] = ['等待触摸事件...']
  @State parentClickCount: number = 0
  @State childClickCount: number = 0

  private modeNames: string[] = ['Default', 'Block', 'Transparent', 'None']
  private modeColors: string[] = ['#E3F2FD', '#FFEBEE', '#E8F5E9', '#FFF9C4']
  private modeBorderColors: string[] = ['#90CAF9', '#EF9A9A', '#A5D6A7', '#F9A825']
  private modeDescriptions: string[] = [
    '自身响应 ✅  子组件响应 ✅',
    '自身响应 ✅  子组件响应 ❌(被拦截)',
    '自身响应 ❌(透明)  子组件响应 ✅',
    '自身响应 ❌  子组件响应 ❌(全禁用)'
  ]

  private getHitMode(): HitTestMode {
    switch (this.selectedMode) {
      case 0: return HitTestMode.Default
      case 1: return HitTestMode.Block
      case 2: return HitTestMode.Transparent
      case 3: return HitTestMode.None
      default: return HitTestMode.Default
    }
  }

  private appendLog(msg: string): void {
    this.eventLog = [msg, ...this.eventLog.slice(0, 9)]
  }

  build() {
    Column({ space: 0 }) {
      // 标题栏
      Row({ space: 10 }) {
        Text('👆').fontSize(22)
        Text('触摸拦截模式演示').fontSize(18).fontWeight(FontWeight.Bold).fontColor(Color.White)
      }
      .width('100%').padding({ left: 20, right: 20, top: 16, bottom: 14 })
      .backgroundColor('#1565C0').justifyContent(FlexAlign.Start)

      Scroll() {
        Column({ space: 16 }) {
          // 模式选择
          Text('选择 HitTestMode').fontSize(12).fontColor('#9E9E9E').width('90%').margin({ top: 16 })
          Row({ space: 8 }) {
            ForEach(this.modeNames, (name: string, idx: number) => {
              Text(name)
                .fontSize(12).layoutWeight(1).textAlign(TextAlign.Center)
                .padding({ top: 8, bottom: 8 })
                .backgroundColor(this.selectedMode === idx ? '#1565C0' : '#F5F5F5')
                .fontColor(this.selectedMode === idx ? Color.White : '#555555')
                .fontWeight(this.selectedMode === idx ? FontWeight.Bold : FontWeight.Normal)
                .borderRadius(8)
                .animation({ duration: 150, curve: Curve.EaseOut })
                .onClick(() => {
                  this.selectedMode = idx
                  this.parentClickCount = 0
                  this.childClickCount = 0
                  this.eventLog = [`切换模式:HitTestMode.${name}`]
                })
            })
          }.width('90%')

          // 当前模式说明
          Text(this.modeDescriptions[this.selectedMode])
            .fontSize(13).fontColor('#1565C0').fontWeight(FontWeight.Bold)
            .width('90%').textAlign(TextAlign.Center)
            .padding({ top: 8, bottom: 8, left: 12, right: 12 })
            .backgroundColor('#E3F2FD').borderRadius(10)

          // 演示区
          Text('触摸下方区域测试').fontSize(12).fontColor('#9E9E9E').width('90%')

          // 父容器(绑定 onTouchIntercept)
          Stack({ alignContent: Alignment.Center }) {
            Column({ space: 0 }) {}
              .width('100%').height('100%')
              .backgroundColor(this.modeColors[this.selectedMode])
              .borderRadius(16)
              .border({ width: 2, color: this.modeBorderColors[this.selectedMode], style: BorderStyle.Solid })
              .animation({ duration: 200, curve: Curve.EaseOut })

            Column({ space: 10 }) {
              Text(`父容器(HitTestMode.${this.modeNames[this.selectedMode]})`)
                .fontSize(12).fontColor('#555555').fontWeight(FontWeight.Bold)

              // 子组件(Button)
              Button('👆 点击子组件按钮')
                .height(44).fontSize(13).borderRadius(10)
                .backgroundColor('#1565C0').fontColor(Color.White)
                .onClick(() => {
                  this.childClickCount++
                  this.appendLog(`✅ 子组件 Button 点击  ×${this.childClickCount}`)
                })

              Text(`子组件点击次数:${this.childClickCount}`)
                .fontSize(12).fontColor('#1565C0').fontWeight(FontWeight.Bold)
              Text(`父容器点击次数:${this.parentClickCount}`)
                .fontSize(12).fontColor('#FF9800').fontWeight(FontWeight.Bold)
            }
            .justifyContent(FlexAlign.Center)
          }
          .width('90%').height(200)
          .onTouchIntercept((event: TouchEvent) => {
            const x = event.touches[0]?.x ?? 0
            const y = event.touches[0]?.y ?? 0
            this.appendLog(`🔍 HitTest: (${x.toFixed(0)},${y.toFixed(0)}) → ${this.modeNames[this.selectedMode]}`)
            return this.getHitMode()
          })
          .onClick(() => {
            this.parentClickCount++
            this.appendLog(`📦 父容器 onClick 触发  ×${this.parentClickCount}`)
          })

          // 事件日志
          Column({ space: 4 }) {
            Row() {
              Text('事件日志').fontSize(11).fontColor('#9E9E9E').layoutWeight(1)
              Text('(最新在顶)').fontSize(10).fontColor('#BDBDBD')
            }.width('100%')
            ForEach(this.eventLog, (item: string, idx: number) => {
              Text(item)
                .fontSize(11).fontColor(idx === 0 ? '#1565C0' : '#BDBDBD').width('100%')
            })
          }
          .width('90%').padding(12).backgroundColor('#F9F9F9').borderRadius(12)
          .border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
          .alignItems(HorizontalAlign.Start)

          // 模式说明卡片
          Column({ space: 8 }) {
            Text('💡 四种模式说明').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#1565C0')
            ForEach([
              ['Default', '自身 ✅ 子组件 ✅', '正常交互,默认行为'],
              ['Block', '自身 ✅ 子组件 ❌', '父拦截,适用于遮罩/弹层'],
              ['Transparent', '自身 ❌ 子组件 ✅', '父透明,只有子组件响应'],
              ['None', '自身 ❌ 子组件 ❌', '完全禁用,不可交互区域'],
            ], (row: string[]) => {
              Row({ space: 8 }) {
                Text(row[0]).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#333333').width(90)
                Text(row[1]).fontSize(10).fontColor('#666666').width(110)
                Text(row[2]).fontSize(10).fontColor('#888888').layoutWeight(1)
              }.width('100%')
            })
          }
          .width('90%').padding(14).backgroundColor('#FFF9C4').borderRadius(12)
          .border({ width: 1, color: '#F9A825', style: BorderStyle.Solid })
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%').alignItems(HorizontalAlign.Center).padding({ bottom: 24 })
      }
      .layoutWeight(1).width('100%')
    }
    .width('100%').height('100%').backgroundColor(Color.White)
  }
}

运行效果如图:




四、进阶:基于坐标的动态分区拦截

4.1 左右分区拦截

onTouchIntercept 最强大的能力是按触点坐标动态返回不同模式,实现不规则交互区域:

typescript 复制代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct ZonedInterceptDemo {
  @State log: string[] = ['等待触摸...']
  @State touchZone: string = '---'

  private appendLog(msg: string): void {
    this.log = [msg, ...this.log.slice(0, 9)]
  }

  build() {
    Column({ space: 16 }) {
      Text('分区触摸拦截演示').fontSize(20).fontWeight(FontWeight.Bold).margin({ top: 24 })
      Text('左半区:父透传子响应 | 右半区:父拦截子不响应').fontSize(12).fontColor('#888888')

      Text(`当前触摸区域:${this.touchZone}`).fontSize(14).fontColor('#1565C0').fontWeight(FontWeight.Bold)
        .padding({ left: 12, right: 12, top: 6, bottom: 6 })
        .backgroundColor('#E3F2FD').borderRadius(8)

      // 主演示容器(宽度 300vp,触点 x<150 为左区,x>=150 为右区)
      Stack() {
        // 左侧背景色
        Row() {
          Column()
            .width('50%').height('100%')
            .backgroundColor('#E8F5E9')
          Column()
            .width('50%').height('100%')
            .backgroundColor('#FFEBEE')
        }.width('100%').height('100%')

        // 区域标注
        Row() {
          Column({ space: 6 }) {
            Text('⬅ 左区').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
            Text('Transparent').fontSize(11).fontColor('#43A047')
            Text('父透传,子响应').fontSize(10).fontColor('#81C784')
          }.layoutWeight(1).justifyContent(FlexAlign.Center)

          Column({ space: 6 }) {
            Text('右区 ➡').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#C62828')
            Text('Block').fontSize(11).fontColor('#E53935')
            Text('父拦截,子不响应').fontSize(10).fontColor('#EF9A9A')
          }.layoutWeight(1).justifyContent(FlexAlign.Center)
        }.width('100%').height('100%')

        // 子组件(位于中央,跨越左右两区)
        Column({ space: 8 }) {
          Text('🎯').fontSize(30)
          Text('子按钮').fontSize(13).fontWeight(FontWeight.Bold).fontColor(Color.White)
          Text('点左区可触发\n点右区被拦截').fontSize(10).fontColor('#90CAF9').textAlign(TextAlign.Center)
        }
        .width(120).height(100)
        .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor('#1565C0').borderRadius(14)
        .shadow({ radius: 10, color: '#401565C0', offsetX: 0, offsetY: 4 })
        .onClick(() => {
          this.appendLog('🎯 子按钮被点击(左区透传成功)')
        })
      }
      .width(300).height(200)
      .borderRadius(16)
      .border({ width: 1.5, color: '#E0E0E0', style: BorderStyle.Solid })
      .onTouchIntercept((event: TouchEvent) => {
        const x = event.touches[0]?.x ?? 0
        const y = event.touches[0]?.y ?? 0
        if (x < 150) {
          // 左半区:父容器透明,子组件可响应
          this.touchZone = `左区 (${x.toFixed(0)},${y.toFixed(0)}) → Transparent`
          this.appendLog(`⬅ 左区 HitTest → Transparent,子可响应`)
          return HitTestMode.Transparent
        } else {
          // 右半区:父容器拦截,子组件不响应
          this.touchZone = `右区 (${x.toFixed(0)},${y.toFixed(0)}) → Block`
          this.appendLog(`➡ 右区 HitTest → Block,父拦截`)
          return HitTestMode.Block
        }
      })
      .onClick(() => {
        this.appendLog('📦 父容器 onClick(右区 Block 拦截触发)')
      })

      // 事件日志
      Column({ space: 4 }) {
        Text('事件日志').fontSize(11).fontColor('#9E9E9E').width('100%')
        ForEach(this.log, (item: string, idx: number) => {
          Text(item).fontSize(11).fontColor(idx === 0 ? '#1565C0' : '#BDBDBD').width('100%')
        })
      }
      .width('90%').padding(12).backgroundColor('#F9F9F9').borderRadius(12)
      .border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%').height('100%')
    .alignItems(HorizontalAlign.Center)
    .backgroundColor(Color.White)
  }
}

运行效果如图:

4.2 圆形有效区域(不规则形状)

通过判断触点到中心的距离,实现圆形有效触摸区域------圆内响应,圆外禁用:

typescript 复制代码
// 圆形触摸区域:圆内 Default,圆外 None
Stack() {
  // 视觉上是圆形的组件
  Column()
    .width(200).height(200)
    .borderRadius(100)  // 视觉圆形
    .backgroundColor('#1565C0')
}
.width(200).height(200)
.onTouchIntercept((event: TouchEvent) => {
  const x = event.touches[0]?.x ?? 0
  const y = event.touches[0]?.y ?? 0
  // 计算触点到圆心(100,100)的距离
  const dx = x - 100
  const dy = y - 100
  const distance = Math.sqrt(dx * dx + dy * dy)
  if (distance <= 100) {
    // 圆内:正常响应
    return HitTestMode.Default
  } else {
    // 圆外(矩形边角区域):完全禁用
    return HitTestMode.None
  }
})
.onClick(() => {
  console.info('圆形区域内点击有效')
})

五、实战场景:多层叠加透明穿透

5.1 场景说明

以下示例模拟一个常见的悬浮工具栏覆盖在内容区上方的场景。工具栏有按钮区和空白区:

  • 按钮区Block 模式,点击由工具栏按钮消费
  • 空白区Transparent 模式,触摸穿透到下方内容区
typescript 复制代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct FloatToolbarDemo {
  @State log: string[] = ['等待操作...']
  @State contentClickCount: number = 0
  @State toolbarClickLabel: string = '未操作'
  // 工具栏按钮区域(x: 0-260, y: 0-56),其余为空白区
  private readonly TOOLBAR_HEIGHT: number = 56
  private readonly TOOLBAR_BTN_WIDTH: number = 260

  private appendLog(msg: string): void {
    this.log = [msg, ...this.log.slice(0, 9)]
  }

  build() {
    Stack({ alignContent: Alignment.TopStart }) {
      // 底层:内容区(背景地图/文章列表模拟)
      Column({ space: 0 }) {
        ForEach(['内容行 A(可点击)', '内容行 B(可点击)', '内容行 C(可点击)',
          '内容行 D(可点击)', '内容行 E(可点击)'], (item: string, idx: number) => {
          Row({ space: 12 }) {
            Text(`${idx + 1}`).fontSize(14).fontColor('#9E9E9E').width(24).textAlign(TextAlign.Center)
            Text(item).fontSize(14).fontColor('#333333').layoutWeight(1)
            Text(`×${this.contentClickCount}`).fontSize(12).fontColor('#1565C0')
          }
          .width('100%').height(52).padding({ left: 16, right: 16 })
          .backgroundColor(idx % 2 === 0 ? Color.White : '#FAFAFA')
          .border({ width: { bottom: 1 }, color: '#F0F0F0', style: BorderStyle.Solid })
          .onClick(() => {
            this.contentClickCount++
            this.appendLog(`📄 内容行 ${item} 被点击(工具栏透传成功)×${this.contentClickCount}`)
          })
        })
      }
      .width('100%')

      // 顶层:悬浮工具栏
      Stack({ alignContent: Alignment.TopStart }) {
        // 工具栏背景(半透明)
        Column()
          .width('100%').height(56)
          .backgroundColor('#CC1565C0')  // 半透明蓝

        // 工具栏按钮行
        Row({ space: 8 }) {
          Button('📌 收藏').height(36).fontSize(12).borderRadius(8)
            .backgroundColor('#2196F3').fontColor(Color.White)
            .onClick(() => {
              this.toolbarClickLabel = '📌 收藏'
              this.appendLog('📌 工具栏:收藏按钮点击')
            })
          Button('🔗 分享').height(36).fontSize(12).borderRadius(8)
            .backgroundColor('#4CAF50').fontColor(Color.White)
            .onClick(() => {
              this.toolbarClickLabel = '🔗 分享'
              this.appendLog('🔗 工具栏:分享按钮点击')
            })
          Button('⋯ 更多').height(36).fontSize(12).borderRadius(8)
            .backgroundColor('#FF9800').fontColor(Color.White)
            .onClick(() => {
              this.toolbarClickLabel = '⋯ 更多'
              this.appendLog('⋯ 工具栏:更多按钮点击')
            })
        }
        .height(56).padding({ left: 12, right: 12 })
      }
      .width('100%').height(56)
      // 关键:动态分区拦截
      .onTouchIntercept((event: TouchEvent) => {
        const x = event.touches[0]?.x ?? 0
        const y = event.touches[0]?.y ?? 0
        // 仅按钮区(y 在工具栏范围内且 x < 按钮总宽度)拦截
        if (y < this.TOOLBAR_HEIGHT && x < this.TOOLBAR_BTN_WIDTH) {
          // 按钮区:Block,工具栏消费事件
          return HitTestMode.Block
        }
        // 其他区域:Transparent,穿透到下方内容区
        return HitTestMode.Transparent
      })

      // 事件日志(固定在底部)
      Column({ space: 4 }) {
        Row() {
          Text(`工具栏操作:${this.toolbarClickLabel}`).fontSize(11).fontColor('#1565C0').layoutWeight(1)
          Text(`内容点击:×${this.contentClickCount}`).fontSize(11).fontColor('#FF9800')
        }.width('100%')
        ForEach(this.log.slice(0, 5), (item: string, idx: number) => {
          Text(item).fontSize(10).fontColor(idx === 0 ? '#1565C0' : '#BDBDBD').width('100%')
        })
      }
      .width('100%').padding(12).backgroundColor('#F9F9F9')
      .border({ width: { top: 1 }, color: '#E0E0E0', style: BorderStyle.Solid })
      .position({ x: 0, y: 280 })
    }
    .width('100%').height('100%').backgroundColor(Color.White)
  }
}

运行效果如图:


六、综合实战:四场景触摸拦截演示页面

6.1 功能说明

以下综合示例整合本文所有核心知识点,单页面通过 Tab 切换展示四个场景:

  1. 模式对比 :四种 HitTestMode 实时切换对比,显示触发日志
  2. 分区拦截:左右分区动态返回不同模式,演示坐标驱动拦截
  3. 圆形区域 :矩形容器只有圆内有效,圆外返回 None
  4. 悬浮工具栏:工具栏覆盖内容区,按钮区拦截,空白区透传

6.2 完整可运行代码

typescript 复制代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct TouchInterceptAllDemo {
  // ── Tab 控制 ──────────────────────────────────────
  @State activeTab: number = 0
  private tabs: string[] = ['🔀 模式', '↔ 分区', '⭕ 圆形', '🛠 工具栏']

  // ── Tab0:模式对比 ────────────────────────────────
  @State t0Mode: number = 0
  @State t0Log: string[] = ['等待触摸...']
  @State t0Parent: number = 0
  @State t0Child: number = 0
  private modeNames: string[] = ['Default', 'Block', 'Transparent', 'None']
  private modeBg: string[] = ['#E3F2FD', '#FFEBEE', '#E8F5E9', '#FFF9C4']

  // ── Tab1:分区拦截 ────────────────────────────────
  @State t1Log: string[] = ['等待触摸...']
  @State t1Zone: string = '---'

  // ── Tab2:圆形区域 ────────────────────────────────
  @State t2Log: string[] = ['等待触摸...']
  @State t2InCircle: boolean = false

  // ── Tab3:悬浮工具栏 ──────────────────────────────
  @State t3Log: string[] = ['等待操作...']
  @State t3ContentCount: number = 0
  @State t3ToolLabel: string = '未操作'

  private appendLog(arr: string[], msg: string): string[] {
    return [msg, ...arr.slice(0, 7)]
  }

  private getHitMode(idx: number): HitTestMode {
    if (idx === 0) return HitTestMode.Default
    if (idx === 1) return HitTestMode.Block
    if (idx === 2) return HitTestMode.Transparent
    return HitTestMode.None
  }

  @Builder
  Tab0() {
    Column({ space: 12 }) {
      // 模式选择
      Row({ space: 6 }) {
        ForEach(this.modeNames, (name: string, idx: number) => {
          Text(name).fontSize(11).layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 7, bottom: 7 })
            .backgroundColor(this.t0Mode === idx ? '#1565C0' : '#F5F5F5')
            .fontColor(this.t0Mode === idx ? Color.White : '#555555')
            .borderRadius(8).animation({ duration: 120, curve: Curve.EaseOut })
            .onClick(() => {
              this.t0Mode = idx
              this.t0Parent = 0; this.t0Child = 0
              this.t0Log = [`切换:HitTestMode.${name}`]
            })
        })
      }.width('92%')

      // 演示区
      Stack({ alignContent: Alignment.Center }) {
        Column().width('100%').height('100%')
          .backgroundColor(this.modeBg[this.t0Mode]).borderRadius(14)
          .animation({ duration: 200, curve: Curve.EaseOut })
        Column({ space: 8 }) {
          Text(`父:HitTestMode.${this.modeNames[this.t0Mode]}`).fontSize(12).fontColor('#555555')
          Button('子组件按钮').height(40).fontSize(13).borderRadius(10)
            .backgroundColor('#1565C0').fontColor(Color.White)
            .onClick(() => { this.t0Child++; this.t0Log = this.appendLog(this.t0Log, `子组件点击 ×${this.t0Child}`) })
          Row({ space: 16 }) {
            Text(`父: ×${this.t0Parent}`).fontSize(12).fontColor('#FF9800').fontWeight(FontWeight.Bold)
            Text(`子: ×${this.t0Child}`).fontSize(12).fontColor('#1565C0').fontWeight(FontWeight.Bold)
          }
        }
      }
      .width('92%').height(160).borderRadius(14)
      .border({ width: 1.5, color: '#E0E0E0', style: BorderStyle.Solid })
      .onTouchIntercept((event: TouchEvent) => {
        const x = event.touches[0]?.x ?? 0
        this.t0Log = this.appendLog(this.t0Log, `HitTest(${x.toFixed(0)}) → ${this.modeNames[this.t0Mode]}`)
        return this.getHitMode(this.t0Mode)
      })
      .onClick(() => { this.t0Parent++; this.t0Log = this.appendLog(this.t0Log, `父容器点击 ×${this.t0Parent}`) })

      Column({ space: 3 }) {
        ForEach(this.t0Log, (item: string, idx: number) => {
          Text(item).fontSize(10).fontColor(idx === 0 ? '#1565C0' : '#BDBDBD').width('100%')
        })
      }
      .width('92%').padding(10).backgroundColor('#F9F9F9').borderRadius(10)
      .border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%').alignItems(HorizontalAlign.Center).margin({ top: 14 })
  }

  @Builder
  Tab1() {
    Column({ space: 12 }) {
      Text('触点 x<150 → Transparent  |  x≥150 → Block').fontSize(11).fontColor('#888888').margin({ top: 14 })
      Text(`当前:${this.t1Zone}`).fontSize(12).fontColor('#1565C0').fontWeight(FontWeight.Bold)
        .padding({ left: 10, right: 10, top: 5, bottom: 5 }).backgroundColor('#E3F2FD').borderRadius(8)

      Stack() {
        Row() {
          Column().width('50%').height('100%').backgroundColor('#E8F5E9')
          Column().width('50%').height('100%').backgroundColor('#FFEBEE')
        }.width('100%').height('100%').borderRadius(14)
        Row() {
          Column({ space: 4 }) {
            Text('左区').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
            Text('Transparent').fontSize(10).fontColor('#43A047')
          }.layoutWeight(1).justifyContent(FlexAlign.Center)
          Column({ space: 4 }) {
            Text('右区').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#C62828')
            Text('Block').fontSize(10).fontColor('#E53935')
          }.layoutWeight(1).justifyContent(FlexAlign.Center)
        }.width('100%').height('100%')
        Button('子按钮(左区可触发)').height(40).fontSize(12).borderRadius(10)
          .backgroundColor('#1565C0').fontColor(Color.White)
          .onClick(() => { this.t1Log = this.appendLog(this.t1Log, '子按钮点击(左区透传成功)') })
      }
      .width('92%').height(160).borderRadius(14)
      .border({ width: 1.5, color: '#E0E0E0', style: BorderStyle.Solid })
      .onTouchIntercept((event: TouchEvent) => {
        const x = event.touches[0]?.x ?? 0
        if (x < 150) {
          this.t1Zone = `左区(${x.toFixed(0)}) → Transparent`
          return HitTestMode.Transparent
        }
        this.t1Zone = `右区(${x.toFixed(0)}) → Block`
        this.t1Log = this.appendLog(this.t1Log, `右区 Block 拦截 x=${x.toFixed(0)}`)
        return HitTestMode.Block
      })
      .onClick(() => { this.t1Log = this.appendLog(this.t1Log, '父容器 Block 点击触发') })

      Column({ space: 3 }) {
        ForEach(this.t1Log, (item: string, idx: number) => {
          Text(item).fontSize(10).fontColor(idx === 0 ? '#1565C0' : '#BDBDBD').width('100%')
        })
      }
      .width('92%').padding(10).backgroundColor('#F9F9F9').borderRadius(10)
      .border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%').alignItems(HorizontalAlign.Center)
  }

  @Builder
  Tab2() {
    Column({ space: 12 }) {
      Text('圆内 Default 响应,圆外矩形角 None 禁用').fontSize(11).fontColor('#888888').margin({ top: 14 })
      Text(this.t2InCircle ? '✅ 圆内:响应' : '❌ 圆外:禁用').fontSize(14).fontWeight(FontWeight.Bold)
        .fontColor(this.t2InCircle ? '#4CAF50' : '#F44336')
        .padding({ left: 12, right: 12, top: 6, bottom: 6 })
        .backgroundColor(this.t2InCircle ? '#E8F5E9' : '#FFEBEE').borderRadius(8)
        .animation({ duration: 150, curve: Curve.EaseOut })

      Stack({ alignContent: Alignment.Center }) {
        Column().width(200).height(200).backgroundColor('#EDE7F6').borderRadius(0)
        Column().width(200).height(200).borderRadius(100).backgroundColor('#7B1FA2').opacity(0.15)
        Text('点击圆内有效\n点击边角无效').fontSize(12).fontColor('#4A148C').textAlign(TextAlign.Center)
          .fontWeight(FontWeight.Bold)
      }
      .width(200).height(200)
      .border({ width: 1.5, color: '#CE93D8', style: BorderStyle.Solid })
      .onTouchIntercept((event: TouchEvent) => {
        const x = event.touches[0]?.x ?? 0
        const y = event.touches[0]?.y ?? 0
        const dx = x - 100; const dy = y - 100
        const inCircle = Math.sqrt(dx * dx + dy * dy) <= 100
        this.t2InCircle = inCircle
        this.t2Log = this.appendLog(this.t2Log,
          `(${x.toFixed(0)},${y.toFixed(0)}) 距圆心${Math.sqrt(dx*dx+dy*dy).toFixed(0)}px → ${inCircle ? 'Default' : 'None'}`)
        return inCircle ? HitTestMode.Default : HitTestMode.None
      })
      .onClick(() => { this.t2Log = this.appendLog(this.t2Log, '⭕ 圆内点击响应') })

      Column({ space: 3 }) {
        ForEach(this.t2Log, (item: string, idx: number) => {
          Text(item).fontSize(10).fontColor(idx === 0 ? '#7B1FA2' : '#BDBDBD').width('100%')
        })
      }
      .width('92%').padding(10).backgroundColor('#F9F9F9').borderRadius(10)
      .border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%').alignItems(HorizontalAlign.Center)
  }

  @Builder
  Tab3() {
    Stack({ alignContent: Alignment.TopStart }) {
      // 底层内容
      Column({ space: 0 }) {
        ForEach(['内容行 A', '内容行 B', '内容行 C', '内容行 D', '内容行 E'], (item: string, idx: number) => {
          Row({ space: 10 }) {
            Text(`${idx+1}`).fontSize(13).fontColor('#9E9E9E').width(20)
            Text(item).fontSize(13).fontColor('#333333').layoutWeight(1)
            Text(`×${this.t3ContentCount}`).fontSize(11).fontColor('#1565C0')
          }
          .width('100%').height(50).padding({ left: 14, right: 14 })
          .backgroundColor(idx % 2 === 0 ? Color.White : '#FAFAFA')
          .border({ width: { bottom: 1 }, color: '#F0F0F0', style: BorderStyle.Solid })
          .onClick(() => { this.t3ContentCount++; this.t3Log = this.appendLog(this.t3Log, `内容"${item}"点击 ×${this.t3ContentCount}`) })
        })
      }.width('100%')

      // 顶层工具栏
      Stack({ alignContent: Alignment.TopStart }) {
        Column().width('100%').height(50).backgroundColor('#CC1565C0')
        Row({ space: 8 }) {
          Button('收藏').height(34).fontSize(12).borderRadius(8).backgroundColor('#2196F3').fontColor(Color.White)
            .onClick(() => { this.t3ToolLabel = '收藏'; this.t3Log = this.appendLog(this.t3Log, '工具栏:收藏') })
          Button('分享').height(34).fontSize(12).borderRadius(8).backgroundColor('#4CAF50').fontColor(Color.White)
            .onClick(() => { this.t3ToolLabel = '分享'; this.t3Log = this.appendLog(this.t3Log, '工具栏:分享') })
          Button('更多').height(34).fontSize(12).borderRadius(8).backgroundColor('#FF9800').fontColor(Color.White)
            .onClick(() => { this.t3ToolLabel = '更多'; this.t3Log = this.appendLog(this.t3Log, '工具栏:更多') })
          Text(this.t3ToolLabel).fontSize(11).fontColor(Color.White).layoutWeight(1).textAlign(TextAlign.End)
        }.height(50).padding({ left: 10, right: 10 })
      }
      .width('100%').height(50)
      .onTouchIntercept((event: TouchEvent) => {
        const x = event.touches[0]?.x ?? 0
        // 按钮区(前 220vp)拦截,右侧透传到内容
        return x < 220 ? HitTestMode.Block : HitTestMode.Transparent
      })

      // 日志
      Column({ space: 3 }) {
        ForEach(this.t3Log.slice(0, 4), (item: string, idx: number) => {
          Text(item).fontSize(10).fontColor(idx === 0 ? '#1565C0' : '#BDBDBD').width('100%')
        })
      }
      .width('100%').padding(10).backgroundColor('#F9F9F9')
      .border({ width: { top: 1 }, color: '#E0E0E0', style: BorderStyle.Solid })
      .position({ x: 0, y: 280 })
    }
    .width('100%').height(380)
  }

  build() {
    Column({ space: 0 }) {
      Text('ArkUI 触摸拦截综合演示')
        .fontSize(18).fontWeight(FontWeight.Bold).fontColor(Color.White)
        .width('100%').textAlign(TextAlign.Center)
        .padding({ top: 16, bottom: 12 })
        .backgroundColor('#1565C0')

      Row() {
        ForEach(this.tabs, (tab: string, index: number) => {
          Text(tab)
            .fontSize(12).layoutWeight(1).textAlign(TextAlign.Center)
            .fontColor(this.activeTab === index ? '#1565C0' : '#9E9E9E')
            .fontWeight(this.activeTab === index ? FontWeight.Bold : FontWeight.Normal)
            .padding({ top: 10, bottom: 10 })
            .border({ width: { bottom: this.activeTab === index ? 2 : 0 },
              color: '#1565C0', style: BorderStyle.Solid })
            .onClick(() => { this.activeTab = index })
        })
      }
      .width('100%').backgroundColor(Color.White)
      .border({ width: { bottom: 1 }, color: '#E0E0E0', style: BorderStyle.Solid })

      Scroll() {
        Column() {
          if (this.activeTab === 0) {
            this.Tab0()
          } else if (this.activeTab === 1) {
            this.Tab1()
          } else if (this.activeTab === 2) {
            this.Tab2()
          } else {
            this.Tab3()
          }
        }
        .width('100%').padding({ bottom: 24 })
      }
      .layoutWeight(1).width('100%')
    }
    .width('100%').height('100%').backgroundColor(Color.White)
  }
}


总结

  1. 动态 vs 静态onTouchIntercepthitTestBehavior 的动态增强版,每次 HitTest 前触发回调,可根据触点坐标或业务状态动态返回 不同的 HitTestMode
  2. 四种模式Default(自身+子均响应)、Block(自身响应+子被拦截)、Transparent(自身透明+子正常)、None(全部禁用)
  3. 分区控制 :通过判断 event.touches[0].x/y 坐标,实现左右/上下分区不同拦截策略,覆盖不规则形状场景
  4. 必须同步 :回调内禁止异步操作,状态需提前用 @State 缓存;复杂计算建议在 onTouch 事件中预处理
  5. 性能意识onTouchIntercept 在触摸高频阶段执行,回调逻辑务必轻量,避免影响手势响应流畅度
  6. 实战选型 :简单固定场景用 hitTestBehavior;需要动态分区、不规则形状、条件拦截的场景用 onTouchIntercept

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

相关推荐
是稻香啊5 小时前
HarmonyOS6 ArkUI .restoreId() 滚动位置恢复全解析
harmonyos6
是稻香啊5 小时前
HarmonyOS6 ArkUI 子组件触摸测试控制(onChildTouchTest)全面解析与实战演示
华为·harmonyos·harmonyos6
生活很暖很治愈5 小时前
Linux——UDP编程&通信
linux·服务器·c++·ubuntu
炽天使3286 小时前
龙虾尝鲜记(3)——装ubuntu(续)
linux·运维·ubuntu
海兰6 小时前
利用Elastic构建欺诈检测框架
大数据·人工智能·ubuntu
岚天start6 小时前
OpenClaw大龙虾部署(国内环境)详细指南
ubuntu·openclaw·大龙虾·国内环境
敲代码还房贷6 小时前
Ubuntu24安装xcp_d
linux·ubuntu·医学生·afni
MIXLLRED7 小时前
解决:Ubuntu系统引导修复操作步骤
linux·windows·ubuntu
fei_sun7 小时前
【鸿蒙智能硬件】(一)环境配置
华为·harmonyos