上一篇我们解决了"能传",这一篇要解决"传哪"。同样一张图片,碰左边进素材库,碰中间进画布,碰右边进属性区——坐标到底是怎么变成业务落点的?
先讲个我调试时遇到的事。
我把CrossDrop工作台分成了三个区域:左边素材库,中间编辑画布,右边属性面板。代码写好以后,拿手机一碰——嘿,不管碰哪,图片全进了素材库。
我当时就懵了。系统明明给了触点坐标,为什么区域判断全错?
后来打日志才发现:系统给的坐标是屏幕坐标,我拿它直接和组件坐标比,差了整整一个标题栏的高度。
就这一个坑,调了一下午。
一、先搞清楚:四种坐标根本不是一回事
我们日常说"坐标",好像就是一个(x,y)。但在窗口系统里,至少有四种坐标,每两种之间都差着偏移量:
第一种——屏幕坐标。
整个显示器左上角是(0,0),你碰屏幕哪一点,系统返回的就是这个坐标。这个坐标是"全局"的,不管你开了几个窗口,它都是相对于整个屏幕算的。
第二种——窗口坐标。
相对于当前窗口的左上角。窗口标题栏、边框这些算进去没有?不同系统不一样。窗口在屏幕上移了,这个坐标会不会变?不会,它是窗口内部的。
第三种——页面坐标。
相对于应用内容区域。状态栏、标题栏、导航栏这些都不算,从真正的内容区域开始算。
第四种——组件局部坐标。
相对于某个具体组件的左上角。素材库有自己的坐标,画布有自己的坐标。
你看,从"屏幕上哪一点"到"哪个组件的哪一块",中间要换算四层。
少算任何一层,结果都是错的。
二、一步一步拆:触点坐标怎么变成业务落点
我们拿一个具体例子走一遍。假设用户碰屏幕位置是(x=300, y=200)。
第一步:屏幕坐标 → 窗口坐标
先知道这个触点落在哪个窗口里。
系统会告诉你:这个点属于CrossDrop主窗口。窗口在屏幕上的位置是(offsetX=100, offsetY=50)。
那窗口内部坐标就是:
- 窗口内x = 300 - 100 = 200
- 窗口内y = 200 - 50 = 150
这一步你得问窗口要它的位置。窗口移了,位置变了,换算公式就要跟着变。
第二步:窗口坐标 → 页面坐标
窗口里不是全是内容。上面有标题栏,左边有侧边栏。
标题栏高度是40px。那内容区域的起点就是y=40。
页面坐标:
- 页面x = 200(假设左边没有边距)
- 页面y = 150 - 40 = 110
这一步最容易漏。我第一次调的时候就忘了减标题栏高度,结果所有区域判断都往下偏了一截。
第三步:页面坐标 → 组件区域判断
现在知道了内容区域里的位置,接下来判断落在哪个业务区域。
CrossDrop工作台布局大概是这样:
- 左边素材库:x从0到250
- 中间画布:x从250到750
- 右边属性区:x从750到1000
页面x=200,小于250,那就是落在素材库区域。
碰中间画布位置,x大概是500,落在250~750之间,那就是插入画布。
碰右边属性区,x大概是800,大于750,那就是进入待处理资源区。
这段代码解决什么问题:坐标转换工具。
文件:coordinate/CoordinateConverter.ets
用途:系统坐标转应用业务坐标
接入位置:触点事件回调后
classCoordinateConverter{// 窗口在屏幕上的偏移privatewindowOffsetX:number=0;privatewindowOffsetY:number=0;// 标题栏高度privatetitleBarHeight:number=40;// 更新窗口位置updateWindowPosition(offsetX:number,offsetY:number){this.windowOffsetX=offsetX;this.windowOffsetY=offsetY;}// 屏幕坐标 → 页面内容坐标toContentCoordinate(screenX:number,screenY:number):{x:number;y:number}{return{x:screenX-this.windowOffsetX,y:screenY-this.windowOffsetY-this.titleBarHeight};}}第四步:组件区域匹配
光知道坐标落在哪个大块还不够,还要知道具体是哪个组件。
这时候有两种做法:
做法一:自己算边界。
每个区域记录自己的位置和尺寸,判断坐标在不在这个矩形里。
做法二:用系统的HitTest。
让ArkUI自己做命中测试,直接返回落在哪个组件上。
自己算的好处是可控,坏处是布局变了边界也要跟着改。HitTest的好处是自动跟随布局,坏处是有时候你想要的业务区域和组件边界不完全一致。
这段代码解决什么问题:区域匹配判断。
文件:router/RegionMatcher.ets
用途:根据坐标判断业务落点
接入位置:坐标转换完成后
interfaceRegion{id:string;name:string;x:number;y:number;width:number;height:number;handler:string;}classRegionMatcher{privateregions:Region[]=[];addRegion(region:Region){this.regions.push(region);}match(x:number,y:number):Region|null{for(letregionofthis.regions){if(x>=region.x&&x<=region.x+region.width&&y>=region.y&&y<=region.y+region.height){returnregion;}}returnnull;}}// 使用constmatcher=newRegionMatcher();matcher.addRegion({id:'material',name:'素材库',x:0,y:0,width:250,height:800,handler:'add_material'});matcher.addRegion({id:'canvas',name:'编辑画布',x:250,y:0,width:500,height:800,handler:'insert_canvas'});matcher.addRegion({id:'property',name:'属性区',x:750,y:0,width:250,height:800,handler:'add_pending'});三、坐标这块最容易踩的坑
坐标转换看起来简单,实际全是细节。我踩过的坑列一下:
第一个坑:状态栏高度没算。系统返回的坐标到底算不算状态栏?不同设备不一样。手机和PC不一样,横屏和竖屏也不一样。这个要实测,别想当然。
第二个坑:窗口标题栏偏移。PC上窗口有标题栏,你是从窗口边框开始算,还是从客户区开始算?这个差几个像素,判断就错了。
第三个坑:缩放比例。有些设备有屏幕缩放,比如125%缩放。你拿到的坐标是物理像素还是逻辑像素?这个一定要搞清楚。
第四个坑:窗口移动了。用户把窗口从左边拖到右边,你还在用旧的窗口偏移量算,结果全错。所以窗口位置变化的时候,要监听并更新。
第五个坑:横竖屏切换。手机竖屏和横屏,窗口尺寸完全变了。区域边界也要跟着重算。
第六个坑:自由窗口尺寸变了。PC上用户可以拖窗口边缘调整大小,窗口尺寸变了,你的区域边界也要重新布局。
第二篇总结:
坐标转换这件事,核心不是写几行换算代码,而是搞清楚四种坐标之间的层级关系:屏幕坐标 → 窗口坐标 → 页面坐标 → 组件局部坐标。
每一层都有自己的偏移量,少算一层结果就错。
工程上最容易翻车的不是算法,是细节:状态栏高度、标题栏偏移、缩放比例、窗口移动、横竖屏切换——这些环境变量任何一个变了,你的坐标换算就要跟着变。
下一篇我们把难度再往上提一级:PC上同时开了三个窗口,用户碰屏幕的时候,怎么知道他碰的是哪个窗口?