我看到 HarmonyOS 7 里“精准碰一碰”这个能力时脑子里冒出来的第一个想法不是“又多了一个分享方式”而是如果手机里的素材能碰到大屏演示文稿里的某个区域再直接插进去这件事其实很像真实办公场景。所以我决定做一个小 Demo名字叫SlideDrop。这套五连载不追求一上来就把所有边界全讲完而是像平时做工程一样从第一版能跑通开始一步一步把它做成一个看得懂、能演示、还能继续扩展的小项目。第一篇的目标很明确先把“选素材 → 碰一碰 → 识别目标区域 → 插入到演示页”这条主链路跑通。一、我为什么会想到做 SlideDrop很多时候我们在写技术文章时容易陷入一种习惯上来先解释概念再列能力点再贴几段 API。这样当然没错但真到工程里开发者往往不是因为“概念很完整”才开始写代码而是因为眼前有一个特别具体的问题。我这次的出发点就很简单平时做课件、讲方案、做汇报的时候手机里经常会存着一些临时素材。可能是一张产品图也可能是一张现场拍的照片或者一段整理好的要点截图。传统方式通常是先把文件传到电脑再手动打开演示文稿再拖进去再调整位置。整个过程并不复杂但碎。HarmonyOS 7 的“精准碰一碰”让我觉得它天然适合把这个动作变短手机端负责选择素材大屏端负责准备一个可接收的演示页系统帮助建立“碰一碰”的投递链路应用根据触点位置判断用户到底想把素材放到哪里最后把图片、文本或图表插入对应区域。你会发现这不是一个“炫技 Demo”而是一个有很强场景感的 Demo。它适合办公、适合演示也适合用来解释“精准”两个字到底精准在哪里。所以我给这个 Demo 起了一个很直接的名字SlideDrop。它不是做“跨设备文件传输”这么宽的命题而是收得很小从手机把素材精准投递到演示文稿的指定位置。这个范围一收小第一篇要做的事情就清楚了手机端能看到并选择素材演示页里先定义几个明确的目标区域能拿到一次“碰一碰”动作返回的落点信息根据落点判断命中了哪个区域把素材插进去并让页面和日志都能反馈成功。这五件事如果都打通了第一篇就成立了。二、第一篇不求复杂先把主链路跑通如果一开始就想把标题区、图片区、图表区、备注区、素材类型、冲突处理、失败恢复、撤销逻辑全部做完这个项目会马上变重。更麻烦的是文章也会跟着发散读者看不清主线作者自己也容易越写越散。所以我给第一篇定的范围非常克制素材类型先以图片为主接收端先只做一个简单的“演示页编辑器”区域先分成四块标题区、图片区、图表区、备注区先证明“碰哪放哪”这件事可以成立其他边界问题留到后面几篇继续拆。这其实是我很喜欢的一种连载写法第一篇只做“能跑起来的第一版”第二篇开始补坐标和区域第三篇再处理素材规则第四篇解决状态和异常第五篇最后做工程收口。这样整个系列会像一个真实项目而不是五篇互相没关系的技术稿。SlideDrop 第一版的页面结构也很简单我把它拆成两端1手机发送端发送端的职责很清楚展示待投递素材让用户选择一项内容调用封装好的投递控制器发起“碰一碰”动作等待系统返回结果。2大屏接收端 / 演示页端接收端也不复杂提前定义一页演示文稿中的可投放区域接收系统传回的触点坐标通过 hitTest 命中目标区域调用插入逻辑更新页面把结果回写到日志和界面状态。第一篇的价值不在于“功能很多”而在于这条链路跑通以后后面四篇就有了真正可以演进的基础。三、先把演示页上的“可投放区域”定义清楚做这种 Demo最怕的一种写法是一上来就讲跨设备讲能力封装讲流程编排结果最基本的页面接收对象没定义清楚。我反而觉得这个项目第一步最应该做的是先问一句演示页到底哪里可以接素材如果这个问题没说清楚后面所谓的“精准”其实是站不住的。所以我先在 SlidePage 里定义了四类区域标题区title图片区image图表区chart备注区note每个区域至少包含四类信息id区域唯一标识type区域类型rect区域在页面中的位置和尺寸label界面上给人看的名称。这一步看着很基础但它其实把“演示页是一个接收平面”这件事说清楚了。只有先把接收平面描述出来后面拿到触点坐标时才有可能做命中判断。下面这段代码就是第一版里最关键的一段“区域定义”。文件位置entry/src/main/ets/pages/SlidePage.ets用途先把演示页中的四个投放区域描述清楚后续所有的坐标判断和插入逻辑都围绕它来做。enum ZoneType { TITLE title, IMAGE image, CHART chart, NOTE note } interface DropZone { id: string type: ZoneType rect: Rect label: string } private targetZones: DropZone[] [ { id: zone-title, type: ZoneType.TITLE, rect: { x: 40, y: 120, width: 640, height: 80 }, label: 标题区 }, { id: zone-image, type: ZoneType.IMAGE, rect: { x: 40, y: 220, width: 300, height: 220 }, label: 图片区 }, { id: zone-chart, type: ZoneType.CHART, rect: { x: 380, y: 220, width: 300, height: 220 }, label: 图表区 }, { id: zone-note, type: ZoneType.NOTE, rect: { x: 40, y: 460, width: 640, height: 120 }, label: 备注区 } ]这个结构的好处是后面很容易扩展。比如第二篇如果我要支持更多布局只需要继续调整区域坐标如果第三篇我要做不同素材的插入规则也只是围绕type继续下沉逻辑并不用推倒重来。四、主链路其实没有那么神秘就是“素材、触点、命中、插入”四步把页面结构摆清楚以后我会立刻回头梳理主链路。因为一个 Demo 到底清不清楚往往不在于页面画了多少而在于你能不能用一句人话把它讲顺。我把 SlideDrop 第一版主链路收成四个动作1素材准备用户在手机端先选中一张图片。这个素材可以来自应用内置素材库也可以来自相册映射后的演示数据。第一篇里为了保证流程稳定我先用了内置素材列表。2触发投递当用户点“准备投递”或“开始投递”后控制器负责把当前素材封装成一次 SlideDrop 可识别的任务。系统在这个阶段会介入设备发现、建立连接以及后续的“碰一碰”行为。3命中区域接收端拿到触点位置以后不是直接插而是先执行一次 hitTest。也就是说先判断这个点落在了标题区、图片区、图表区还是备注区。4执行插入只有确认命中区域以后才进入真正的业务逻辑命中标题区就按标题逻辑插入命中图片区就按图片逻辑插入命中图表区就走图表逻辑命中备注区就写到备注区。第一篇里我先重点验证图片区因为它最直观也最适合作为第一次效果演示。把这条链路讲清楚以后很多事情都不再抽象。所谓“精准碰一碰”本质上不是一句宣传词而是手机素材 触点坐标 区域命中 业务插入这张类型二的图就是我希望后面文章里一直保留的那种说明方式既有 DevEco 拟真截图也有关系流程图、实机感照片和标注读者一眼就能明白核心链路。五、发送端先做轻一点把“选素材”做好比“堆能力”更重要很多 Demo 写到这里容易忍不住给手机端加很多功能比如筛选、搜索、分组、最近使用、云端同步。但第一篇真的没必要。因为第一篇发送端的目标就两件事让用户明确知道“我要投递什么”让系统明确知道“当前要投递的是哪个对象”。所以我把发送端写得很轻顶部一个简单的标题中间是素材网格底部一个主按钮“准备碰一碰投递”选中态用蓝色勾选高亮。这类页面最重要的不是华丽而是可确认。因为跨设备动作一旦开始如果用户自己都不确定当前选的是哪张素材体验会非常差。所以你会发现我在 UI 上故意强化了两点选中卡片有明显蓝色描边和勾选状态底部按钮直接把数量写出来让用户知道已经选择了 1 项素材。第一篇里这个页面已经足够用了。它没有太多“产品感”但工程上非常明确。接着我在发送端加了一层很薄的控制器封装。它不负责处理所有业务只负责把“当前选中的素材”交给后面的投递链路。这里有个很重要的工程判断页面层不要直接到处散落“准备投递”“发起投递”“处理结果”的代码。第一版项目还小看不出问题但如果不提前收住后面第二篇第三篇一加功能页面就会开始失控。所以第一篇我就先把这层控制器放进去即便逻辑还不复杂也先给项目留出结构空间。六、接收端的关键不是插入而是先做 hitTest如果要说第一篇最有“技术味儿”的点我觉得不是投递不是日志甚至也不是 UI而是hitTest。因为 SlideDrop 这个项目最核心的体验其实就取决于这一点当用户碰到演示页时系统给回来的坐标到底怎么转成具体区域这一步如果说不清楚整个项目会沦为“碰一碰传一张图片”的普通演示而不是“精准投到某个位置”的场景 Demo。我的处理方式很直白先把每个区域定义成一个rect接到触点point后遍历所有区域判断point.x、point.y是否落在对应rect范围内命中哪个就返回哪个区域对象。这个判断本身并不复杂但它把“触点 → 区域”的映射关系明确落成了代码。下面这段代码就是第一版接收端的核心。文件位置entry/src/main/ets/pages/SlidePage.ets用途接收触点事件执行命中判断再把素材插入对应区域。private hitTest(point: Point): DropZone | undefined { return this.targetZones.find(zone { return point.x zone.rect.x point.x zone.rect.x zone.rect.width point.y zone.rect.y point.y zone.rect.y zone.rect.height }) } private insertMaterial(zone: DropZone, material: MaterialItem) { switch (zone.type) { case ZoneType.TITLE: this.insertTitle(material) break case ZoneType.IMAGE: this.insertImage(material) break case ZoneType.CHART: this.insertChart(material) break case ZoneType.NOTE: this.insertNote(material) break } } onDrop(event: DragEvent) { const point { x: event.x, y: event.y } const zone this.hitTest(point) if (zone event.data) { const material event.data as MaterialItem this.insertMaterial(zone, material) console.info(已在【${zone.label}】插入: ${material.name}) } }这段代码解决了两个问题第一命中判断。它回答的是“你碰到了哪里”。第二插入分发。它回答的是“碰到这个位置以后该按哪类逻辑处理”。这就是为什么我说第一篇真正的骨架不是“投递”而是“目标区域 命中判断 分发执行”。七、日志一定要早加而且要围绕主链路打很多人做 Demo 会把日志留到最后。但我这几年越来越觉得日志应该尽早加而且要尽早围绕主链路去打。原因很简单第一篇要验证的不是“页面看起来像成功了”而是整个动作链条真的跑通了。所以我在第一版里把日志重点放在了下面几个节点选择素材成功调用 prepare 成功发起投递成功收到触点坐标命中目标区域插入成功当前页与耗时信息。这样做有两个好处。1文章更好写因为你不是在空口讲流程而是有证据。哪一步发生了哪一步成功了哪一步还没做都能从日志里读出来。2后面几篇更好演进第二篇如果我要分析坐标偏差日志就能直接复用第三篇如果我要分析不同素材策略日志结构也已经在第四篇第五篇做异常恢复和工程收口时这些日志还能继续成为排查入口。我很不喜欢那种“前面三篇都不打日志最后一篇突然加一堆输出”的写法。那样看起来像补作业不像工程。所以第一篇虽然只是最小 Demo我也要求它在日志层能讲清楚故事[SlideDrop] 选择素材: 产品图.jpg [SlideDrop] 准备素材成功mimeTypeimage/png [SlideDrop] startDrop 调用成功等待结果 [SlideDrop] 接收到触点坐标: x642, y1188 [SlideDrop] 命中目标区域: IMAGE [SlideDrop] 插入成功目标应用: 幻灯片编辑器这样的日志不是为了炫而是为了给后面的文章打底。八、第一次跑通效果时我最关注的不是“传过去了”而是“放对了”第一版跑通以后最容易高兴的一点当然是手机上的素材真的进到演示文稿里了。但如果只停在“传过去了”我觉得这个项目还谈不上成立。因为普通的传图方式也能做到“传过去了”。SlideDrop 第一篇真正让我觉得值得继续写下去的地方是我看到它已经开始具备“放对位置”的雏形。比如我把图片素材碰到图片区最后它就真的插到了图片区而不是整个页面随便哪个地方。这个感受很重要。它说明这个 Demo 不是一个“跨设备展示效果”而是在朝“跨设备操作能力”走。当然第一篇我也很清楚它还不完整目前更多是图片主链路还没有处理重复插入和覆盖逻辑文本、图表、备注虽然区域在但策略还很轻UI 也还是偏工程演示。但这都没关系。第一篇不是为了“完整”而是为了建立第一块真正稳的地基。下面这张实机感效果图就是我希望读者在第一篇最后看到的状态手机端确认素材已发送大屏端演示文稿当前页已插入图片整个流程能被肉眼感知也能被日志印证。九、第一篇里我刻意没做的几件事我觉得写连载有一个很重要的习惯就是不只告诉读者“这篇做了什么”还要告诉读者“这篇故意没做什么”。因为这能帮助整套系列保持节奏。第一篇里我刻意没做下面这些事情1没做复杂素材库没有做搜索、分组、最近使用、云素材同步。因为第一篇的目标不是做素材管理而是跑通精准投递。2没做区域冲突处理如果标题区已经有内容、图片区已经有图怎么办这类冲突很真实但不适合第一篇上来就塞进来。3没做结果回撤和撤销这会直接带来状态同步和 UI 反馈问题我准备放到后面的篇章里再讲。4没做更完整的设备状态判断比如设备未发现、连接超时、用户取消、落点无效等。这些都是真问题但在第一篇里我只保留了最小路径。其实这就是连载式写法的好处。你不用每一篇都追求“大而全”而是把真实问题一层一层展开。第一篇负责建立信心第二篇第三篇再慢慢加厚。十、我会怎么验收 SlideDrop 第一篇技术文章写到最后如果没有验收标准很多内容会显得很虚。所以我给第一篇定了一套很朴素但很有用的验收标准1页面结构清楚发送端能选素材接收端能看到四个目标区域交互入口明显不会让人不知道下一步干什么。2主链路跑通选择图片准备投递发起碰一碰收到触点命中图片区演示页显示插入结果。3日志能解释过程不是只看到最后一个成功提示而是整条链路的关键节点都能在日志中看到。4页面结果和日志一致如果界面显示插入成功日志也应该能看到命中区域和插入结果如果日志显示命中图片区页面最终也应该真的落在图片区。5项目结构还能继续扩展第一篇不求完美但至少要看得出来发送端和接收端没有完全搅在一起区域定义是独立的hitTest 是独立的插入分发逻辑后面还能往下拆。这五条如果都满足我就认为第一篇是成立的。十一、本文小记如果让我给第一篇下一个最直接的总结我会说SlideDrop 第一篇最重要的不是“碰一碰成功了”而是“精准插入这件事第一次具备了工程雏形”。这篇文章做的事其实并不复杂定义演示页的目标区域建一个轻量发送端打通投递控制器根据触点做命中判断把图片插入到对应位置用日志把整个过程串起来。但我很喜欢这种节奏。因为它不是一口气追求一个看起来很大的成品而是像真实开发一样先把第一块骨架搭起来。到这里SlideDrop 五连载的第一步就算走稳了。后面第二篇我会继续往下走重点不再是“能不能传过去”而是触碰坐标 区域映射怎么把‘碰哪放哪’这件事讲得更细、更准、更像一个真正可用的演示协作工具。十二、第一篇做完以后我已经看到的几个真实问题第一篇虽然已经把主链路跑通了但我写到这里时脑子里其实已经冒出好几个后续必须面对的问题。把这些问题提前写出来我觉得比假装“第一篇已经很完整”更有意义。1命中区域目前还是偏静态现在的区域坐标来自我手工定义的rect。这对第一篇完全够用因为它能帮助我们把“精准落位”这件事先讲清楚。但如果后面演示页支持不同模板、不同布局甚至支持缩放和滚动单纯静态坐标就不够了。也就是说第二篇我肯定要继续处理“坐标系统”和“区域映射”这两个问题。2素材类型虽然有四类插入策略还不对等第一篇里最成熟的是图片。标题、图表、备注虽然已经留好了区域也在分发逻辑里占了位置但它们目前更像是“占坑”而不是“完整能力”。这没有问题因为工程本来就应该先打通一条最清晰的主链路。只是我心里会一直记着后面文章不能停留在“图片区演示成功”而要真正让四类区域都逐步具备业务意义。3发送端和接收端的状态还没有做得很细现在用户能看到选中态、按钮态和插入结果但整个过程里的“准备中、等待碰一碰、投递中、成功、失败、取消”等状态还没有完全展开。对 Demo 来说这不是致命问题可一旦要把体验往“可用”推进这部分就会变得很关键。4日志已经有了但诊断层次还不够我在第一篇里已经开始打主链路日志这是一个很好的开端。但如果后面真遇到坐标不准、区域识别错误、素材插入失败仅靠现在这几条日志还是不够。后面我会继续把日志拆成“发送端日志、接收端日志、区域判断日志、插入结果日志”这样文章也会更像真实项目复盘。5最重要的一点第一篇已经值得继续写下去我做连载时很看重一个信号做完第一篇以后你是不是还能自然地知道第二篇要写什么。如果第一篇做完以后脑子里一片空白说明这个选题大概率不适合连载。但 SlideDrop 恰恰相反。第一篇写完以后第二篇、第三篇、第四篇要做什么我反而更清楚了。因为这个 Demo 已经不是“一个静态演示页面”而是真正出现了后续问题坐标、区域、素材规则、状态同步、异常恢复、工程收口。这说明它具备继续往下写的空间。所以我对第一篇的评价其实很简单它还远远不完整但它已经足够真实它还只是第一版但它已经有了继续演进的方向。对一个五连载项目来说这就很重要。