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 图,把两个坐标系的差异和转换关系画清楚了。你仔细看,就能明白偏移是怎么产生的。

getX 与 getRawX 坐标系关系图 屏幕坐标系(getRawX / getRawY) 父容器(ScrollView / RecyclerView) 子 View getX() 坐标系原点 触摸点 getRawX = 屏幕绝对坐标 getX = 相对 View 的坐标 View 左上角到屏幕顶部的距离 偏移公式: getX = getRawX - view.getLeft() - 父容器滚动偏移 - 变换矩阵偏移

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() 是全局坐标。偏移的本质,是坐标系没对齐。 你只要记住这个,以后遇到触摸偏移问题,第一反应就是去查“我用的哪个坐标系?目标坐标系是什么?” 问题就解决了一半。