前两篇我们默认应用只有一个窗口。但PC上干活谁不是开一堆窗口?素材库一个、编辑器一个、预览一个,手机一碰,到底传给谁?
上一篇写完坐标转换,我自己拿PC测了一下。
我同时开了三个CrossDrop窗口:左边素材库,中间主编辑器,右边预览窗口。拿手机碰中间那个窗口的画布区域——结果呢?图片跑到左边素材库去了。
我当时就愣了。坐标明明算对了,怎么会错窗口?
后来想明白了:我只算了"这个点在哪个区域",没算"这个点在哪个窗口"。
屏幕上有三个窗口,坐标转换是相对于哪个窗口算的?
这个问题不解决,多窗口下就是乱传。
一、先搞清楚:多窗口到底多在哪
我们上一篇的模型是:一个应用,一个主窗口,一个页面,三个区域。
但PC和2in1设备不是这样的。
第一种窗口——主窗口。
应用启动默认打开的那个,里面是完整的工作台。
第二种窗口——素材库窗口。
用户可以单独开一个素材管理窗口,拖到屏幕另一边。
第三种窗口——预览窗口。
单独开个小窗实时预览编辑效果。
这三个窗口同时在屏幕上,每个都有自己的位置、尺寸、内容区域。
用户拿手机碰屏幕的时候,系统告诉你:触点在屏幕坐标(x=500, y=300)。
你得先回答一个问题:这个坐标落在哪个窗口里?
知道了是哪个窗口,才能用那个窗口的位置去做坐标转换。知道了窗口,才能判断是素材库区域、画布区域还是预览区域。
你看,比上一篇多了一层:先选窗口,再转坐标,再匹配区域。
二、WindowTargetRegistry:给每个窗口上户口
我们需要一个东西,专门记录"当前应用有哪些窗口、每个窗口在哪、每个窗口该怎么处理数据"。
我叫它WindowTargetRegistry——窗口目标注册表。
它要干三件事:
第一件:窗口创建时注册。
主窗口创建了,告诉注册表:我在这,我的位置是(x1,y1),我的业务类型是editor。素材库窗口创建了,告诉注册表:我在这,我的业务类型是material。
第二件:窗口位置变化时更新。
用户把窗口从左边拖到右边了,注册表要更新这个窗口的最新位置。窗口大小变了,也要更新。
第三件:窗口关闭时注销。
预览窗口关了,注册表要把它删掉。不然下次碰的时候,还去找这个已经不存在的窗口,就出问题了。
这段代码解决什么问题:窗口目标注册表。
文件:registry/WindowTargetRegistry.ets
用途:管理所有可接收分享的窗口
接入位置:窗口创建和销毁时
interfaceWindowTarget{windowId:string;type:'editor'|'material'|'preview';offsetX:number;offsetY:number;width:number;height:number;isActive:boolean;}classWindowTargetRegistry{privatetargets:Map<string,WindowTarget>=newMap();// 注册窗口register(target:WindowTarget){this.targets.set(target.windowId,target);console.info('注册窗口:'+target.type+' at ('+target.offsetX+','+target.offsetY+')');}// 更新窗口位置updatePosition(windowId:string,x:number,y:number,w:number,h:number){lettarget=this.targets.get(windowId);if(target){target.offsetX=x;target.offsetY=y;target.width=w;target.height=h;}}// 注销窗口unregister(windowId:string){this.targets.delete(windowId);console.info('注销窗口:'+windowId);}// 根据屏幕坐标找落在哪个窗口里findWindowAt(screenX:number,screenY:number):WindowTarget|null{for(lettargetofthis.targets.values()){if(screenX>=target.offsetX&&screenX<=target.offsetX+target.width&&screenY>=target.offsetY&&screenY<=target.offsetY+target.height){returntarget;}}returnnull;}}三、完整路由链:从触点到业务处理器
现在我们有了窗口注册表。一条完整的路由链就清楚了:
第一步:触点 → 找窗口。
屏幕坐标(x=500, y=300),问注册表:这个点落在哪个窗口?注册表遍历所有窗口,发现落在编辑器窗口里。
第二步:窗口 → 转坐标。
知道了是编辑器窗口,拿这个窗口的偏移量做坐标转换。屏幕坐标减去窗口偏移,得到窗口内坐标。再减标题栏,得到内容区域坐标。
第三步:坐标 → 匹配区域。
内容区域坐标x=200,判断落在编辑器窗口的哪个业务区域?左边是素材面板,中间是画布,右边是属性栏。x=200在中间画布区域。
第四步:区域 → 调处理器。
找到了"画布区域",调对应的处理器:insertCanvas()。
这段代码解决什么问题:完整路由链。
文件:router/TouchRouter.ets
用途:从触点坐标到业务处理的完整路由
接入位置:收到精准分享事件后
classTouchRouter{privateregistry:WindowTargetRegistry;privateregionMatcher:RegionMatcher;privatedispatcher:BusinessDispatcher;constructor(registry:WindowTargetRegistry){this.registry=registry;this.regionMatcher=newRegionMatcher();this.dispatcher=newBusinessDispatcher();}// 完整路由:触点 → 窗口 → 区域 → 业务routeTouch(screenX:number,screenY:number,payload:SharePayload){// 第一步:找窗口letwindow=this.registry.findWindowAt(screenX,screenY);if(!window){console.warn('触点不在任何已注册窗口内');return;}console.info('命中窗口:'+window.type);// 第二步:坐标转换letcontentX=screenX-window.offsetX;letcontentY=screenY-window.offsetY-40;// 减标题栏// 第三步:匹配业务区域letregion=this.regionMatcher.match(contentX,contentY);if(!region){console.warn('坐标不在任何业务区域内');return;}console.info('命中区域:'+region.name);// 第四步:分发到业务处理器this.dispatcher.dispatch(region.handler,payload);}}四、多窗口下最容易踩的坑
多窗口比单窗口复杂多了,坑也更多:
第一个坑:窗口层级变化。
两个窗口重叠了,上面那个盖住下面那个。你碰到的是上面那个窗口,但注册表里还把它算进去了。怎么判断哪个窗口在最前面?要问窗口系统拿窗口层级顺序。
第二个坑:窗口移动后路由残留。
窗口从左边拖到右边了,但注册表还存着旧位置。用户碰新位置,找不到窗口;碰旧位置,那个地方已经没有窗口了。所以窗口移动事件一定要监听。
第三个坑:窗口关闭后残留路由。
预览窗口关了,但注册表里没删掉。下次碰那个位置,还去找这个已经不存在的窗口,报错。窗口关闭时一定要注销。
第四个坑:悬浮窗覆盖。
有个小的悬浮窗盖在主窗口上面,用户碰的是悬浮窗,但你的路由判断成了主窗口。悬浮窗也算一个窗口,要纳入注册表。
第五个坑:平板和PC的窗口模型不一样。
平板上可能就是全屏单窗口,PC上可以多窗口自由拖。你的代码不能写死,要适配不同设备形态。
第六个坑:自由窗口尺寸变化。
用户拖窗口边缘调大小,窗口尺寸变了,区域边界也要跟着重算。不能写死250px、500px这种固定值。
第三篇总结:
多窗口的核心问题不是"坐标怎么算",而是"先知道是哪个窗口"。
单窗口模型里,你默认只有一个应用窗口,所以直接算坐标就行。多窗口下,你得先回答"这是谁的地盘",再谈坐标转换。
工程上最容易翻车的是窗口生命周期:创建时注册、移动时更新、关闭时注销——这三步少一步,路由就是错的。
下一篇我们换个方向:一次碰过来20张图,怎么保证传输不乱?