大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

地图页面上叠一个登录面板、地点详情卡片或业务弹窗,是很常见的 UI 结构。问题也很典型:明明手指按在最上面的弹窗上,滑动弹窗内容时,下面的地图却跟着平移;有时点击弹窗空白区域,地图还能收到点击。

这类现象很容易被误判成地图手势冲突。实际上,华为开发者联盟已经给出了几乎完全一致的官方诊断案例:上层组件如果设置了 HitTestMode.Transparent,不会阻塞兄弟节点的触摸测试,因此下层地图仍可能进入触摸测试结果并继续接收事件;官方给出的修改建议是将上层组件改为 HitTestMode.Default 或 HitTestMode.Block。

这篇文章就围绕这个问题搭一个最小场景,把 hitTestBehavior、触摸测试、地图手势以及排查顺序串起来。

一、问题不一定出在地图,而可能出在“谁被命中了”

先看一个典型页面结构:

Stack
├── MapComponent        // 下层:地图
└── Scroll / Column     // 上层:业务面板

视觉上,后面的业务面板盖住地图,因此很容易形成一个直觉:既然看不到地图,地图自然也收不到手势。

但视觉遮挡和事件命中不是一回事。

ArkUI 在触屏事件真正分发之前,会先根据按压点和组件区域执行触摸测试,收集需要响应事件的组件,然后再依据触摸测试结果分发后续事件。hitTestBehavior 控制的正是这个阶段,而不是简单控制组件是否“看得见”。华为官方的触摸交互文档也把相关能力归入“触摸测试控制”。

所以排查这类问题时,真正应该问的不是:

地图为什么抢了弹窗的手势?

而是:

手指落在弹窗区域后,为什么下层地图仍然进入了触摸测试结果?

这个问题一旦换过来,定位方向就清楚很多。

【建议插图1:Stack 中 MapComponent 与上层业务面板的层级示意图,并标出手指触摸区域】

二、官方案例已经指出了关键:HitTestMode.Transparent

华为官方 FAQ 描述的现象是“登录页面覆盖在地图之上,滑动登录页面时地图同步滑动”。官方定位方式是全局搜索 hitTestBehavior,最终发现登录页面被设置成:

.hitTestBehavior(HitTestMode.Transparent)

问题就在 Transparent。

官方对这一模式的说明非常关键:设置为 Transparent 后,自身和子节点仍参与触摸测试,但不会阻塞兄弟节点的触摸测试。因此在 Stack 这种上下叠放的结构里,上层业务区域和下层地图是兄弟节点时,下层地图仍有机会接收到触摸事件。

这也是为什么 Transparent 不能简单理解成“透明背景”。

这里的 Transparent 描述的是触摸测试策略,和:

.backgroundColor(Color.Transparent)

完全不是一回事。

一个组件视觉上完全透明,并不意味着触摸一定穿透;反过来,一个不透明组件如果配置了 HitTestMode.Transparent,同样可能让下层兄弟节点继续参与触摸测试。

三、Transparent 本身没有错,错的是使用场景

HitTestMode.Transparent 并不是一个应该避免的模式。

官方 overlay 文档反而给出了它非常合适的使用场景:如果浮层只是水印之类的展示内容,不应该妨碍用户操作下面的组件,可以在浮层最外层设置:

.hitTestBehavior(HitTestMode.Transparent)

这样浮层存在,但下面的内容仍然能够交互。

因此可以把两种需求分开理解:

场景希望下层继续交互吗处理方向
水印、纯展示浮层是可使用 Transparent
登录面板、地点详情卡片、操作弹窗否不应使用 Transparent
弹窗完全接管覆盖区域否使用官方建议的 Default 或按需求评估 Block
当前组件自身不应响应,并允许其他层响应是可进一步评估 HitTestMode.None

官方另一个 FAQ 也给出了 HitTestMode.None 的实际用途:让当前组件不响应触摸,同时不影响同位置其他层级组件的点击响应。

所以真正需要记住的不是“Transparent 好还是不好”,而是:

当前这个 UI 层到底应该不应该阻止下面的兄弟节点进入触摸测试。

四、HarmonyOS 7 下先确认版本和能力边界

本文以 HarmonyOS 7 为开发背景。华为当前版本资料显示,HarmonyOS 7.0 对应 API 26.0.0,官方建议开发者使用 26.0.0 开发套件进行升级适配。

