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

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

相关推荐
LN花开富贵4 小时前
Ubuntu aarch64 架构安装 NoMachine 远程控制 避坑与实战
linux·运维·笔记·学习·ubuntu·嵌入式
取经蜗牛4 小时前
Windows 11 WSL + Ubuntu 24.04 安装指南
linux·windows·ubuntu
aqi005 小时前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony
环信即时通讯云7 小时前
环信Flutter UIKit适配鸿蒙实战指南
flutter·华为·harmonyos
Swift社区8 小时前
鸿蒙 PC 应用启动优化全解析
华为·harmonyos
bush48 小时前
linux开发板连接virtualbox虚拟机ubuntu的usb网卡,访问外网方法。
linux·运维·ubuntu
大明者省9 小时前
完整 Ubuntu 服务器 XFCE 桌面 + XRDP 远程桌面 部署使用全流程
运维·服务器·ubuntu
richard_yuu9 小时前
鸿蒙本地数据存储实战|Preferences 封装、数据隔离与隐私合规存储方案
android·华为·harmonyos
Lynnb10 小时前
Mac电脑烧录 RK3588 鸿蒙开发板固件教程
华为·harmonyos·鸿蒙系统