【发布时间】:2011-09-18 14:33:48
【问题描述】:
在Android中,我们如何检测用户是否触摸按钮并拖出该按钮的区域?
【问题讨论】:
在Android中,我们如何检测用户是否触摸按钮并拖出该按钮的区域?
【问题讨论】:
我遇到了与 OP 相同的问题,因此我想知道 (1) 何时按下特定的 View 以及 (2a) 在 View 或 (2b) 上释放按下时当向下触摸移动到View 的范围之外时。我在这个线程中汇集了各种答案,创建了一个简单的View.OnTouchListener 扩展(名为SimpleTouchListener),这样其他人就不必摆弄MotionEvent 对象。该课程的来源可以在here 或此答案的底部找到。
要使用这个类,只需将其设置为View.setOnTouchListener(View.OnTouchListener)方法的参数如下:
myView.setOnTouchListener(new SimpleTouchListener() {
@Override
public void onDownTouchAction() {
// do something when the View is touched down
}
@Override
public void onUpTouchAction() {
// do something when the down touch is released on the View
}
@Override
public void onCancelTouchAction() {
// do something when the down touch is canceled
// (e.g. because the down touch moved outside the bounds of the View
}
});
这里是类的源代码,欢迎您添加到您的项目中:
public abstract class SimpleTouchListener implements View.OnTouchListener {
/**
* Flag determining whether the down touch has stayed with the bounds of the view.
*/
private boolean touchStayedWithinViewBounds;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchStayedWithinViewBounds = true;
onDownTouchAction();
return true;
case MotionEvent.ACTION_UP:
if (touchStayedWithinViewBounds) {
onUpTouchAction();
}
return true;
case MotionEvent.ACTION_MOVE:
if (touchStayedWithinViewBounds
&& !isMotionEventInsideView(view, event)) {
onCancelTouchAction();
touchStayedWithinViewBounds = false;
}
return true;
case MotionEvent.ACTION_CANCEL:
onCancelTouchAction();
return true;
default:
return false;
}
}
/**
* Method which is called when the {@link View} is touched down.
*/
public abstract void onDownTouchAction();
/**
* Method which is called when the down touch is released on the {@link View}.
*/
public abstract void onUpTouchAction();
/**
* Method which is called when the down touch is canceled,
* e.g. because the down touch moved outside the bounds of the {@link View}.
*/
public abstract void onCancelTouchAction();
/**
* Determines whether the provided {@link MotionEvent} represents a touch event
* that occurred within the bounds of the provided {@link View}.
*
* @param view the {@link View} to which the {@link MotionEvent} has been dispatched.
* @param event the {@link MotionEvent} of interest.
* @return true iff the provided {@link MotionEvent} represents a touch event
* that occurred within the bounds of the provided {@link View}.
*/
private boolean isMotionEventInsideView(View view, MotionEvent event) {
Rect viewRect = new Rect(
view.getLeft(),
view.getTop(),
view.getRight(),
view.getBottom()
);
return viewRect.contains(
view.getLeft() + (int) event.getX(),
view.getTop() + (int) event.getY()
);
}
}
【讨论】:
我从两个自定义扩展函数开始:
val MotionEvent.up get() = action == MotionEvent.ACTION_UP
fun MotionEvent.isIn(view: View): Boolean {
val rect = Rect(view.left, view.top, view.right, view.bottom)
return rect.contains((view.left + x).toInt(), (view.top + y).toInt())
}
然后聆听对视图的触摸。仅当 ACTION_DOWN 最初在视图上时才会触发。当您松开手指时,它会检查手指是否仍在视图中。
myView.setOnTouchListener { view, motionEvent ->
if (motionEvent.up && !motionEvent.isIn(view)) {
// Talk your action here
}
false
}
【讨论】:
前 2 个答案很好,除非视图位于滚动视图内:当由于您移动手指而发生滚动时,它仍被注册为触摸事件,而不是 MotionEvent.ACTION_MOVE 事件。因此,要改进答案(仅当您的视图位于滚动元素内时才需要):
private Rect rect; // Variable rect to hold the bounds of the view
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
// Construct a rect of the view's bounds
rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
} else if(rect != null && !rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())){
// User moved outside bounds
}
return false;
}
我在 Android 4.3 和 Android 4.4 上对此进行了测试
我没有注意到莫里茨的回答与前 2 名之间有任何区别,但这也适用于他的回答:
private Rect rect; // Variable rect to hold the bounds of the view
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
// Construct a rect of the view's bounds
rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
} else if (rect != null){
v.getHitRect(rect);
if(rect.contains(
Math.round(v.getX() + event.getX()),
Math.round(v.getY() + event.getY()))) {
// inside
} else {
// outside
}
}
return false;
}
【讨论】:
在我的情况下,Entreco 发布的答案需要稍作调整。我不得不替换:
if(!rect.contains((int)event.getX(), (int)event.getY()))
为
if(!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY()))
因为event.getX() 和event.getY() 只适用于ImageView 本身,而不适用于整个屏幕。
【讨论】:
view.setClickable(true);
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (!v.isPressed()) {
Log.e("onTouch", "Moved outside view!");
}
return false;
}
});
view.isPressed 使用view.pointInView 并包含一些触摸溢出。如果您不想草率,只需从内部 view.pointInView 复制逻辑(这是公开的,但隐藏,因此它不是官方 API 的一部分,可能随时消失)。
view.setClickable(true);
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
v.setTag(true);
} else {
boolean pointInView = event.getX() >= 0 && event.getY() >= 0
&& event.getX() < (getRight() - getLeft())
&& event.getY() < (getBottom() - getTop());
boolean eventInView = ((boolean) v.getTag()) && pointInView;
Log.e("onTouch", String.format("Dragging currently in view? %b", pointInView));
Log.e("onTouch", String.format("Dragging always in view? %b", eventInView));
v.setTag(eventInView);
}
return false;
}
});
【讨论】:
我在我的 OnTouch 中添加了一些日志记录,发现 MotionEvent.ACTION_CANCEL 被点击了。这对我来说已经足够了......
【讨论】:
View 包含在父 ViewGroup 中,您将收到 MotionEvent.ACTION_CANCEL 回调,例如 ScrollView,它有兴趣从其孩子那里窃取移动触摸,否则无法保证您'将在您的View 中获得MotionEvent.ACTION_CANCEL 回调。
这是一个View.OnTouchListener,您可以使用它来查看是否在用户将手指放在视图之外时发送了MotionEvent.ACTION_UP:
private OnTouchListener mOnTouchListener = new View.OnTouchListener() {
private Rect rect;
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v == null) return true;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
return true;
case MotionEvent.ACTION_UP:
if (rect != null
&& !rect.contains(v.getLeft() + (int) event.getX(),
v.getTop() + (int) event.getY())) {
// The motion event was outside of the view, handle this as a non-click event
return true;
}
// The view was clicked.
// TODO: do stuff
return true;
default:
return true;
}
}
};
【讨论】:
虽然@FrostRocket 的答案是正确的,但您也应该使用 view.getX() 和 Y 来解释翻译更改:
view.getHitRect(viewRect);
if(viewRect.contains(
Math.round(view.getX() + event.getX()),
Math.round(view.getY() + event.getY()))) {
// inside
} else {
// outside
}
【讨论】:
检查 MotionEvent.MOVE_OUTSIDE: 检查 MotionEvent.MOVE:
private Rect rect; // Variable rect to hold the bounds of the view
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
// Construct a rect of the view's bounds
rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
}
if(event.getAction() == MotionEvent.ACTION_MOVE){
if(!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())){
// User moved outside bounds
}
}
return false;
}
注意:如果您想以 Android 4.0 为目标,则会打开一个充满新可能性的世界: http://developer.android.com/reference/android/view/MotionEvent.html#ACTION_HOVER_ENTER
【讨论】:
ACTION_OUTSIDE,你需要使用getActionMasked()。或者您可以比较掩码:if ((event.getAction() & MotionEvent.ACTION_OUTSIDE) == MotionEvent.ACTION_OUTSIDE)。这是因为ACTION_OUTSIDE 在没有其他操作的情况下永远不会触发,所以它会被屏蔽。