本文涉及的核心 UI 能力属于 ArkUI。旧的 touchable 已从 API version 9 起废弃,官方明确建议使用 hitTestBehavior 替代,因此新代码没有必要再通过 touchable 解决这类触摸测试问题。

地图部分使用 Map Kit 的 MapComponent。当前 Map Kit 文档显示,地图能力可以通过:

import { MapComponent, mapCommon, map } from '@kit.MapKit';

接入,MapComponent 负责地图 UI 展示,并通过回调获得 MapComponentController。当前 Map Kit 模块文档列出的核心对象也包括 MapComponent、MapComponentController 和 MapEventManager。

本文只显示固定中心点地图,不读取用户当前位置,因此示例本身不需要为了这个问题额外申请定位权限。如果业务进一步开启“我的位置”,官方实践明确要求申请 ohos.permission.LOCATION 和 ohos.permission.APPROXIMATELY_LOCATION。

五、先把 Map Kit 的最小前置配置补齐

如果项目已经能够正常显示地图,可以直接跳到下一节。

对于 Map Kit,除了代码导入,还需要在 AppGallery Connect 为对应应用开通地图服务,并在 Entry 模块的 module.json5 中配置应用的 client_id。官方资料特别强调,这里需要使用应用的 Client ID,而不是项目的 Client ID。

最小配置形式如下:

{
  "module": {
    // 其他现有配置……
    "metadata": [
      {
        "name": "client_id",
        "value": "这里填写 AppGallery Connect 中应用的 Client ID"
      }
    ]
  }
}

如果地图无法初始化,先不要把问题归因于 hitTestBehavior。Map Kit 官方错误码文档明确列出了身份校验失败、未开通 Map Kit、网络不可用等情况;其中应用身份校验失败也可能由 module.json5 中 client_id 与 AppGallery Connect 不一致或公钥指纹配置异常引起。

也就是说,排查触摸穿透的前提应该是:地图本身已经能够正常显示和交互。

【建议插图2:AppGallery Connect 中应用 Client ID 与 Entry 模块 module.json5 的 metadata 配置位置对照】

六、搭一个最小的“地图 + 上层滚动面板”

下面代码按照官方 MapComponent 的初始化方式组织一个最小页面。重点不是地图业务,而是让 MapComponent 和业务面板成为 Stack 中互相覆盖的兄弟节点。

import { MapComponent, mapCommon, map } from '@kit.MapKit';
import { AsyncCallback } from '@kit.BasicServicesKit';

@Entry
@Component
struct HitTestMapDemo {
  private mapOptions?: mapCommon.MapOptions;
  private mapCallback?: AsyncCallback<map.MapComponentController>;
  private mapController?: map.MapComponentController;

  aboutToAppear(): void {
    this.mapOptions = {
      position: {
        target: {
          latitude: 39.9,
          longitude: 116.4
        },
        zoom: 12
      }
    };

    this.mapCallback = async (err, controller) => {
      if (!err) {
        this.mapController = controller;

        this.mapController.on('mapClick', (position) => {
          console.info(
            `mapClick: ${position.latitude}, ${position.longitude}`
          );
        });
      }
    };
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      MapComponent({
        mapOptions: this.mapOptions,
        mapCallback: this.mapCallback
      })
        .width('100%')
        .height('100%')

      Scroll() {
        Column({ space: 16 }) {
          Text('地点详情')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)

          Text('这里模拟覆盖在地图上的业务面板。')
            .fontSize(16)

          ForEach([1, 2, 3, 4, 5, 6], (item: number) => {
            Text(`业务内容 ${item}`)
              .width('100%')
              .height(64)
              .padding(16)
              .backgroundColor('#F2F3F5')
              .borderRadius(12)
          })
        }
        .width('100%')
        .padding(20)
      }
      .width('100%')
      .height(360)
      .backgroundColor(Color.White)
      .borderRadius({ topLeft: 24, topRight: 24 })
      .hitTestBehavior(HitTestMode.Transparent)
      .onClick(() => {
        console.info('panel click');
      })
      .gesture(
        LongPressGesture()
          .onAction(() => {
            console.info('panel long press');
          })
      )
    }
    .width('100%')
    .height('100%')
  }
}

地图初始化方式来自 Map Kit 官方 MapComponent 文档;地图点击监听的使用形式也可以在华为官方 Map Kit 场景实践中找到对应示例。

