我第一次看地图长按交互时,脑子里的对象只有一个:地图上的点。红色图钉是点,底图上的餐厅名称也是点,用户长按它们,不就是“长按一个点”吗?于是我打算只放一个计数器,再用一段统一回调把名称和坐标显示出来。
后来我在页面里把两个监听写在一起读,才发现自己把两种来源不同、对象能力也不同的东西揉扁了。页面手动添加的 Marker 是我握在手里的对象:我知道它的标题,能通过 getPosition() 取它的位置。底图上的 Poi 则来自地图数据:回调给的是 name、id 和 position。它们都能成为用户长按的目标,不代表它们应该走同一条记录路径。
这篇不讨论搜索相关性,也不把当前页面的等待文本写成 SDK 回调已经收到。代码注册了 onMarkerLongClick 与 onPoiLongClick,并为两者单独计数;要证明设备上真实发生过长按,仍需要地图初始化成功、合法认证和真机回调材料。现在能先确认的是对象语义和页面如何防止混记。
“没反应”常常不是一个问题
当用户说“我长按没反应”,我过去会只看数字有没有加一。现在我先问他按的是哪一种东西:是页面添加的 Article 08 长按 Marker,还是底图本身可识别的 POI?还是空白地图、普通点击,甚至是页面外面的按钮?这些动作在页面说明里已被明确区分,不能都拿来要求同一个监听响应。
这不是咬文嚼字。若把 Marker 和 POI 共用一个“长按成功”计数,即便数字加了,我也不知道用户长按了哪种对象。后续若要按 Marker 的标题创建测试记录,或者按 POI 的 id 做地点引用,信息就会在最开始被混掉。所谓没反应,可能是按错目标,也可能是对应监听没有被触发,不能从一个总数里猜。
这个流程图让我先放弃了“统一回调”的想法。统一展示可以在更高层做,但进入页面状态之前,来源必须保留。代码里使用两套计数,不是为了凑两个卡片,而是让一次变化本身就有归属。
回到初始化:监听不是凭空存在的

我第二个错误猜测是,只要在构建函数里写了地图组件,长按监听自然已经就位。看 onMapReady 才发现顺序不是这么回事。回调先需要给出有效的 controller,页面才能通过 controller.getEventManager() 拿到事件管理对象;再在这个对象上分别注册两个监听;之后才尝试添加测试 Marker。若初始化失败,页面把地图状态写为失败、Marker 状态写为未添加,并调用 markOperation 留下回调失败的操作说明。
this.mapController = controller;
this.mapEventManager = controller.getEventManager();
this.mapRuntime = {
mapState: '地图控制器已就绪,长按监听已注册',
markerState: '正在添加 Marker',
errorMessage: '暂无错误',
callbackCount: callbackCount
};
this.mapEventManager.onMarkerLongClick((marker: map.Marker) => {
// Marker 的处理在这里注册
});
this.mapEventManager.onPoiLongClick((poi: mapCommon.Poi) => {
// POI 的处理在这里注册
});
这里有一个很实际的排除过程。若 mapRuntime.mapState 仍是“等待 MapComponent 回调”,我就不应该继续责怪 Marker 或 POI。此时控制器还没有回来,监听也没有注册。若页面显示初始化失败,则先看 mapRuntime.errorMessage,不是去反复在空白地图上长按。若控制器就绪但 Marker 添加失败,Marker 目标本身可能不存在,但 POI 监听的注册是另一件事。把这些状态拆开,才能让“没反应”有具体位置。
我也不会把 MapComponent 出现在布局里当作“真实地图已经可交互”的证明。当前运行环境是否完成认证、地图是否真正加载、POI 是否可选,都不是从源码一眼就能得出的事实。页面状态是排查导航,不是替代设备记录。
两段回调,留下的是两类对象信息
Marker 回调里,代码先 marker.getPosition(),最后事件中写的是标题和经纬度;POI 回调直接使用 poi.name、poi.id 与 poi.position。这并不意味着 Marker 比 POI 更可靠,或者 POI 就是搜索结果。它只说明两种对象由不同回调提供,页面按各自能拿到的字段记录。
this.mapEventManager.onMarkerLongClick((marker: map.Marker) => {
const position = marker.getPosition();
this.evidenceState = {
query: this.evidenceState.query,
resultName: this.evidenceState.resultName,
reliability: this.evidenceState.reliability,
markerLongClickCount: this.evidenceState.markerLongClickCount + 1,
poiLongClickCount: this.evidenceState.poiLongClickCount,
lastEvent: `Marker 长按:${marker.getTitle()} @ ${position.latitude},${position.longitude}`
};
this.lastTriggerTime = timestamp();
this.operationSequence += 1;
this.lastOperationId = `MAP-${this.operationSequence}`;
});
POI 分支保持 markerLongClickCount 不动,只增加 poiLongClickCount,最后事件里还保留 poi.id。这个“不动”很关键。若两类回调都把两个数字一起加,界面看起来会很忙,数据却失去意义。分别保留旧值,才是页面在说“这次只有一类对象发生了变化”。
this.mapEventManager.onPoiLongClick((poi: mapCommon.Poi) => {
this.evidenceState = {
query: this.evidenceState.query,
resultName: this.evidenceState.resultName,
reliability: this.evidenceState.reliability,
markerLongClickCount: this.evidenceState.markerLongClickCount,
poiLongClickCount: this.evidenceState.poiLongClickCount + 1,
lastEvent: `POI 长按:${poi.name} (${poi.id}) @ ${poi.position.latitude},${poi.position.longitude}`
};
this.lastTriggerTime = timestamp();
this.operationSequence += 1;
this.lastOperationId = `MAP-${this.operationSequence}`;
});

