同一只鼠标,升级后画布拖得更快:HarmonyOS 7 rawDeltaX/Y不能直接当界面位移

在电脑端画布里拖动鼠标,升级前移动一小段,升级后画布一下滑出去很远。代码里没有改灵敏度,问题可能出在rawDeltaX/Y的含义恢复了原本的定义,而旧代码仍把返回值当成已经换算过的界面增量。

HarmonyOS 7将这两个字段改为鼠标硬件的原始移动数据。本文不建议看到raw就统一除一个常数,而是先决定场景需要什么单位:保留旧画布交互效果,还是按原始输入控制视角。两个案例分别给出边界函数和测试。

官方Beta1变更说明更新于2026年8月19日,本文9月28日核对。计算模型在宿主环境运行通过,尚未在API26设备接入鼠标测量。文中的数值是可复现测试输入,不是某款鼠标的采样结果。

变化的是输入语义,不是鼠标突然更灵敏

接口从API15提供。变更前返回的是原始移动数据缩小X倍,X为系统显示大小比例;变更后返回硬件原始移动数据。官方汇总把它列为全部生效,没有给出仅target26生效的隔离条件。

原始移动数据不等于屏幕指针坐标,也不能直接说成固定毫米数。硬件如何上报由设备决定,不同鼠标和设置可能不同。字段名里有Delta,只能说明它是增量,不能证明它已经符合你的画布坐标系。

官方提供的恢复旧效果方式是使用当前UIContext的px2vp转换。这里应准确理解成此次适配建议,不要由函数名推导“硬件原始数据本来就是屏幕像素”的结论。

界面平移与原始输入控制使用不同的转换边界

案例一:二维画布保留原来的显示效果

先在输入边界完成转换,业务平移只接收明确单位。不要在鼠标回调里转换一次,又在通用移动函数里转换一次。

// 接入现有组件的鼠标处理器;这里只演示数值转换。
private toLegacyDelta(event: MouseEvent): number[] {
  const context = this.getUIContext();
  return [context.px2vp(event.rawDeltaX), context.px2vp(event.rawDeltaY)];
}

实际画布还应判断是否处于拖动模式、是否按下正确按钮、是否应该接收本次事件。本文聚焦单位迁移,不把所有鼠标移动都当拖动。

下面的函数用注入方式接收转换器,因此可以验证调用次数及结果。vp到画布世界坐标还要除以视图缩放倍数;这是应用的坐标模型,不是rawDelta字段的系统定义。

type Delta = { x:number; y:number; unit:'raw'|'vp' };
type Point = { x:number; y:number };
function finitePair(x:number,y:number): void {
  if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error('invalid delta');
}
function toViewport(delta:Delta, px2vp:(value:number)=>number): Delta {
  finitePair(delta.x,delta.y);
  if (delta.unit === 'vp') return {...delta};
  const x = px2vp(delta.x), y = px2vp(delta.y);
  finitePair(x,y);
  return {x,y,unit:'vp'};
}
function panWorld(position:Point, delta:Delta, zoom:number): Point {
  if (delta.unit !== 'vp') throw new Error('viewport delta required');
  if (!Number.isFinite(zoom) || zoom <= 0) throw new Error('invalid zoom');
  return {x:position.x + delta.x/zoom, y:position.y + delta.y/zoom};
}
function check(value:boolean): void { if (!value) throw new Error('assertion failed'); }
let conversions = 0;
const mockConvert = (n:number):number => { conversions++; return n/2; };
const viewport = toViewport({x:20,y:-8,unit:'raw'},mockConvert);
check(viewport.x === 10 && viewport.y === -4);
const unchanged = toViewport(viewport,mockConvert);
check(conversions === 2 && unchanged.x === 10);
const moved = panWorld({x:100,y:100},viewport,2);
check(moved.x === 105 && moved.y === 98);
let rejected = false;
try { panWorld({x:0,y:0},{x:20,y:0,unit:'raw'},1); } catch { rejected = true; }
check(rejected);

mockConvert除以2只是测试桩,用来检查数据只经过一次转换。生产代码应调用所属窗口的UIContext,不应把这个2复制进项目。窗口或显示配置变化时,缓存的手写比例尤其容易过期。

这里为已转换数据添加unit标记,目的不是让类型系统解决所有坐标问题,而是使错误跨层传递更明显。若项目已经有严格的输入事件类型,可以把raw和viewport定义成两个不同接口,减少字符串状态的组合空间。

案例二:三维视角直接使用原始增量

相机旋转通常不是移动一个vp距离,而是按输入增量和应用灵敏度计算角度。此时未必应该恢复旧显示缩放效果。若继续把系统显示比例混入角度,调整桌面显示大小就可能改变控制手感。

以下模型把灵敏度明确为“每个原始输入单位对应的角度”,并限制俯仰范围。它没有承诺不同鼠标的物理距离一致,那需要硬件与产品层校准。

type ViewAngle = { yaw:number; pitch:number };
function rotateView(current:ViewAngle, raw:Delta, sensitivity:number): ViewAngle {
  if (raw.unit !== 'raw') throw new Error('raw input required');
  finitePair(raw.x,raw.y);
  if (!Number.isFinite(sensitivity) || sensitivity < 0) throw new Error('invalid sensitivity');
  const yaw = current.yaw + raw.x*sensitivity;
  const pitch = Math.max(-89,Math.min(89,current.pitch + raw.y*sensitivity));
  return {yaw,pitch};
}
const angle = rotateView({yaw:0,pitch:80},{x:20,y:100,unit:'raw'},0.2);
check(angle.yaw === 4 && angle.pitch === 89);
const disabled = rotateView({yaw:3,pitch:4},{x:20,y:100,unit:'raw'},0);
check(disabled.yaw === 3 && disabled.pitch === 4);
let wrongUnit = false;
try { rotateView({yaw:0,pitch:0},{x:1,y:1,unit:'vp'},1); } catch { wrongUnit = true; }
check(wrongUnit);

相机系统还需要处理输入捕获、焦点切换和异常大增量。不能只因为函数返回角度,就认为已经完成游戏控制器。建议先将单位迁移与平滑算法分开验证:否则加入平滑后,手感变化可能被掩盖,根因更难找。

为什么不统一乘一个“兼容系数”

因为两种场景的目标不同。画布要保留界面平移效果,可以按官方方式转换;相机要消费原始输入,则应明确应用灵敏度。一个全局系数同时修两种场景,看似省事,实际把显示单位和设备输入重新混在一起。

场景输入处理仍需检查
保留旧画布交互UIContext.px2vp一次缩放、拖动模式、窗口上下文
视角控制原始增量乘应用灵敏度鼠标差异、俯仰限制、焦点
已有库已经转换不再重复转换库版本与输入接口约定
Native接入检查对应RawDelta接口不要只修ArkTS一层

设备补测要记录什么

至少覆盖两个系统显示大小、两个画布缩放级别、正负方向、小幅移动和快速移动。每条记录保留系统版本、应用target、raw输入、转换后的值和最终业务位移。不要记录键盘输入内容或其它无关隐私。

还要检查Native侧OH_ArkUI_MouseEvent_GetRawDeltaX/Y,官方将它们列在同一变更里。跨语言封装可能已经做过转换,必须沿数据链找一遍,不能仅在最外层再补一次。

这次适配的判断标准不是“看起来没那么快了”,而是每一层都能解释自己的单位。先明确单位,再处理灵敏度,最后做设备体验验证,才能把版本变化和产品手感分开讨论。

资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