[Android]通过setOnTouchListener实现移动View功能
需求
有时需要移动View,以前写的代码不太好,就上网上找,也不好用,和我自己写的都有的问题是View 会比光标的位置靠右下方。以前都忍了,偏就偏吧,现在决心要改,也可能是因为在这个特殊时期比较闲吧。
实现
正如上面所说,靠右下方,而且相比于靠右,靠下的程度更大。这很难不让人想到是状态栏造成的影响,而且单单是状态栏都还不够,还有一个ToolBar,所以要点在于如何去除这个高度。
/**
* Returns the original raw Y coordinate of this event. For touch
* events on the screen, this is the original location of the event
* on the screen, before it had been adjusted for the containing window
* and views.
*
* @see #getY(int)
* @see #AXIS_Y
*/
public final float getRawY() {
return nativeGetRawAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
}
getRawX 或者getRawY 返回的都是相对于整个屏幕的,在测试时,如果不加限制,甚至可以滑动到状态栏,事件一直有效。
/**
* {@link #getY(int)} for the first pointer index (may be an
* arbitrary pointer identifier).
*
* @see #AXIS_Y
*/
public final float getY() {
return nativeGetAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
}
可是看这个getX 函数根本不明白说的什么意思,甚至它们的实现完全一样,可能是mNativePtr 或者HISTORY_CURRENT 有什么不同吧,不管了。到网上查,是我们触摸的点相对这个我们设置触摸事件的View 的位置(下面有图)。
在每次设定位置时,需要的是设置当前的View 相对于父View 的坐标,在每次移动时getRawY 的值需要减去getY 的值,得到的值就是当前view相对整个屏幕的位置,在减去当前view 相对于父窗体的位置,这样得到的值就是父窗体相对于整个屏幕的位置,最后得到的值是跟我们没有关系的,在获得当前view 需要的位置时在减去这个值。父窗体是不会动的,所以后者的这个位置可以在MotionEvent.ACTION_DOWN 时获取,同样的,getY 的值也需要在这个时候获取,因为光标相对于当前的View 的位置也是不能改变的。
像这样:
case MotionEvent.ACTION_DOWN:
x = event.getX();
y = event.getY();
left = event.getRawX() - view.getLeft() - x;
top = event.getRawY() - view.getTop() - y;
return true;

当用户只是点击时,是不会有MotionEvent.ACTION_MOVE 事件的,所以在这个事件下记录当前用户是否进行的是移动操作。
case MotionEvent.ACTION_MOVE:
if (!moved) {
moved = true;
}
setXPosition(event.getRawX() - x - left);
setYPosition(event.getRawY() - y - top);
return true;

我们用光标相对于整个屏幕的位置减去父窗体的位置,减去光标相对于父窗体的位置,就是当前view 的左上角相对于父窗体的位置。
当用户抬手时,判断是否是移动了,如果移动了,那就不是点击事件,而且这个移动操作也结束了,恢复原样,如果不是,那就是点击事件,就调用performClick 函数。
case MotionEvent.ACTION_UP:
if (moved) {
moved = false;
} else {
v.performClick();
}
return true;
至于那个performClick:
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
这样onClickListener 就能够正常运行了,而不是被我们阻止了。
这还没有完:
/**
* Entry point for {@link #performClick()} - other methods on View should call it instead of
* {@code performClick()} directly to make sure the autofill manager is notified when
* necessary (as subclasses could extend {@code performClick()} without calling the parent's
* method).
*/
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
他提醒我不让我直接调用performClick ,而是使用performClickInternal ,说是为了保证autofill manager 工作。可是这个函数是个私有函数,可能说的不是我的这种情况吧。
至于perfomClick 的返回值,感觉不重要就不管了。如果想用这个返回值,就把v.performClick(); 部分改为return v.performClick();。
最后
如果是悬浮窗,view.getTop() 返回的总是0,所以需要通过LayoutParams来获取这个top 的值,并且left不再需要,因为它一直等于零。
WindowManager.LayoutParams layoutParams= (WindowManager.LayoutParams) v.getLayoutParams();
top = event.getRawY() -layoutParams.y- y;