我过去还想把这两段文字统一成“长按地点”。现在不会这么写。Marker 可以是业务主动放上去的测试锚点,也可以是应用维护的对象;POI 是地图底图或服务数据中被识别的地点对象。页面可以在以后把它们统一成某种“地点草稿”,但那一步必须显式写清来源。没有来源,后面就无法解释字段为何不同、谁该负责校对。
为什么单独计数比总计数更诚实
两个卡片分别显示 Marker 长按和 POI 长按次数。初看像是 Demo 的展示技巧,其实它给复测留了最短的路径。长按 Marker 后,期望是前者增加、后者不变;长按 POI 后,期望是后者增加、前者不变。任何一种“两个都没变”都不能直接归结为回调失效,因为还要看初始化和目标类型;但任何一种“按 Marker 却加到了 POI”都能立刻暴露记录边界写错。
这套检查没有偷换成“点击页面按钮就算长按成功”。现有页面也特意写了,计数只由 Map Kit 回调增加,没有模拟成功按钮。原因很直接:若我能自己点一下让数字加一,就再也分不清页面状态演示和真实手势回调了。对于依赖地图服务的部分,宁可留下等待,也不要制造一个好看的假结果。
结束监听也是对象边界的一部分
aboutToDisappear 中会在 mapEventManager 存在时依次调用 offMarkerLongClick() 与 offPoiLongClick()。这段代码让我又改掉一个习惯:页面离开后,不要笼统地说“关闭地图事件”,而要和注册时一样,按对象类型解除。注册分开、计数分开、解除也分开,读代码的人一眼就能知道这个页面没有把两类语义塞进一个黑盒。
aboutToDisappear(): void {
if (this.mapEventManager !== undefined) {
this.mapEventManager.offMarkerLongClick();
this.mapEventManager.offPoiLongClick();
}
}

我不会把这段扩写成“解决了所有页面切换后的回调问题”。它只是当前页面在离开时作出的明确解除动作。是否真的触发过回调、不同设备上是否存在特殊表现,仍要靠真机日志补上。代码的职责是把意图写清楚,运行材料的职责是告诉我们意图有没有抵达设备。
这次复盘留下的三个问题
以前我觉得地图交互的麻烦在手势。现在更觉得,麻烦在于我们太容易把视觉上相近的点叫作同一种东西。Marker 和 POI 都在地图上,但它们到达页面的路径、携带的字段、可以承担的业务含义都不同。先把对象叫对,才谈得上排查“为什么没反应”。
先看地图状态,再碰地图对象
把 Marker 和 POI 区分开之后,我又犯过一个更早的错误:一看到页面中间有地图区域,就默认可以开始验证对象回调。实际上,页面把“地图控件出现”“控制器回调成功”“监听注册完成”“测试 Marker 添加完成”拆成了几层状态。它们听起来很接近,但排查时每层都不能跳。
mapRuntime.mapState 初始是“等待 MapComponent 回调”,markerState 初始是“等待地图初始化”。若 onMapReady 收到 error,或者 controller 是 undefined,代码不会去注册事件,也不会尝试添加 Marker;它会将地图状态改成初始化失败,把错误格式化后留进 errorMessage。这时我若继续长按屏幕,得到的不是“Marker 长按失败”,而是一个连测试前提都没有具备的动作。
if (error !== undefined || controller === undefined) {
this.mapRuntime = {
mapState: '地图初始化失败',
markerState: '未添加 Marker',
errorMessage: error !== undefined ? formatRuntimeError(error) :
'MapComponent 未返回 controller',
callbackCount: callbackCount
};
this.markOperation('MapComponent 回调失败');
return;
}
我以前会把这类状态区看成演示附属文字,现在会把它当成测试入口。callbackCount 至少提示 MapComponent 回调是否抵达过页面;mapState 提示控制器和监听所处位置;markerState 则单独说明测试 Marker 的添加过程。三个字段没有哪一个可以被另一个完全代替。控制器成功不等于 Marker 已添加,Marker 添加失败也不自动说明 POI 监听失败,反之亦然。

