在3.0时代之前,要判断一个点是否落在 View 上只需要两步:第一步:得到 View 的 Rect,第二步:判断点是否再这个 Rect 内。
但从 Android 3.0 开始这样的简单日子就结束了。
原因在于,Google 为 Android 3.0 提供了一套新的动画框架:Property Animation 。View/ViewGroup为此获得了强大的动画能力,但代价是View/ViewGroup的实现比以前更复杂了。3.0 前的 View/ViewGroup 在被画到画布前只会经过一次矩阵变换(如果用户不自定义的话);但现在,View/ViewGroup 的矩阵变换变为两次,并且一些新的方法和字段被添加到 View/ViewGroup 类,这些都增加了 View/ViewGroup 类的复杂程度。
Android 3.0前的View类并不持有matrix,只有canvas才持有matrix,View只有通过修改canvas的matrix才能完成一些矩阵的基 本操作(移位、旋转、缩放)。但从3.0开始,为了更好的用户体验,Google让View也亲自持有 matrix(由于这个matrix是为 property animation 服务的,因此就称它为 property matrix),这让 View 在每次 Draw 时增加了一层新的矩阵变换。但仅仅在『绘制』时增加一层转换是不够的,因为绘制只是输出,View还要处理输入(比如touch event),因此View还需要为触摸处理加一层矩阵变换。简而言之,所有和输入、输出相关的操作都需要加一层 property matrix 矩阵变换。
具体来说, View 持有的 property matrix 在 DispatchDraw 和 DispatchTouchEvent 分别被计算一次:DispatchDraw 时会用 Bitmap 乘以这个矩阵;DispatchTouchEvent 则会用 MotionEvent 的坐标(x,y)乘以这个矩阵的逆矩阵(Matrix.invert)。这可以理解为,显示的时候 view 要处理的是自己的bitmap,而在处理 motionevent 的时候,view需要处理的是 motion event。
再回到最初的问题:怎样判断某个点落在一个View里。
View 提供一个方法 getHitRect()
,文档说它的作用是 Hit rectangle in parent‘s coordinates
。但实际上这个方法得到的结果是错误的。那么 getHitRect() 有什么问题呢,先看看 getHitRect()
的实现:
/**
* Hit rectangle in parent‘s coordinates
*
* @param outRect The hit rectangle of the view.
*/
public void getHitRect(Rect outRect) {
updateMatrix();
final TransformationInfo info = mTransformationInfo;
if (info == null || info.mMatrixIsIdentity || mAttachInfo == null) {
outRect.set(mLeft, mTop, mRight, mBottom);
} else {
final RectF tmpRect = mAttachInfo.mTmpTransformRect;
tmpRect.set(-info.mPivotX, -info.mPivotY,
getWidth() - info.mPivotX, getHeight() - info.mPivotY);
info.mMatrix.mapRect(tmpRect);
outRect.set((int) tmpRect.left + mLeft, (int) tmpRect.top + mTop,
(int) tmpRect.right + mLeft, (int) tmpRect.bottom + mTop);
}
}
getHitRect 的做法是:计算出view在矩阵变换前的rect,然后再对这个rect进行一次矩阵变换,得到当前在parent的rect。 因此关键在 getHitRect() 是如何得到原始 rect 的,它以:
left: -info.mPivotX,
top: -info.mPivotY,
right: getWidth() - info.mPivotX,
bottom: getHeight() - info.mPivotY);
这样的数据作为原始 rect 。
这样做是错的。原因有两个:
- 没有引入 mScrollX 和 mScrollY
- -info.mPivotX 不能作为 left; -info.mPivotY 不能作为 top; getWidth() - info.mPivotX 不能作为 right; getHeight() - info.mPivotY) 也不能作为 bottom。
第二个错误简单的想象一下就能得到答案:假设在窗口坐标 (0, 0, 2, 2) 有一个矩形,然后以 [1, 1] 为中轴缩放 0.5 倍,这样就得到了一个 (0.5, 0.5, 1.5, 1.5) 的矩形,显然 getHitRect() 不能得到正确的结果。
因此,怎样判断某个点是否落在在一个 View 里就得重新写一个方法,代码如下:
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
*/
private boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
// get x, y offset
float localX = x + getScrollX() - child.getLeft();
float localY = y + getScrollY() - child.getTop();
// restore location
final float[] localXY = new float[2];
localXY[0] = localX;
localXY[1] = localY;
final Matrix inverseMatrix = new Matrix();
child.getMatrix().invert(inverseMatrix);
inverseMatrix.mapPoints(localXY);
localX = localXY[0];
localY = localXY[1];
// fill out data
final boolean isInView = pointInView(child, localX, localY);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(localX, localY);
}
return isInView;
}
/**
* Determines whether the given point, in local coordinates is inside the view.
*/
private static boolean pointInView(View view, float localX, float localY) {
return localX >= 0 && localX < (view.getRight() - view.getLeft())
&& localY >= 0 && localY < (view.getBottom()- view.getTop());
}
上面的方法将点(x,y)用view的inverse matrix进行了一次变换,然后再判断变换后的点是否落在view的rect内。
刚接触到 Property Animation 时我觉得它真是一个非常cool的结构,至少从使用者的角度来看是。但现在我认为它并不是一个成熟的方案,因为一些复杂的情况[注1] google 并没有做考虑,所以,在使用 Property Animation 和与 Property Animation 有关的 api 时一定要小心。
注1: 另一个复杂的情况是对View用多点触摸进行scale操作。由于 MotionEvent 没有 getRawX(int) 方法,它就只能用 getX(int) 来模拟 getRawX(int) ,getX(int)使用的是相对于ViewGroup的坐标,这会导致scale计算出错。