这里故意留下问题:

.hitTestBehavior(HitTestMode.Transparent)

按照华为官方针对“登录页面覆盖地图”的问题诊断,这样配置后,上层组件不会阻塞下层兄弟节点的触摸测试,因此就具备了出现“面板在响应,地图也响应”的条件。

【建议插图3:页面运行结构,上半部分为地图,下半部分白色 Scroll 面板覆盖地图】

七、真正的修复只有一行,但先别急着改地图

对于这个明确的场景,官方修改建议是把上层组件调整为 HitTestMode.Default 或 HitTestMode.Block。

因此最小修改可以先使用:

.hitTestBehavior(HitTestMode.Default)

完整位置是:

Scroll() {
  // 面板内容
}
.width('100%')
.height(360)
.backgroundColor(Color.White)
.hitTestBehavior(HitTestMode.Default)

这里建议先从 Default 开始验证。

原因不是说 Block “不能用”,而是本文的目标只是修复一次错误的 Transparent 配置。官方已经明确把 Default 和 Block 都列为该问题的修改方案;如果弹窗内部还有按钮、输入框、滚动区域、长按手势等复杂交互,采用 Default 做最小修改,再逐项验证内部交互,通常更容易把问题范围控制住。对于需要更明确阻断策略的页面,再结合实际组件树评估 Block。

不要为了阻止地图滑动,第一时间去关闭 Map Kit 的缩放、移动等手势。Map Kit 本身就提供移动、缩放、旋转等地图交互能力。 如果问题只发生在弹窗覆盖区域,那么先修改地图配置,实际上是在绕过触摸命中问题,而没有处理事件为什么会到达地图这一层。

八、点击、滑动、长按应该分别验证

触摸穿透修好以后,不建议只验证“地图不动了”。

这个页面至少应该检查三组交互。

点击。 点击面板内容区域,确认面板自己的点击逻辑正常;同时观察 mapClick 日志,不应该因为点击上层面板而触发下面地图的点击逻辑。再点击没有被面板覆盖的地图区域,地图应继续保持原有交互。

滑动。 在面板内部上下滑动 Scroll,确认业务内容能够滚动,同时观察下面地图是否仍发生平移。然后直接在裸露地图区域滑动,确认地图自身的移动手势仍然可用。

长按。 在面板区域触发 LongPressGesture,确认上层业务手势仍能工作,并检查下层交互是否被意外触发。

这里的重点是“按区域验证”,而不是简单判断整个页面能不能滑。

【建议插图4:修改前后对比——左侧 Transparent 时滑动面板地图同步移动,右侧 Default 时只有面板滚动】

【建议插图5:DevEco Studio 日志窗口,分别展示 panel click、panel long press 和裸露地图区域 mapClick 的日志】

九、为什么这个问题不只会发生在地图

地图只是最容易观察出问题的组件之一,因为地图天然包含平移、缩放、旋转等手势,触摸一旦穿过去,视觉反馈非常明显。

同样的排查思路也适用于视频控制层、Canvas/XComponent 绘制区域、Slider、自定义手势组件以及其他可交互内容。

华为官方甚至给出了一个反向案例:在 Slider 上覆盖自定义 Row 时,恰恰需要给上层 Row 配置 HitTestMode.Transparent,让下面的 Slider 继续接收滑动。

这两个案例放在一起特别容易理解:

水印 / Slider 装饰层
    ↓
希望下层继续响应
    ↓
Transparent 有价值

而:

登录面板 / 地点详情弹窗
    ↓
不希望地图继续响应
    ↓
Transparent 就可能成为问题来源

所以 hitTestBehavior 不是地图专属属性,也不是“解决手势冲突的万能开关”。它控制的是更靠前的触摸测试过程。

十、一个容易走偏的方向:把事件穿透当成手势竞争

ArkUI 确实存在手势冲突、手势优先级、自定义手势判定等更复杂的问题,但不要一看到“两个组件都动”就直接进入手势竞争分析。

对于“上层组件 + 下层兄弟组件”的覆盖结构,可以先判断下层组件为什么被命中。

官方自定义事件分发文档说明,ArkUI 会先进行触摸测试,收集需要响应的组件,再根据结果进行触屏事件分发。 因此,如果触摸测试阶段就因为 Transparent 让下层兄弟节点进入结果集,那么继续研究 PanGesture 优先级,很可能把简单问题复杂化。

