如何在安卓中处理视图外的触摸事件?

14
我发现了 Gmail Android 应用程序中使用的 "Undo Bar" 实现。 "UndoBar" 基本上是在布局顶部显示的视图。
不幸的是,它并不完整 - 它没有触摸屏幕外部栏的解除功能。
我已经实现了一个 FrameLayout,覆盖了 onInterceptTouchEvent 来处理栏的解除,但是触摸操作栏没有任何反应。
有没有办法从操作栏处理这样的事件?
下面有一张展示 "UndoBar" 的图片。我想要实现的是处理由红点表示的操作栏上的触摸。

enter image description here


它没有通过触摸栏外屏幕来解除栏的功能。“栏”是指应用程序中的一个界面元素,通常位于屏幕顶部或底部,用于显示菜单、工具栏、状态栏等内容。 - Blackbelt
这只是一个视图,确切地说它是一个LinearLayout - pixel
我不明白。您想要更改它的可见性吗?您能添加相关的代码片段吗? - Blackbelt
不,我要处理视图外的触摸,特别是在操作栏中。我有一个带有操作栏的小视图和活动。在 Gmail 应用程序中,当您删除消息时,“UndoBar” 会出现,当您单击任何地方 - 操作栏、背景等时,UndoBar 就会消失。我希望拥有相同的行为。 - pixel
ListView 上实现一个滚动监听器,然后在滚动时调用 hideUndoBar(true) 即可。这不是字面上的触摸,但是最轻微的滚动也会移除撤销栏。 - Ali
8个回答

8
尝试重写您的活动的 dispatchTouchEvent。
dispatchTouchEvent(MotionEvent event){

     int x= event.getRawX();
     int y= event.getRawY();


         if(/*check bounds of your view*/){
          // set your views visiblity to gone or what you want. 
         }
      //for prevent consuming the event.
      return super().dispatchTouchEvent(event);    
}

更新

dispatchTouchEvent(MotionEvent event){

     int x= event.getRawX();
     int y= event.getRawY();

      return super().dispatchTouchEvent(event)||[YourView].onTouch(event);    
}

4
为了捕获整个屏幕的触摸事件(包括 ActionBar),需要向 Window 添加一个视图。
View overlayView = new View(this);
WindowManager.LayoutParams p = new WindowManager.LayoutParams();
p.gravity = Gravity.TOP;
p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
p.token = overlayView.getWindowToken();
overlayView.setOnTouchListener(new OnTouchListener() {
    public boolean onTouch(View view, MotionEvent event) {
        // Get the action bar
        int actionBarHeight = actionBar.getHeight();
        if ((event.getAction() == MotionEvent.ACTION_DOWN)
            && (event.getRawY() < actionBarHeight)) {
            // Touch inside the actionBar so let's consume it
            // Do something
            return true;
        }
        return false;
    }
});
WindowManager mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mWindowManager.addView(overlayView, p);

希望这可以帮到你。

如何从尚未附加的布局中获取windowToken?!overlayView - hasan

2

您只需要重写Activity的dispatchTouchEvent(ev: MotionEvent)方法。


看这个无限Snackbar消失的例子。

class MainActivity : AppCompatActivity() {

private var snackbar: Snackbar? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    setSupportActionBar(toolbar)

    // show indefinite snackbar
    snackbar = Snackbar.make(coordinator_layout, "Hello world!", Snackbar.LENGTH_INDEFINITE).apply {
        show()
    }
}

/**
 * On each touch event:
 * Check is [snackbar] present and displayed
 * and dismiss it if user touched anywhere outside it's bounds
 */
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    // dismiss shown snackbar if user tapped anywhere outside snackbar
    snackbar?.takeIf { it.isShown }?.run {
        val touchPoint = Point(Math.round(ev.rawX), Math.round(ev.rawY))
        if (!isPointInsideViewBounds(view, touchPoint)) {
            dismiss()
            snackbar = null // set snackbar to null to prevent this block being executed twice
        }
    }

    // call super
    return super.dispatchTouchEvent(ev)
}

/**
 * Defines bounds of displayed view and check is it contains [Point]
 * @param view View to define bounds
 * @param point Point to check inside bounds
 * @return `true` if view bounds contains point, `false` - otherwise
 */
private fun isPointInsideViewBounds(view: View, point: Point): Boolean = Rect().run {
    // get view rectangle
    view.getDrawingRect(this)

    // apply offset
    IntArray(2).also { locationOnScreen ->
        view.getLocationOnScreen(locationOnScreen)
        offset(locationOnScreen[0], locationOnScreen[1])
    }

    // check is rectangle contains point
    contains(point.x, point.y)
}
}        

或者查看完整的代码片段(gist)


