ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS 7 精准碰一碰 SlideDrop 实战 03:素材类型 + 占位槽位完善演示页智能插入规则【鸿蒙心迹】

HarmonyOS 7 精准碰一碰 SlideDrop 实战 03:素材类型 + 占位槽位完善演示页智能插入规则【鸿蒙心迹】 前两篇里SlideDrop 已经完成了两件很关键的事第一篇跑通“手机素材 → 精准碰一碰 → 演示页插入”的第一条链路第二篇又把系统触点转换成页面坐标并通过区域映射解决了“到底碰到了哪里”。到了第三篇问题开始从“位置准不准”转向“内容该不该放进去”图片碰到标题区怎么办文本碰到图片区怎么办目标槽位已经有内容时是替换、拒绝还是继续叠加这一篇我就把素材类型和占位槽位正式拆开让 SlideDrop 从“碰哪放哪”继续走到“什么素材放到什么位置才合理”。一、第二篇解决了“落在哪里”第三篇开始解决“能不能落”第二篇写完以后我特意连续做了几轮测试。我拿同一张山景图片分别去碰标题区、图片区、图表区和备注区。坐标转换已经没有问题区域命中也比较稳定日志里能清楚看到systemPoint: x842, y356 pagePoint: x421, y178 hitRegion: image如果只看第二篇的目标这已经算完成了。但一旦我把测试素材从“只有图片”扩展成图片、文本、截图、简单图表数据之后一个更现实的问题马上出现区域命中了并不代表素材就应该被接受。最典型的场景就是一段标题文本碰到了图片区一张风景照片碰到了标题区一个图表数据对象碰到了备注区已经放了一张主图的图片区又来了一张新图。如果应用只做“命中区域 → 直接插入”那么所谓精准落位最后会变成另一种混乱位置是准的业务语义却不准。所以第三篇我给自己定了一个新的判断触点负责告诉我“用户碰到了哪里”规则层负责判断“这个东西能不能放到这里”。这两件事一定要分开。HarmonyOS 7 的精准分享能力能帮助应用识别目标窗口和触碰坐标应用拿到这些信息以后仍然需要结合自己的业务界面决定最终插入位置和处理规则。SlideDrop 这一篇做的就是把这个“应用自己的判断”真正补起来。二、先看第三篇后的项目目录规则层要正式出现了前两篇里项目结构还比较轻。到了第三篇我不想继续把所有判断都堆在SlidePage.ets里所以专门加了一层rules。这一版我把目录整理成下面这样SlideDropDemo/ ├── AppScope/ ├── entry/ │ └── src/ │ └── main/ │ ├── ets/ │ │ ├── pages/ │ │ │ ├── Index.ets │ │ │ └── SlidePage.ets │ │ ├── components/ │ │ │ ├── MaterialCard.ets │ │ │ ├── SlideSlot.ets │ │ │ └── PlacementToast.ets │ │ ├── model/ │ │ │ ├── MaterialModel.ets │ │ │ ├── SlotModel.ets │ │ │ └── PlacementResult.ets │ │ ├── rules/ │ │ │ └── PlacementRuleEngine.ets │ │ ├── utils/ │ │ │ ├── CoordinateMapper.ets │ │ │ └── HitTestUtil.ets │ │ └── common/ │ │ └── SlideDropController.ets │ └── resources/ └── ohosTest/这次最关键的新文件有三个MaterialModel.ets描述“手机端到底传过来了什么”SlotModel.ets描述“演示页上这个槽位能接什么”PlacementRuleEngine.ets把素材和槽位放在一起判断返回允许、替换、拒绝等结果。我很想把这一层单独拎出来因为它让整个 Demo 的职责第一次真正清楚起来CoordinateMapper 解决坐标转换 HitTestUtil 解决命中哪个区域 PlacementRuleEngine 解决素材能否进入这个槽位 SlidePage 只负责把结果展示出来当这几层各自只回答一个问题时第三篇就不再只是“继续加几个 if”而是在给后面第四篇的状态冲突和异常恢复提前打基础。三、素材不能再只是一个 uri我先把 MaterialModel 补完整第一篇只有图片时素材对象很简单很多时候有一个uri就够了。到了第三篇这种结构已经不够用。因为规则层如果连素材是什么都不知道就没法判断它适不适合某个槽位。所以我先定义一个比较轻的MaterialItem文件位置entry/src/main/ets/model/MaterialModel.ets用途统一描述图片、文本、图表数据和截图等投递素材。export enum MaterialType { IMAGE image, TEXT text, CHART chart, SCREENSHOT screenshot } export interface MaterialItem { id: string type: MaterialType name: string uri?: string text?: string width?: number height?: number size?: number }这段模型没有做得很重但已经足够让业务层区分几类核心素材。比如手机端选择一张普通照片时const material: MaterialItem { id: img-001, type: MaterialType.IMAGE, name: 山景主图.jpg, uri: file://media/Photo/IMG_001.jpg, width: 1920, height: 1080 }如果是用户准备投递的一段标题文本const material: MaterialItem { id: text-001, type: MaterialType.TEXT, name: 演示标题, text: 季度项目进度汇报 }这时候 SlideDrop 才真正知道我收到的不是“一个数据对象”而是一张图、一段字或者一份图表数据。后面所有“能不能插入”的判断都建立在这层明确的素材语义上。四、占位槽位也要有自己的规则不能只靠视觉上画个虚线框素材有类型以后下一步就是把页面区域从“几块矩形”升级成真正的 Slot。第二篇里标题区、图片区、图表区、备注区主要用于 hitTest。第三篇里我会继续给它们补上业务属性。文件位置entry/src/main/ets/model/SlotModel.ets用途描述槽位类型、允许的素材类型以及当前占用状态。import { MaterialType } from ./MaterialModel export enum SlotType { TITLE title, IMAGE image, CHART chart, NOTE note } export interface SlideSlot { id: string type: SlotType label: string rect: Rect accept: MaterialType[] occupied: boolean materialId?: string }然后我给四个槽位写出第一版接收规则private slots: SlideSlot[] [ { id: slot-title, type: SlotType.TITLE, label: 标题区, rect: this.titleRect, accept: [MaterialType.TEXT], occupied: false }, { id: slot-image, type: SlotType.IMAGE, label: 图片区, rect: this.imageRect, accept: [MaterialType.IMAGE, MaterialType.SCREENSHOT], occupied: false }, { id: slot-chart, type: SlotType.CHART, label: 图表区, rect: this.chartRect, accept: [MaterialType.CHART, MaterialType.SCREENSHOT], occupied: false }, { id: slot-note, type: SlotType.NOTE, label: 备注区, rect: this.noteRect, accept: [MaterialType.TEXT, MaterialType.SCREENSHOT], occupied: false } ]这里有一个我很在意的点同一种素材不一定只能去一个地方同一个槽位也不一定只接一种素材。比如截图就很特别。一张数据看板截图可以进图表区一张说明截图也可能进备注区它本质上仍然是图像但业务上又不完全等于普通照片。所以我没有把规则写成死板的“一对一映射”而是让 Slot 自己声明一个accept数组。这样后面规则变化时不用回到页面里改一大堆条件。五、真正的核心来了PlacementRuleEngine 只负责判断不直接改 UI第三篇最关键的一层就是PlacementRuleEngine。我给它定的职责非常克制输入一个素材和一个槽位返回“允许插入、需要替换、还是拒绝”。它不直接改页面也不直接弹 Toast更不直接操作图片控件。先定义结果类型export enum PlacementAction { INSERT insert, REPLACE replace, REJECT reject } export interface PlacementResult { action: PlacementAction message: string }然后写第一版规则文件位置entry/src/main/ets/rules/PlacementRuleEngine.ets用途判断素材类型与槽位规则是否匹配以及槽位已占用时如何处理。export class PlacementRuleEngine { static evaluate( material: MaterialItem, slot: SlideSlot ): PlacementResult { const accepted slot.accept.includes(material.type) if (!accepted) { return { action: PlacementAction.REJECT, message: ${material.type} 不能放入 ${slot.label} } } if (slot.occupied) { return { action: PlacementAction.REPLACE, message: ${slot.label} 已有内容准备替换 } } return { action: PlacementAction.INSERT, message: 允许插入到 ${slot.label} } } }这层代码看起来并不复杂但它带来的变化很明显。以前是触点 → 命中图片区 → 插图现在变成触点 ↓ 命中图片区 ↓ 拿到 MaterialItem ↓ RuleEngine 判断是否接受 ↓ INSERT / REPLACE / REJECT ↓ 页面执行对应动作这一步让“精准”不再只体现在坐标上也开始体现在业务规则上。六、页面层现在只需要串起 3 个判断坐标、槽位、规则有了前面几层之后SlidePage.ets反而变简单了。它不需要知道所有规则细节只要负责把事件串起来。文件位置entry/src/main/ets/pages/SlidePage.ets用途把触点坐标、槽位命中和规则判断连接成完整投递流程。private async handlePreciseDrop( point: Point, material: MaterialItem ) { const pagePoint this.coordinateMapper.toPagePoint(point) const slot this.hitTestUtil.findSlot(pagePoint, this.slots) if (!slot) { this.statusText 没有命中可投放区域 return } const result PlacementRuleEngine.evaluate(material, slot) console.info( [SlideDrop] material${material.type}, slot${slot.type}, action${result.action} ) switch (result.action) { case PlacementAction.INSERT: await this.insertToSlot(slot, material) break case PlacementAction.REPLACE: await this.replaceSlotContent(slot, material) break case PlacementAction.REJECT: this.statusText result.message break } }这里我特别喜欢的一点是现在每一层失败都能说清楚原因slot没找到说明坐标没命中REJECT说明类型不匹配REPLACE说明槽位已经有内容INSERT说明可以正常插入。这种结构比一堆嵌套if/else更适合连载因为后面第四篇要继续补冲突和状态恢复时基本不用推翻第三篇。七、图片进入图片区后也不能直接“原样塞进去”做到这里我又遇到一个非常现实的问题图片类型虽然匹配了图片区但图片比例并不一定匹配占位槽。比如一张 9:16 的竖图投到一个 16:9 的图片区如果直接拉伸会很难看如果强制铺满又可能把主体裁掉。所以第三篇里我顺手补了一个很轻的适配策略。我没有做复杂编辑器只先支持三个模式export enum FitMode { CONTAIN contain, COVER cover, ORIGINAL original }第一版默认规则是普通演示主图使用COVER截图或信息图优先CONTAIN特殊情况下保留ORIGINAL。简单处理可以写成private resolveFitMode(material: MaterialItem): FitMode { if (material.type MaterialType.SCREENSHOT) { return FitMode.CONTAIN } if (material.type MaterialType.IMAGE) { return FitMode.COVER } return FitMode.ORIGINAL }然后插入时把这个模式一起交给SlideSlotprivate async insertToSlot( slot: SlideSlot, material: MaterialItem ) { const fitMode this.resolveFitMode(material) slot.occupied true slot.materialId material.id this.currentPlacement { slotId: slot.id, material, fitMode } this.statusText ${material.name} 已插入 ${slot.label} }这个处理虽然很轻但它让第三篇的“智能插入规则”更像回事了。因为“能不能放进去”和“放进去以后怎么展示”本来就是两个连续的问题。八、槽位已占用时我没有直接替换而是把冲突暴露出来第三篇还有一个我不想绕过去的问题目标区域已经有内容怎么办如果是一个真正的演示编辑器直接覆盖用户现有内容通常并不友好。可如果每次都拒绝又会让精准投递变得很笨。所以第一版我先采取一个中间方案规则层返回 REPLACE但页面层先展示“已有内容”的状态再由用户确认。也就是说RuleEngine只负责发现冲突不替用户做最终决定。页面可以显示图片区已有“产品主图.jpg” 新素材“现场照片.jpg”准备替换 [取消] [替换]确认后再真正执行private async confirmReplace( slot: SlideSlot, material: MaterialItem ) { const previousId slot.materialId await this.replaceSlotContent(slot, material) console.info( [SlideDrop] replace slot${slot.id}, old${previousId}, new${material.id} ) }这段逻辑为第四篇留下了很自然的入口。因为第四篇原本就要处理连续投递、重复素材、目标区域已占用、失败、取消以及页面切换。第三篇在这里不需要把全部冲突状态一次解决只要把“冲突”从隐藏行为变成显式状态就已经完成了它的任务。九、为了不让规则越来越乱我又加了一层“素材归一化”第三篇写到这里我又发现一个很容易被忽略的问题手机端传过来的素材描述未必永远长成我们想要的样子。比如同样是一张图片有的来源只有uri有的会带宽高有的可能还会带 MIME 类型截图和普通照片从文件角度看都可能是 image但业务上我又希望把它们区分开。如果这些差异全部交给PlacementRuleEngine去判断规则层会很快变脏。它本来只应该回答“能不能放”最后却要开始关心“这个字段有没有”“这个来源是不是截图”“宽高是不是缺失”。所以我补了一个非常轻量的归一化步骤把输入先整理成统一的MaterialItem。文件位置entry/src/main/ets/common/MaterialNormalizer.ets用途把不同来源的素材数据整理成统一业务模型再交给规则层。export interface RawDropData { uri?: string text?: string mimeType?: string width?: number height?: number source?: string } export class MaterialNormalizer { static normalize(data: RawDropData): MaterialItem { const type this.resolveType(data) return { id: ${Date.now()}, type, name: this.resolveName(data, type), uri: data.uri, text: data.text, width: data.width, height: data.height } } private static resolveType(data: RawDropData): MaterialType { if (data.source screenshot) { return MaterialType.SCREENSHOT } if (data.mimeType?.startsWith(image/)) { return MaterialType.IMAGE } if (data.mimeType application/chartjson) { return MaterialType.CHART } return MaterialType.TEXT } }这样做以后后面的主链路就更稳定了原始投递数据 ↓ MaterialNormalizer ↓ MaterialItem ↓ PlacementRuleEngine ↓ 插入 / 替换 / 拒绝这一步的价值不在于多了一层文件而在于它把“输入数据差异”和“业务规则差异”分开了。后面第四篇如果要处理失败重试或重复素材也不至于把各种来源判断继续往页面和规则层里塞。十、规则命中后页面反馈也要说人话不能只打印 REJECT还有一个很容易被技术 Demo 忽略的细节规则层判断正确不代表用户体验就正确。比如文本碰到了图片区代码里返回actionREJECT对开发者来说已经很清楚但用户并不知道为什么失败。所以第三篇里我给页面状态补了一层更明确的反馈。private applyPlacementResult(result: PlacementResult) { switch (result.action) { case PlacementAction.INSERT: this.statusText 素材已插入当前槽位 break case PlacementAction.REPLACE: this.statusText 当前槽位已有内容请确认是否替换 break case PlacementAction.REJECT: this.statusText result.message break } }我更喜欢这种做法因为它让“规则”不仅存在于代码里也能在 UI 上被理解。尤其是第三篇这种主题如果读者只看到日志里一堆INSERT / REPLACE / REJECT很容易把文章看成规则引擎练习但如果手机页面上能同时看到“图片区只接受图片或截图”“当前槽位已有内容”等提示它就会更像一个真实的编辑器。这里我还加了一个很小的视觉状态可接受素材时槽位边框保持低饱和绿色不匹配时短暂出现浅红边框和提示需要替换时槽位出现橙色提示状态真正插入成功后再恢复普通状态。这不是为了做花哨动画而是为了让“规则发生了什么”一眼可见。十一、规则层越清楚第四篇的异常和状态才越好接做到这一篇后我越来越确定第三篇真正的价值不是多支持了几种素材而是把“位置判断”和“业务规则”拆开了。这件事对下一篇非常重要。因为第四篇要处理的连续投递、重复素材、用户取消、失败恢复本质上都依赖一个前提系统必须先知道当前这次投递到底属于哪一种业务状态。如果第三篇没有MaterialType、SlotType、PlacementAction这些明确状态第四篇就只能继续堆 if。现在则不一样。后面可以很自然地把状态扩成IDLE PREPARING WAITING_DROP VALIDATING INSERTING WAITING_REPLACE_CONFIRM SUCCESS FAILED CANCELED也就是说第三篇其实是在给第四篇的状态机做铺垫。这就是我喜欢这种 Demo 连载的地方每一篇不只是完成当下的功能还会给下一篇留出结构空间。十二、我还专门测了“同一素材跨不同槽位”的边界为了确认规则层不是只在理想路径下有效我又拿同一张素材连续碰不同槽位。比如一张产品主图碰图片区应当INSERT碰标题区应当REJECT碰图表区如果它只是普通照片也应该REJECT碰备注区如果业务不允许图片备注同样拒绝。这类测试很重要因为它能防止我们把“某一条成功路径”误认为“规则设计已经正确”。我最后给每次判断都保留一条统一日志[Rule] materialimage - slotimage : INSERT [Rule] materialimage - slottitle : REJECT [Rule] materialimage - slotchart : REJECT这样一来调试时我不用盯着页面猜而是能直接确认规则是否按预期执行。更关键的是这组测试把第三篇的目标真正闭环了我们不只是证明“某种素材能进入某个槽位”还证明了“不合适的素材会被明确挡住”。这才是智能插入规则应该具备的最基本边界。十三、我给第三篇补了一组真实测试不同素材碰不同槽位规则写出来以后如果只跑一条“图片 → 图片区”的成功路径第三篇其实还不算站住。所以我给自己准备了几组很直接的测试1图片 → 图片区预期INSERTmaterialimage slotimage resultINSERT2文本 → 标题区预期INSERTmaterialtext slottitle resultINSERT3文本 → 图片区预期REJECTmaterialtext slotimage resultREJECT4图表 → 图表区预期INSERTmaterialchart slotchart resultINSERT5截图 → 图表区预期允许并使用CONTAIN6新图片 → 已占用图片区预期REPLACE但先等待用户确认。这几组测试特别适合在 DevEco 的日志里看因为它能把“坐标没问题但规则不允许”和“坐标命中且规则允许”明确区分开。我最后希望日志至少能达到这种粒度[Drop] point(421,178) hitimage [Rule] materialimage slotimage actionINSERT [Insert] success fitCOVER [Drop] point(438,190) hitimage [Rule] materialtext slotimage actionREJECT [Drop] point(419,176) hitimage [Rule] materialimage slotimage actionREPLACE做到这里以后我再回头看第一篇会发现 SlideDrop 的主线已经变化很明显了。第一篇更像“把东西送过去”第二篇是“把坐标找准”第三篇则开始变成“知道什么东西该落在哪里而且落下以后怎么处理”。十四、类型二综合图在这一篇最适合讲什么你前面让我保留一种“类型二”图里面同时有拟真 DevEco、手机截图、关系图、实机照片再配合少量红色标注。第三篇我觉得它最适合讲“素材类型 → 槽位规则 → 最终落位”这条线。关系图部分可以很简单手机端素材 ↓ MaterialType ↓ 精准碰一碰 坐标命中 ↓ SlotType ↓ PlacementRuleEngine ↓ INSERT / REPLACE / REJECT旁边再用 DevEco 截图展示PlacementRuleEngine.evaluate()用纯手机截图展示当前槽位和结果用实机照片展示最终插入。这种组合图有一个很明显的好处它不是为了“好看”而是能让读者在一张图里把代码、关系和效果串起来。所以后面几篇我也会继续保留但每一篇的内容重心会变顺序也可以调整不让五篇看起来完全是同一模板。十五、第三篇我会怎么验收这篇的验收标准已经不能只是“素材插进去了”。我会重点看下面几个问题。1素材类型有没有被明确识别图片、文本、截图、图表不能再都当成一个uri处理。2槽位有没有声明自己的接收规则标题区、图片区、图表区、备注区必须知道自己能接什么。3规则不匹配时有没有明确拒绝比如文本碰到图片区不能静默塞进去也不能假装成功。4槽位占用后有没有进入冲突流程已有主图再来一张图应该进入REPLACE而不是直接覆盖。5图片展示有没有基本适配普通照片和截图至少要能区分COVER与CONTAIN否则“插入成功”仍然可能带来很差的视觉结果。6日志能否区分坐标问题和规则问题这是我很看重的一点。如果没有命中槽位是坐标层的问题如果命中了但被拒绝是规则层的问题。日志必须能把这两种情况分开否则后面排查会非常痛苦。十六、本文小记第三篇写完以后我对 SlideDrop 这个 Demo 的感觉明显变了。第一篇的时候它还是一个“手机素材能不能送到演示文稿里”的实验第二篇把触点和区域映射做稳以后它开始具备“碰哪放哪”的样子第三篇再加上素材类型和槽位规则以后它才真正开始有一点“编辑器逻辑”。这一篇最重要的变化我觉得不是多了几个枚举也不是多了一个PlacementRuleEngine而是我们把两个经常混在一起的问题拆开了触点决定位置规则决定合理性。系统能力帮助我们得到目标窗口和触碰坐标SlideDrop 自己则要决定这个素材是什么这个槽位允许什么能不能插已占用时要不要替换插进去以后该用什么展示策略。当这些问题有了明确边界以后SlideDrop 就不再只是“碰一碰传文件”的外壳而更像一个围绕精准投递长出来的演示文稿协作 Demo。下一篇我会继续处理第三篇已经暴露出来但还没有彻底解决的东西连续投递、重复素材、目标槽位冲突、用户取消、页面切换以及失败后的状态恢复。那一篇会更偏状态和异常也会更接近真实项目里真正难维护的地方。另外我会刻意保留两类“失败是正常结果”的情况没有命中任何槽位以及素材类型和槽位不匹配。前者说明触点或布局映射需要继续检查后者则说明规则层正在正确工作。很多 Demo 容易把所有失败都写成同一句“投递失败”这样一来坐标问题、规则问题和真正的系统异常会混在一起。第三篇把这三类状态分开以后后面做连续投递和异常恢复时诊断成本会低很多。
返回列表