4. getX与getRawX的区别:底层实现、应用场景与偏移根源
好,咱们进入第4章。这一章要聊的,是触摸调试里最基础、也最容易踩坑的一对兄弟——getX() 和 getRawX()。
我见过不少刚入行的同学,在屏幕上画线时发现手指明明没动,线条却自己“跳”了一下。排查半天,最后发现是这两个方法用混了。说白了,一个给的是“相对坐标”,一个给的是“绝对坐标”。但为什么会有这种区别?底层到底怎么算的?咱们今天一次讲透。
4.1 先看底层实现:它们分别从哪里拿数据?
先上代码。Android 里,MotionEvent 是触摸事件的载体。我们调用的 getX() 和 getRawX(),最终指向的是 native 层不同的字段。
// MotionEvent.java (简化示意)
public final float getX() {
return nativeGetAxisValue(mNativePtr, AXIS_X, 0, mHistoryPos);
}
public final float getRawX() {
return nativeGetRawAxisValue(mNativePtr, RAW_X, 0, mHistoryPos);
}
看到没?一个走 AXIS_X,一个走 RAW_X。在 native 层,AXIS_X 存的是 相对于当前 View 左上角的偏移,而 RAW_X 存的是 相对于屏幕左上角的绝对坐标。
我当年第一次看源码时,以为 getX() 就是 getRawX() - getLeft()。后来发现不完全对——因为 getX() 还会考虑父容器的滚动偏移和变换矩阵。嗯,这里要注意。
4.2 核心差异对比表
| 对比维度 | getX() / getY() | getRawX() / getRawY() |
|---|---|---|
| 坐标系原点 | 当前 View 左上角 | 屏幕左上角 |
| 受父容器滚动影响 | 是(ScrollView、RecyclerView 滚动后坐标会变) | 否(始终不变) |
| 受 View 变换影响 | 是(平移、缩放、旋转后坐标会变) | 否 |
| 典型应用场景 | View 内部的拖拽、绘制、点击判断 | 全局手势轨迹、跨 View 区域判断、弹窗定位 |
| 偏移风险 | 父容器滚动或变换时,坐标“漂移” | 无偏移,但需要手动转换到 View 坐标系 |
4.3 为什么会导致偏移?三个典型场景
偏移不是 bug,是坐标系没对齐。我总结了三类最常见的“偏移事故”:
场景一:父容器滚动后,getX() 坐标“乱跳”
假设你有一个 ScrollView,里面放了一个可拖拽的 View。手指按住 View 往下拖,同时 ScrollView 也在滚动。这时候 getX() 返回的值会突然变小——因为 View 的左上角随着滚动往上跑了。
偏移公式:
getX() = getRawX() - view.getLeft() - 父容器已滚动的距离
如果父容器滚动,view.getLeft() 不变,但 getX() 会变。这就是偏移的根源。
场景二:View 应用了缩放/旋转
我记得有一次做图片查看器,用户双指缩放后,再单指拖拽图片,拖拽点总是对不上。排查后发现,getX() 返回的是变换后的坐标,而我的拖拽逻辑用的是变换前的坐标。说白了,getX() 已经帮你把矩阵变换算进去了,但你不知道。
场景三:跨 View 区域判断
比如你要判断手指是否滑出了当前 View 的边界。用 getX() 判断,永远在 View 的局部坐标系里,永远出不去——因为 getX() 的范围就是 [0, viewWidth]。这时候必须用 getRawX() 配合 getLocationOnScreen() 来算。
4.4 各自的应用场景:什么时候用哪个?
我个人习惯这样分:
- 用 getX(): 当你的操作只跟当前 View 内部有关。比如自定义 View 里画一条跟随手指的线、按钮的按下效果、列表项的滑动删除。这些场景下,你只关心手指在 View 内部的位置,父容器怎么滚跟你没关系。
- 用 getRawX(): 当你的操作需要跟屏幕坐标系打交道。比如全局悬浮窗的拖拽、跨 View 的拖放(Drag and Drop)、手势轨迹的录制与回放。这些场景下,你需要一个“不撒谎”的坐标。
我的一个小技巧:
如果你不确定该用哪个,先问自己一个问题:“这个坐标要不要跟其他 View 的位置做比较?” 如果要,用 getRawX();如果不要,用 getX()。
4.5 避坑指南:我曾经踩过的三个坑
- 坑一: 我曾经在
RecyclerView的 item 里做拖拽排序,用getX()计算拖拽偏移。结果列表一滚动,拖拽的 item 直接飞出去了。后来改成getRawX() - item.getLeft()才稳定。 - 坑二: 做手势绘制时,用
getX()记录轨迹点,然后回放。结果回放时 View 的位置变了,轨迹对不上。解决方案:记录时用getRawX(),回放时再转成当前 View 的局部坐标。 - 坑三: 在
onTouchEvent里同时用getX()和getRawX()做判断,结果两个坐标系混用,逻辑乱成一团。我的建议是:一个事件处理流程里,只选一个坐标系,不要混着用。
4.6 一张图看懂 getX 与 getRawX 的关系
下面这张 SVG 图,把两个坐标系的差异和转换关系画清楚了。你仔细看,就能明白偏移是怎么产生的。
4.7 实战代码:如何正确转换?
最后给一段我常用的工具方法。当你需要在 getRawX() 和 getX() 之间转换时,直接调就行。
// 将屏幕绝对坐标转换为 View 局部坐标
public static float screenToViewX(View view, float rawX) {
int[] location = new int[2];
view.getLocationOnScreen(location);
return rawX - location[0];
}
// 将 View 局部坐标转换为屏幕绝对坐标
public static float viewToScreenX(View view, float localX) {
int[] location = new int[2];
view.getLocationOnScreen(location);
return localX + location[0];
}
注意: getLocationOnScreen() 必须在 View 完成布局后调用,否则返回 0。建议在 onWindowFocusChanged() 或 View.post() 中获取。
好了,这一章的核心就是:getX() 是局部坐标,getRawX() 是全局坐标。偏移的本质,是坐标系没对齐。 你只要记住这个,以后遇到触摸偏移问题,第一反应就是去查“我用的哪个坐标系?目标坐标系是什么?” 问题就解决了一半。