我的情况完美解决。谢谢。 - Pablo R.

0

我对在此找到的任何答案都不满意。我的解决方案是将触摸监听器附加到父视图,如下所示:

((View)getParent()).setOnTouchListener((v, event) -> {
    return true;
});

如果您的父视图有任何子按钮或其他可点击元素,您的onTouchListener会触发吗? - Andrey Busik

0
你尝试过在你的FrameLayout上使用onTouchEvent吗?
我还没有尝试过,但我认为如果在该布局上发生MotionEvent,应该触发ACTION_OUTSIDE动作。
请尝试以下方法。
public boolean onTouchEvent(MotionEvent event)  
{  

       if(event.getAction() == MotionEvent.ACTION_OUTSIDE){  

             undoBar.setVisibility(View.GONE);
       }  
       return true;  
}  

现在我正在使用FrameLayout,但它无法处理ActionBar上的触摸操作。 - pixel
是的,这对我也很重要。在Gmail应用程序中,他们设法做到了这一点... - pixel

0
params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL|WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
                PixelFormat.TRANSPARENT);

你实际需要的是:

WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL|WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH

文档说明:

窗口标志:如果您已设置FLAG_NOT_TOUCH_MODAL,则可以设置此标志以接收一个特殊的MotionEvent,其中包含动作MotionEvent.ACTION_OUTSIDE,用于在窗口外发生的触摸事件。


这样做的缺点是触摸事件也会传递到背景视图。 - Lii

0

有两种可能的解决方案。

正如我在评论中提到的那样,您可以在您的ListView上实现一个滚动监听器,并简单地调用hideUndoBar(true)来轻微滚动。

您可以修改UndoBarController。您会注意到撤销栏只是一个View,在构造函数中将OnFocusChange监听器挂载到View上,并在show方法中将焦点设置为该视图。

在您的OnFocusChange中检查视图是否失去焦点并调用hideUndoBar(true)

更新

我在这里创建了一个Gist https://gist.github.com/atgheb/5551961,展示了如何更改UndoBarController以添加失去焦点时隐藏的功能。

我还没有测试它,但我不认为它会出问题。


抱歉,我错过了。你的操作栏是什么样子的?如果你可以在其中放置一个自定义布局,你可以在其中放置一个空的线性布局,并在空布局上设置一个单击事件来关闭undobar。类似于任何菜单项的onclick,隐藏撤销栏。 - Ali
我有一个大标题和两个图标。我想像 Gmail 应用程序中一样处理它们上的触摸事件。 - pixel
你可以为标题栏创建一个自定义布局,将所有需要的功能放入其中。您可以在其中放置标题和菜单项,这样您就可以将片段或活动注册为它们的单击侦听器。然后,在OnClick方法中,您首先要隐藏撤消栏,然后执行其他操作。或者,如果您正在使用SherlockActionBar,您可以修改该库以实现您想要的效果。 - Ali
你认为 Gmail 应用程序是这样做的吗?我认为有更好的方法来做这件事... - pixel
好的,撤销栏可以只是一个位于屏幕底部的自定义对话框。话虽如此,Undobar是由一位在谷歌工作的人编写的,所以谁知道他们是怎么做的呢?它甚至可以是一个透明的活动,当有人点击背景时就会完成它。 - Ali
我使用的例子来自Google的一个人,但他的实现方式远不够可用。它不能以对话框的形式实现,因为这将需要两个时钟。 - pixel

-2

您可以使用以下代码在您的Activity中获取所有触摸事件。我猜这段代码也会获取来自ActionBar的触摸事件。

public class MainActivity extends Activity {
...
// This example shows an Activity, but you would use the same approach if
// you were subclassing a View.
@Override
public boolean onTouchEvent(MotionEvent event){ 

    int action = MotionEventCompat.getActionMasked(event);

    switch(action) {
        case (MotionEvent.ACTION_DOWN) :
            Log.d(DEBUG_TAG,"Action was DOWN");
            return true;
        case (MotionEvent.ACTION_MOVE) :
            Log.d(DEBUG_TAG,"Action was MOVE");
            return true;
        case (MotionEvent.ACTION_UP) :
            Log.d(DEBUG_TAG,"Action was UP");
            return true;
        case (MotionEvent.ACTION_CANCEL) :
            Log.d(DEBUG_TAG,"Action was CANCEL");
            return true;
        case (MotionEvent.ACTION_OUTSIDE) :
            Log.d(DEBUG_TAG,"Movement occurred outside bounds " +
                    "of current screen element");
            return true;      
        default : 
            return super.onTouchEvent(event);
    }      
}

不是顶部视图消耗MotionEvents的事实 - pixel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接