坐标系基础:屏幕坐标系、View坐标系、触摸事件坐标系
做触摸偏移调试,说白了就是跟坐标系打交道。我记得刚入行那会儿,遇到一个诡异的点击偏移问题,折腾了两天没搞定。后来一位老同事看了一眼我的日志,说了一句:「你坐标系搞混了。」嗯,从那以后,我花了一整周把 Android 的三大坐标系彻底捋了一遍。
今天咱们就把这三个坐标系掰开揉碎讲清楚。你想想看,触摸事件从手指落到屏幕,到最终被你的 View 消费掉,中间经历了至少三次坐标变换。任何一个环节算错了,偏移就来了。
2.1 屏幕坐标系
屏幕坐标系,也叫绝对坐标系。它的原点在屏幕的左上角,X 轴向右为正,Y 轴向下为正。这个跟数学课上学的坐标系不一样——数学里 Y 轴是向上的,但屏幕坐标系是反的。
关键点:屏幕坐标系的原点永远在屏幕左上角,单位是像素(px)。
举个例子,假设屏幕分辨率是 1080×1920,那么右下角的坐标就是 (1079, 1919)。注意是从 0 开始数的,不是从 1。
我在项目中遇到过一个问题:用 MotionEvent.getRawX() 和 getRawY() 拿到的就是屏幕坐标系下的坐标。这个值不受状态栏、导航栏的影响,就是手指在屏幕上的绝对位置。
2.2 View 坐标系
View 坐标系是相对坐标系。每个 View 都有自己的坐标系,原点在 View 的左上角,X 轴向右,Y 轴向下。
说白了,View 坐标系就是「站在 View 自己的角度看世界」。不管这个 View 被放在屏幕的哪个位置,它的左上角永远是 (0, 0)。
| 方法 | 返回值含义 | 坐标系 |
|---|---|---|
getLeft() |
View 左边缘到父 View 左边缘的距离 | 父 View 坐标系 |
getTop() |
View 上边缘到父 View 上边缘的距离 | 父 View 坐标系 |
getX() |
View 左上角在父 View 中的 X 坐标 | 父 View 坐标系 |
getY() |
View 左上角在父 View 中的 Y 坐标 | 父 View 坐标系 |
getWidth() |
View 自身的宽度 | 自身坐标系 |
getHeight() |
View 自身的高度 | 自身坐标系 |
个人习惯:我调试偏移问题时,第一件事就是打印 View 的 getLeft()、getTop()、getX()、getY()。这四个值能告诉我 View 在父容器里的位置,是判断偏移的关键线索。
2.3 触摸事件坐标系
触摸事件坐标系稍微复杂一点。它取决于你通过哪个方法获取坐标:
event.getX()/event.getY():返回的是触摸点相对于当前 View 的坐标。也就是说,如果事件被分发到了一个 Button 上,那么 getX() 返回的就是手指在 Button 内部的位置。event.getRawX()/event.getRawY():返回的是触摸点在屏幕坐标系下的绝对位置。
为什么会这样?因为 Android 在事件分发过程中,会自动帮你做坐标转换。事件从 Window 往下传的时候,每经过一个 ViewGroup,就会把坐标减去该 ViewGroup 的偏移量。最终你拿到的 getX() 就是相对于当前 View 的坐标。
我曾经踩过的坑:在自定义 ViewGroup 里重写 onInterceptTouchEvent 时,直接用 event.getX() 判断触摸点是否在子 View 范围内。结果发现永远对不上。后来才意识到——getX() 是相对于当前 ViewGroup 的坐标,而子 View 的位置是相对于它自己的。正确的做法是用 event.getRawX() 减去子 View 在屏幕上的绝对位置。
2.4 三者之间的转换关系
这三个坐标系之间的转换,说白了就是加减法。没有高深的数学,就是加加减减。
核心公式就两个:
// 屏幕坐标 → View 坐标
viewX = rawX - view.getLeft() - parent.getLeft() - ... - 屏幕偏移
viewY = rawY - view.getTop() - parent.getTop() - ... - 屏幕偏移
// View 坐标 → 屏幕坐标
rawX = viewX + view.getLeft() + parent.getLeft() + ... + 屏幕偏移
rawY = viewY + view.getTop() + parent.getTop() + ... + 屏幕偏移
你想想看,一个 View 嵌套在多层 ViewGroup 里,它的屏幕位置就是所有父 View 的 left/top 累加,再加上它自己的 left/top。
Android 提供了现成的方法帮你算:
// 获取 View 在屏幕上的绝对位置
int[] location = new int[2];
view.getLocationOnScreen(location);
int screenX = location[0]; // View 左上角在屏幕上的 X
int screenY = location[1]; // View 左上角在屏幕上的 Y
// 触摸点相对于 View 的坐标
float viewX = event.getRawX() - screenX;
float viewY = event.getRawY() - screenY;
核心结论:触摸偏移调试的本质,就是搞清楚「手指落下的屏幕坐标」和「View 期望的坐标」之间的差值。而这个差值,往往就是坐标系转换没做对。
2.5 坐标系关系全景图
下面这张图把三个坐标系的关系画出来了。我建议你保存下来,调试的时候对照着看。
2.6 实战:如何用坐标系知识定位偏移
好了,理论说完了。咱们来点实际的。假设你遇到一个点击偏移问题,怎么用坐标系知识快速定位?
我的调试步骤是这样的:
- 打印原始坐标:在 onTouchEvent 里打印
event.getRawX()和event.getRawY()。这两个值最原始,不会骗人。 - 打印 View 位置:用
view.getLocationOnScreen()拿到 View 在屏幕上的左上角坐标。 - 计算期望坐标:用 raw 坐标减去 View 的屏幕坐标,得到触摸点相对于 View 的坐标。
- 对比实际值:把计算出来的值和
event.getX()/event.getY()对比。如果不一样,说明事件分发过程中坐标被篡改了。
// 调试代码模板
int[] viewLocation = new int[2];
targetView.getLocationOnScreen(viewLocation);
float rawX = event.getRawX();
float rawY = event.getRawY();
float expectedViewX = rawX - viewLocation[0];
float expectedViewY = rawY - viewLocation[1];
float actualViewX = event.getX();
float actualViewY = event.getY();
Log.d("CoordDebug", "Raw: (" + rawX + ", " + rawY + ")");
Log.d("CoordDebug", "View on screen: (" + viewLocation[0] + ", " + viewLocation[1] + ")");
Log.d("CoordDebug", "Expected view coords: (" + expectedViewX + ", " + expectedViewY + ")");
Log.d("CoordDebug", "Actual view coords: (" + actualViewX + ", " + actualViewY + ")");
if (Math.abs(expectedViewX - actualViewX) > 0.5f ||
Math.abs(expectedViewY - actualViewY) > 0.5f) {
Log.w("CoordDebug", "⚠️ 坐标不一致!存在偏移!");
}
一个小技巧:我习惯在调试时把 rawX/rawY 和 viewX/viewY 同时打印到日志里,然后用 Excel 拉个表格对比。肉眼扫一遍,偏移量一目了然。比盯着 Logcat 看效率高多了。
2.7 常见坐标系转换错误
最后,我总结几个我在项目中见过的坐标系转换错误,你遇到了可以直接对照排查:
| 错误场景 | 错误表现 | 根本原因 |
|---|---|---|
| 在 ViewGroup 中用 event.getX() 判断子 View 点击 | 点击区域偏移,越靠近屏幕边缘越明显 | getX() 是相对于 ViewGroup 的,不是相对于屏幕的 |
| 在 Dialog 或 PopupWindow 中直接用 getX() | 点击位置整体偏移 | Dialog 的坐标原点不是屏幕左上角,而是 Window 的左上角 |
| 在 RecyclerView 的 Item 中计算点击位置 | 滑动后点击偏移 | Item 的 getTop() 会随滑动变化,需要加上 RecyclerView 的滚动偏移 |
| 在 Fragment 中获取 View 位置时 Fragment 未附加 | getLocationOnScreen() 返回 (0,0) | View 还没有完成布局,坐标系尚未建立 |
我曾经踩过的坑:有一次在 PopupWindow 里做拖拽,发现手指和拖拽的 View 总是差一截。查了半天,发现 PopupWindow 的内容 View 的 getLocationOnScreen() 返回的是相对于 PopupWindow 自身的位置,而不是屏幕位置。后来改用 getRawX()/getRawY() 直接算,问题解决。
嗯,坐标系这块就讲到这里。你只要记住一句话:触摸偏移调试,就是搞清楚「手指在哪」和「View 以为手指在哪」之间的差值。 这个差值,就是坐标系转换没做对的地方。