【问题标题】:Android: Detect if user touches and drags out of button region?Android:检测用户是否触摸并拖出按钮区域?
【发布时间】:2011-09-18 14:33:48
【问题描述】:

在Android中,我们如何检测用户是否触摸按钮并拖出该按钮的区域?

【问题讨论】:

    标签: android button gestures


    【解决方案1】:

    我遇到了与 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()
            );
        }
    }
    

    【讨论】:

    • 这是一个很好的答案,尤其是对于处理 ACTION_CANCEL 未按应触发的问题的人。
    • 是的,这是一个很好的答案!
    【解决方案2】:

    可重复使用的 Kotlin 解决方案

    我从两个自定义扩展函数开始:

    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
    }
    

    【讨论】:

      【解决方案3】:

      前 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;
      }
      

      【讨论】:

      • 我刚刚在一个按钮上实现了这段代码,该按钮包含在 RecyclerView 的一个项目中,并且运行良好。我只是想说,如果你在getHitRect之前添加它,你可以摆脱ACTION_DOWN上的新Rect。 矩形矩形 = 新矩形(); getHitRect(rect);
      【解决方案4】:

      在我的情况下,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 本身,而不适用于整个屏幕。

      【讨论】:

      • +1 谢谢,这是更通用的方法,也适用于自定义视图实现
      【解决方案5】:
      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;
          }
      });
      

      【讨论】:

        【解决方案6】:

        我在我的 OnTouch 中添加了一些日志记录,发现 MotionEvent.ACTION_CANCEL 被点击了。这对我来说已经足够了......

        【讨论】:

        • 如果您的 View 包含在父 ViewGroup 中,您将收到 MotionEvent.ACTION_CANCEL 回调,例如 ScrollView,它有兴趣从其孩子那里窃取移动触摸,否则无法保证您'将在您的View 中获得MotionEvent.ACTION_CANCEL 回调。
        • 唯一对我有用的解决方案,谢谢救了我
        【解决方案7】:

        这是一个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;
                }
            }
        };
        

        【讨论】:

          【解决方案8】:

          虽然@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
           }
          

          【讨论】:

            【解决方案9】:

            检查 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_UP、ACTION_DOWN 和 ACTION_MOVE,但不能捕捉 ACTION_OUTSIDE。
            • 从 API 级别 3 开始提供:developer.android.com/reference/android/view/…
            • 根据这个答案 (stackoverflow.com/questions/6162497/…) 我相信我们只能使用 ACTION_OUTSIDE 来检测触摸是否在 Activity 之外,而不是在视图之外。
            • 查看 FrostRocket 对 rect.contains 测试的更新的答案,以使其适用于未定位在原点的单个视图。
            • 我相信如果你想获得ACTION_OUTSIDE,你需要使用getActionMasked()。或者您可以比较掩码:if ((event.getAction() &amp; MotionEvent.ACTION_OUTSIDE) == MotionEvent.ACTION_OUTSIDE)。这是因为ACTION_OUTSIDE 在没有其他操作的情况下永远不会触发,所以它会被屏蔽。
            猜你喜欢
            • 2012-04-18
            • 1970-01-01
            • 1970-01-01
            • 2012-03-04
            • 1970-01-01
            • 2022-10-14
            • 2011-08-03
            • 2016-01-12
            • 1970-01-01
            相关资源
            最近更新 更多