一个比较实用的判断方法是:

如果去掉 .hitTestBehavior(HitTestMode.Transparent) 后问题直接消失,就先把它归入触摸命中配置问题,而不是地图手势算法问题。

十一、建立一套触摸穿透检查清单

以后再遇到“上层操作、下层也响应”,可以按下面顺序排查:

  1. 先画组件层级。 确认两个响应组件究竟是父子关系、兄弟关系,还是浮层与被覆盖组件的关系。
  2. 全局搜索 hitTestBehavior。 特别检查覆盖区域及其外层容器有没有 HitTestMode.Transparent。
  3. 检查需求到底要不要穿透。 水印、装饰层可能就是需要 Transparent;登录面板、详情弹窗通常不是。
  4. 把异常配置恢复为 Default 做最小验证。 如果问题消失,再决定是否有必要使用其他模式。
  5. 分开验证点击、滑动、长按。 不要只验证其中一种手势。
  6. 检查触摸区域。 ArkUI 的触摸测试与组件响应热区有关;如果只在局部区域异常,还要继续检查 responseRegion 等触摸热区配置。官方说明 responseRegion 会影响点击、触摸、拖拽、鼠标及手势事件的分发。
  7. 最后再进入复杂手势冲突分析。 只有触摸测试配置正确以后,才有必要继续研究父子手势竞争、自定义事件分发等问题。

这套顺序的价值在于先排除成本最低、确定性最高的问题。官方针对“登录页面覆盖地图”的案例,本身也是通过全局搜索 hitTestBehavior 定位到 HitTestMode.Transparent。

十二、HarmonyOS 7 项目还要注意什么

HarmonyOS 7 当前对应 API 26.0.0。升级已有工程时,除了确认代码能否调用,还应结合 targetSdkVersion、API 行为变化和目标设备进行兼容性验证;华为官方的 26.0.0 升级指导也建议在新旧系统设备上完成兼容性测试。

对于本文这个问题,不需要为了 HarmonyOS 7 引入新的系统权限,也不需要修改 Map Kit 的手势能力。真正的修复点仍然是 ArkUI 上层组件的触摸测试策略。

另外,旧项目如果还在通过已经废弃的 touchable 处理类似交互,应该注意官方从 API version 9 起已经将其标记为废弃,并建议改用 hitTestBehavior。

开发经验总结

这类问题表面上看起来像“地图抢手势”,真正需要检查的却是 ArkUI 的触摸测试。

可以留下四个结论。

第一,视觉覆盖不等于事件阻断。组件盖在地图上,只能说明绘制层级发生了覆盖,不能据此判断下层组件一定收不到触摸。

第二,HitTestMode.Transparent 是有明确用途的能力,而不是错误配置本身。水印、Slider 装饰层等希望下层继续工作的场景正需要它;交互弹窗覆盖地图时则通常不符合这个需求。官方文档中的这两类案例正好形成对照。

第三,碰到上下两层同时响应,先查 hitTestBehavior,再查复杂手势冲突。华为针对“登录页面覆盖地图”的官方案例已经给出了非常直接的定位路径和修改建议:Transparent 导致下层地图继续收到事件,可改为 Default 或 Block。

第四,修完以后必须按点击、滑动、长按分别验证。触摸测试影响的是事件收集与后续分发,不能因为某一种手势恢复正常,就默认整个交互链路都已经符合预期。

如果项目里存在地图、视频、Canvas、XComponent 等大面积可交互底层内容,可以顺手检查一下它们上方的遮罩、弹窗和自定义浮层。很多看起来很复杂的“手势冲突”,真正需要改的可能就是一个 hitTestBehavior。

参考资料

  1. 《上层组件滑动时下层组件也同步滑动》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-863

  2. 《触摸交互控制》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/touch-interactions

  3. 《浮层》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/api/ts-universal-attributes-overlay

  4. 《触摸热区设置》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-universal-attributes-touch-target

  5. 《如何实现Slider滑块的自定义设置》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1513

  6. 《Map Kit(地图服务)模块描述》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/map-module-desc

  7. 《快捷骑行体验》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/doccenter-scenario/bpta-shared-bicycle

  8. 《应用升级适配指导——向26.0.0升级》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/en/doc/harmonyos-releases/upgrade-adaptation

  9. 《版本号格式调整说明》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/doccenter-release-notes/version-number-26

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