这也改变了我写故障描述的方式。只有当“控制器已就绪,长按监听已注册”已经出现后,才值得说“真实长按没有改变对应计数”;如果状态还停在等待,就只应说“地图回调未到达,无法进入对象手势验证”。前一句是回调问题,后一句是初始化前提不足。把它们混成“地图没反应”,读者根本无从下手。
添加测试 Marker 不等于识别到了 POI
当前页面在控制器就绪后调用 controller.addMarker,位置固定为北京中心点,标题是 Article 08 长按 Marker。这是应用主动添加的测试对象。它的作用是给 Marker 回调准备一个明确目标,而不是替底图生成 POI,更不是搜索服务返回的地点。
await controller.addMarker({
position: this.center,
title: 'Article 08 长按 Marker',
snippet: '请在地图上真实长按此 Marker',
clickable: true,
draggable: false
});
this.mapRuntime = {
mapState: this.mapRuntime.mapState,
markerState: '真实 Marker 已添加到北京中心点',
errorMessage: '暂无错误',
callbackCount: this.mapRuntime.callbackCount
};
这个对象的固定位置容易造成另一个误判:既然它在北京中心点,长按它就能证明坐标准确。实际上,源码只能证明添加请求使用了 center 这个字段,页面在回调中按对象 API 读取位置。真正地图在设备上如何投影、用户是否按中目标、底图数据是否可见,都还需要真实环境。代码给了一个可定位的测试锚点,不给坐标正确性背书。
同样,POI 也不是“Marker 添加失败后的替代品”。底图 POI 能否被真实触发,依赖地图加载与服务;Marker 则依赖添加请求是否成功。一个失败时去测试另一个,也许能帮助收集更多状态,但不能拿一个对象的表现替另一个对象证明功能正常。
最后事件为什么既要写文字又要保留数字
我一开始以为保留 lastEvent 就够了,例如看到“POI 长按:某名称 (某 ID)”,就知道发生过什么。后来发现单独一条文字经不住连续操作。它会被下一次事件更新,截图也可能只留下最后结果。因此页面同时保留两类计数和最新事件,二者承担不同事情:计数回答某类回调累计改变过几次,文字回答最近一次写入描述的是哪一类对象。

这仍不是完整历史。lastEvent 只有最后一条,计数也不保存每次的字段快照。若测试中需要追溯多次真实长按,应另行记录时间、ID、标题或 POI ID,不要误以为当前 UI 已经保存了全部过程。可观察状态的边界越早讲清楚,事后越不容易拿一张最终截图做过多推断。
我还会检查另一件看似不起眼的事:页面离开后监听是否解除。aboutToDisappear 对两个对象分别调用关闭方法,说明注册和清理都按对象类别对称处理。它不能替我证明设备上已经收到过回调,却避免了代码阅读时把两种监听误当成同一个开关。
我的复测记录不再只写“长按成功”
真正复测时,我会把记录分成前置、动作和结果三栏。前置栏写 mapState、markerState、callbackCount 和 errorMessage;动作栏写长按的目标类别,明确是测试 Marker、底图 POI 还是空白区域;结果栏同时写两个计数、lastEvent、lastOperationId 和时间。这样某次数字没有变化时,能先回看我是否按到了预期对象,也能回看初始化是否完成。
如果目标是空白区域,两个计数不变不应判失败。若目标是 Marker,但 Marker 状态本就显示添加失败,也不能继续要求 onMarkerLongClick 增加计数。只有前置状态清楚、目标类别明确、回调对应计数仍未变化,才值得把问题缩小到监听注册、真实手势或运行环境。这个判断顺序看起来慢,实际很省时间,因为它不会让我在错误前提上反复操作。
最后我把结论收在当前页面能支持的地方:代码已经为两种对象准备了不同监听、不同字段文本和不同计数;真实回调是否发生,必须另外核对。这不是把问题留给以后,而是先把“应当观察什么”摆出来。地图长按真正难的地方,不是按住屏幕那一秒,而是知道按住之后,究竟该由哪个对象、哪组状态来回答。
本文依据当前 MapSearchLongClickPage 的 Marker/POI 注册、独立计数和状态记录撰写。页面尚未取得的真实 Map 回调、坐标准确性和 POI 行为,仍需在认证和真机环境中单独复核。




