李游一个很小的交互到了大屏双栏里反而更容易露出边界。左侧是商品清单右侧是详情。详情页面纵向滚动某个图片对比区域又放了一个可以左右拖动的分割滑块。用户的手指刚接触图片想上下看下一段描述滑块却跟着抖了一下再换一个角度横向比较整个页面又可能先滚动。它不是“两个手势谁优先”这么简单里面至少还有命中区域、方向识别、滑动起点、布局尺寸和状态回退几件事。本篇用PaneCompareLab的CompareDetailPage做一次协议推演。它可以放在平行视界双栏的右侧详情页窄屏时也能以单栏方式预览图片用“原图 Before / 增强 After”做示例实际产品可以换成装修前后、地图年代对照或不同画质版本。GEST-1009-02B、SKU-108、布局代次16、滑块42%→68%等都是设计与测试用的固定样本不是声称已经得到真机调试结果。一、先讲结果滑块应该输掉一些手势假设用户打开右侧SKU-108的详情。第一次从图片中间的圆形把手拖动记录P-041在 400vp 的演示宽度内横向移动 104vp。按照当前比例 42% 计算理论终点为0.42 104/400 0.68也就是 68%。这次手势应该由图片比较组件消费。第二次记为P-042手指起始位置落在把手热区外即使它带有水平方向的位移比较组件也应该拒绝接管父级 Scroll 能不能真正继续滚动则由它自己的识别条件决定。我希望最后得到的不是“成功滑了一下”的模糊反馈而是一张能够解释事件去向的单据P-041ACCEPTP-042REJECTparentScroll1最终比例仍为 68%业务状态没有被第二条轨迹改写。这样才便于判断页面偶发的手势抖动到底出在组件命中区、识别方向还是某个异步布局事件改变了宽度。要强调一个边界平行视界提供的是应用内页面分栏与多页面展示能力应用里的 Before/After 滑块并不是系统分栏的那条分割线。用户拖动系统分栏影响窗口/页面可用宽度用户拖动图片把手只影响图像遮罩比例。把这两条横向拖动都记成onPan后期会出现无法解释的日志。因此本文把它们命名为systemPaneResize与compareHandlePan前者交给实际平行视界适配能力后者才由当前 ArkUI 组件处理。“手势成功”也不等于“业务值已提交”。起点、识别开始、持续更新、松手和取消各有意义。手指拖到 68% 的途中屏幕显示的是临时状态如果收到取消事件或者原来的页面已经失效就应该回到拖动开始的已提交值。真正需要做历史记录、上报或持久化的比例应在这次操作被认可且仍属于当前页面时提交。二、为什么右侧详情页比单张图片麻烦单独一张图片的 Demo很多时候写一个PanGesture就能看到滑块跟随手指移动。可是在商品详情页面里滑块一般嵌在Scroll、List或其他容器中外面还有图片轮播、点击查看大图、长按菜单等功能。手指落在不同位置开发者期待的控件所有权其实不同落在把手附近才谈图像比较落在普通内容区就优先维护父容器原来的阅读路径。官方 ArkUI 文档提供PanGesture、PanDirection.Horizontal、distance以及onActionStart、onActionUpdate、onActionEnd、onActionCancel等事件。PanGesture接收一个方向参数不能因此理解为“所有略微横向移动都会被这个控件优先拿走”。方向条件是识别器的一部分而把手是否属于本组件则是产品自己要给出的判断。onGestureJudgeBegin正好提供了另一个入口。官方说明该接口在手势识别判断时返回GestureJudgeResult.CONTINUE或GestureJudgeResult.REJECT。CONTINUE表示继续系统手势识别流程不是应用承诺这条手势一定成功REJECT表示当前自定义手势识别被判定失败也不等于应用能命令父 Scroll 一定滚动。区别虽然细但日志和调试说明必须照这个边界写。因此我把裁决拆成两层先审资格再识别方向。资格由起点、当前页面身份和把手热区决定方向由PanGesture({ direction: PanDirection.Horizontal })的识别机制决定。把两者混成一个在onActionUpdate里不断比较dx/dy的 if往往太迟——等更新回调到了识别已经发生父容器是否让出手势也受到影响。而且这里的“热区”并不是把圆形图标画多大就自动有多大。视觉把手可能只有 28vp但为了使用便捷交互热区可以在当前坐标系内适当放宽例如以滑块中心为基准左右各扩 24vp。热区要受到组件宽度约束不能越过裁剪区域也不能遮住旁边的播放、收藏或关闭按钮。折叠态和大屏分栏改变宽度之后热区中心必须按当前已提交比例重新算不能拿旧页面坐标继续比较。三、先把比例计算从界面里挪出来PaneCompareLab的基本目录并不特殊CompareDetailPage.ets负责布局和事件绑定CompareSlider.ets承担图像层与把手GestureDecision.ets封装判定规则CompareSession.ets管理拖动会话Logger.ets输出归档诊断。测试先针对纯函数展开这样滑块比例变化能脱离截图单独计算UI 的主要任务变成把本轮事件交给确定的状态转换。第一段代码解决像素位移怎样换算成稳定比例并在边界处截断。最关键的一点是不要将每次onActionUpdate的offsetX都叠加到前一次临时比例上。该偏移量表示一次手势过程中的位移应该始终相对于本次dragStartRatio计算。反复累加会让滑块越拖越快往回拖又出现不连续。// model/GestureDecision.ets纯 ArkTS 业务函数exportinterfaceRatioMove{startRatio:number;offsetX:number;contentWidthVp:number;}exportfunctioncalculateCompareRatio(move:RatioMove):number{if(move.contentWidthVp0||!Number.isFinite(move.contentWidthVp)||!Number.isFinite(move.offsetX)){thrownewError(invalid compare geometry);}constrawmove.startRatiomove.offsetX/move.contentWidthVp;returnMath.max(0,Math.min(1,raw));}// 演示向量calculateCompareRatio({// startRatio: 0.42, offsetX: 104, contentWidthVp: 400// }) 0.68这段代码不涉及图像解码也没有伪造所谓平行视界专用的滑块 API。01 的比例只说明组件内部的遮罩位置把它转换成Image的裁切范围、覆盖层宽度或者自定义绘制坐标时还要根据实际渲染内容考虑objectFit、图片宽高比与安全区域。把“UI 显示宽度”误写成“原始图片像素宽度”在某些图片比例下也许看不出问题换成纵图立刻会露馅。如果被比较的是两张同尺寸图片遮罩比例与横向位置比较直接若两张图原本裁剪边界不同拖动 68% 并不自动代表物理场景中同一个位置。实际项目应该先决定图像对齐策略再允许比较滑块进入交互状态。本文把示例素材假定为经过业务预处理、构图对齐的同尺寸图像不将这段 UI 比例公式当作图像配准算法。四、热区裁决放在识别开始之前接下来要处理的是“从哪里开始拖”。假设当前容器可用宽度为 400vp起始比例 42%把手中心就在 168vp 左右。我们给它左右各 24vp 的命中带触点位于 168vp 附近就允许水平PanGesture继续识别起点在远处直接拒绝当前比较手势。回调接到的触点坐标必须明确来自当前组件的本地坐标系而不是从全屏点击位置生硬减去一个固定左边距。第二段代码解决手势识别器资格和事件生命周期如何成对处理。以下只摘出真正需要观察的 ArkUI 部分图片的 Before/After 遮罩绘制、组件样式与辅助视觉线可在CompareSlider中完成重点是.gesture(...)与.onGestureJudgeBegin(...)的相互配合。代码使用的枚举与事件名称已对照官方 API 参考实际项目仍应以正在编译的 SDK 类型声明做最后核验。// pages/CompareDetailPage.ets核心事件绑定示意EntryComponentstruct CompareDetailPage{Stateratio:number0.42;privatedragStartRatio:number0.42;privatecontentWidthVp:number400;privatehitRadiusVp:number24;build(){Scroll(){Column(){Stack(){Text(图片对比位置${(this.ratio*100).toFixed(0)}%)// 真实项目在这里放置对齐的前后图片和遮罩}.width(100%).height(280).gesture(PanGesture({fingers:1,direction:PanDirection.Horizontal,distance:8}).tag(compareHandle).onActionStart((){this.dragStartRatiothis.ratio;}).onActionUpdate((event:GestureEvent){this.ratiocalculateCompareRatio({startRatio:this.dragStartRatio,offsetX:event.offsetX,contentWidthVp:this.contentWidthVp});}).onActionEnd((){this.dragStartRatiothis.ratio;}).onActionCancel((){this.ratiothis.dragStartRatio;})).onGestureJudgeBegin((info:GestureInfo,event:BaseGestureEvent):GestureJudgeResult{if(info.tag!compareHandle){returnGestureJudgeResult.CONTINUE;}constfingerevent.fingerList[0];constcenterthis.ratio*this.contentWidthVp;if(!finger||Math.abs(finger.localX-center)this.hitRadiusVp){returnGestureJudgeResult.REJECT;}returnGestureJudgeResult.CONTINUE;})}}}}这个片段刻意把contentWidthVp400写成演示输入没有宣称在手机、平板和折叠屏上这个宽度恒定。实际组件必须用布局回调或来自布局层的明确尺寸更新它且更新时要区分正在拖动的旧几何与新几何。在生产实现中尺寸一旦变化当前手势会话通常应收口并等待下一次起点直接把旧offsetX除以新宽度会让滑块突然跳到另一位置。还需要指出一个体验上的取舍过大的命中半径会使页面用户难以上下滚动过小又会要求手指精确按中一个细小把手。工程上可以用真实测试数据找一个合适区间但它不应该根据一次开发者自己的操作凭感觉定死。若产品提供无障碍模式还应给出按钮、步进调整或可聚焦的替代操作而不能要求读屏用户完成精细拖动才能比较图片。图二是按项目目录和示例字段生成的开发环境示意图不是实机 IDE 调试证据。中部突出水平识别和热区判定右侧模拟器展示SKU-108、layoutEpoch16、当前 68% 的比较位置底部 HiLog 风格区域列出 P-041 的 ACCEPT、P-042 的 REJECT。这一页要表达的不是某款设备已经通过了测试而是“状态从哪个判断节点进入日志”。五、两条轨迹不能合并成一个“滑动成功”为了减少歧义演示把 P-041 与 P-042 分开记录。P-041 触点起始位置在把手热区内水平移动被识别临时比例由 42% 更新到 68%松手后确认提交。P-042 触点落在热区外比较组件的识别判断返回REJECT因此不修改已提交比例。示意样本把父级的后续滚动观测记为parentScroll1这是一条独立记录不是REJECT自动产生的副作用。如果只记录gesturesuccess排查时看不到用户究竟触摸了什么区域。如果仅在松手时记录比例则会漏掉“滑块曾移动但随后取消”的问题。如果在onActionUpdate中反复写业务日志一次拖动可能变成数十条不易聚合的记录。因此诊断模型可以拆为attemptId、paneId、layoutEpoch、hitZone、judgeDecision、recognizedDirection、startRatio、committedRatio与parentScrollObserved高频 update 只按采样策略留下摘要。第三段代码解决动作事件与页面代次如何共同确定提交资格。它是业务诊断类而不是系统“手势追踪 API”paneEpoch由页面布局协调者管理Logger的日志内容由应用自己组织。真正集成时日志应避免包含触点原始轨迹与用户图片路径等不必要的信息。// model/CompareSession.ets应用层提交和诊断exportinterfaceGestureAttempt{id:string;paneEpoch:number;startRatio:number;finalRatio:number;decision:string;parentScrollObserved:boolean;}exportclassCompareSession{privateactiveEpoch:number16;privatecommittedRatio:number0.42;privateevents:GestureAttempt[][];commit(id:string,epoch:number,nextRatio:number):boolean{if(epoch!this.activeEpoch||!Number.isFinite(nextRatio)){returnfalse;}this.committedRatioMath.max(0,Math.min(1,nextRatio));this.events.push({id,paneEpoch:epoch,startRatio:0.42,finalRatio:this.committedRatio,decision:ACCEPT,parentScrollObserved:false});returntrue;}reject(id:string,epoch:number,parentScroll:boolean):void{this.events.push({id,paneEpoch:epoch,startRatio:this.committedRatio,finalRatio:this.committedRatio,decision:REJECT,parentScrollObserved:parentScroll});}invalidateForLayout(newEpoch:number):void{this.activeEpochnewEpoch;}}这个类的重点是“提交必须来自当前布局代次”。如果用户开始拖动时布局为 16平行视界分栏比例随后改变页面重新计算成代次 17旧手势在某个迟到回调里提交 68%就有可能覆盖新布局下用户刚做出的选择。commit因而先检查来源代次。上面的 0.42 起点为本轮固定样本正式实现应让startRatio来自每条 attempt 的真实启动快照并用唯一的手势实例 ID 追踪而不是让两次操作共用一个起点。诊断记录也不能被误认为可证明系统事件顺序的日志。它只代表应用层收到并处理到的事件。要调查“父级为什么没滚动”还需要父 Scroll 的独立滚动开始、滚动偏移量和组件挂载时刻等证据。GestureJudgeResult.CONTINUE是允许识别流程继续的决定真正的手势识别成功与否还受到系统识别器内部条件和当前手势组合影响这些结论不能靠应用层一个 boolean 直接代替。六、运行页与诊断页要回答不同的问题运行页只需要告诉用户对比进度和操作方法。图三里同一张山湖场景以 Before/After 分隔分割线当前在 68%底部摘要把“滑动前 42%”“水平位移 104vp”“内容宽度 400vp”“判断 ACCEPT”摆在一起。注意这两张视觉素材是为说明 UI 生成的样例图图像本身不代表真实超分算法效果这篇讨论的是手势协议不评价增强前后画质。诊断页则应服务开发和问题重放。图四不再铺满大图而是把GEST-1009-02B、当前DETAIL页面、layoutEpoch16、P-041 和 P-042 的判定结果分开展示。P-041 的dx104vp / width400vp与42%→68%构成可计算的关系P-042 的起点不在把手热区对比组件拒绝接管父级滚动观测为一次。屏幕顶部的时间和底部记录构成同一次演示序列没有暗示它们来自真实设备。比较值得注意的是两张图中“68%”承担不同角色。运行页的 68% 是用户当前能看到的业务状态诊断页里的 68% 是 P-041 最终提交的值。若将来遇到“日志显示 68%界面只显示 42%”就要继续排查状态层与渲染层的绑定而不是先怀疑手势偏移计算。反过来如果 UI 短暂到 68% 后又恢复 42%需要检查是否收到取消、是否有旧布局快照回贴或是否另一次用户操作完成了提交。这也是我不赞成把调试页简单设计成“成功/失败计数器”的原因。计数只能给出分布单次轨迹才给得出责任链。一个好用的诊断页应在不泄露用户图片的前提下能回答谁开始、谁拒绝、谁接受、按什么几何计算、最后是谁修改了可见状态。七、需要补上的三个真实设备验证层第一层是几何。最小宽度、平板分栏、折叠屏展开以及拖动系统分栏之后图片容器的实际可用宽度都可能不同。验收时应在当前组件尺寸下计算把手位置不把设备像素与 vp 混用。窗口旋转或双栏宽度变化发生在手势中途时要检查是否取消旧尝试、更新布局代次并让新的手势从新几何开始。只调整遮罩的外观却不更新触控热区会造成“看得见把手却摸不到”的问题。第二层是识别。测试至少包括把手中心轻触后横拖、从热区边缘开始斜拖、热区外纵拖、快速回拉、长按后拖动、双指移动、取消事件及页面切换。对于父级 Scroll一定单独观测实际滚动是否发生。P-042 的REJECT不是合格的“父滚动成功”证据需要同时看到父容器位移变化才能写下parentScroll1。如果父容器本来就在边界、被禁用或另有手势竞争就算滑块让行父级也可能保持不动。第三层是可访问性与使用方式。手势只是快捷交互不应该是完成比较的唯一入口。为可聚焦的控件准备明确的语义文本、可操作的增减按钮以及重置到初始位置的路径可以降低精确指尖拖动带来的门槛。若加入动画动画结束也不能重写用户已提交比例视觉插值与业务状态应该是两个层次。对于无法直接使用滑块的用户文本“当前显示增强图 68%”比一个单独的圆点更有意义。真实设备日志还需要记录目标 SDK/API 版本、设备形态、交互入口和截图对应的操作步骤。HarmonyOS 7 的平行视界资料面向大屏多任务介绍了左右推挤、固定覆盖及灵活分栏等场景它并未替应用承诺内部图片对比组件的手势优先级。后者仍需根据 ArkUI 手势参考和最终运行环境验证。把二者混为一种“系统帮我处理冲突”的能力会让工程边界失真。八、以一条反例作为下一轮起点如果把 PaneCompareLab 继续做下去我会故意构造一个更难的用例P-041 开始时详情宽 400vp已经拖动一半用户同时调整了系统分栏比例导致详情宽度变成 320vp。此时重新套用104/320会得到另一个目标值显然不能拿它证明原操作完成。合理的选择是结束旧布局代次的临时手势保存此前已提交的 42%更新视图尺寸后重新等待触点究竟是取消还是保留视觉预览需要产品明确约定。这次工程判断的核心不在一个神奇的手势参数而在给事件划清所有权系统分栏改变页面几何ArkUI 识别器决定手势流程是否继续业务组件根据命中热区决定自己是否有资格参与最终的比较比例再由当前页面代次授权提交。四件事放在不同层才能解释“滑块让行”和“父级真的滚动了”为什么不是一回事。读者可以直接用演示数字检查算法42% 加上 104/400得到 68%命中区内的 P-041 允许比较手势继续命中区外的 P-042 被当前比较识别器拒绝。至于这两次操作在具体真机手势竞争中的识别次序和父级接管结果还需要通过 DevEco Studio、目标 SDK 和真实设备逐条验证。对技术文章来说这样留下可证伪的边界比写一句“已经丝滑解决冲突”更负责。资料核对华为开发者联盟《PanGesture》https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-basic-gestures-pangesture-V5 。华为开发者联盟《Custom Gesture Judgment / onGestureJudgeBegin》https://developer.huawei.com/consumer/en/doc/harmonyos-references-V14/ts-gesture-customize-judge-V14 。华为开发者联盟《HarmonyOS 7API 26新能力解读平行视界开启大屏多任务新体验》https://developer.huawei.com/consumer/cn/forum/topic/0201221235973021541 。上述资料用于核对手势枚举、回调与平行视界场景比较滑块、业务代次、日志字段和所有截图数据均为本文自行设计的演示模型。