Android点击视图外部使视图消失

42

当我按下按钮时,我会显示一些视图。如果我在这些视图之外点击,我希望它们消失。

在Android上,应该如何实现这个功能呢?

此外,我意识到“返回按钮”也可以帮助Android用户完成这个操作-我可能会将其用作关闭视图的辅助方法-但是一些平板电脑甚至不再使用“物理”返回按钮,因为它已经几乎不被强调了。

14个回答

42

一种简单/愚蠢的方法:

  • 创建一个虚拟的空视图(比如说没有源头的 ImageView),让它充满整个父布局。

  • 如果该视图被点击,那么就执行你想要做的事情。

你需要在你的 XML 文件中设置 RelativeLayout 为根标签。它包含两个元素:虚拟视图(将其位置设置为与父布局顶部对齐)。另一个是包含视图和按钮的原始视图(这个视图可以是 LinearLayout 或者其他类型的布局,不要忘记把它的位置设置为 与父布局顶部对齐)。

希望这对你有所帮助,祝你好运!


像魔法般地正常工作!谢谢 :) 我一开始有疑虑空白的 ImageView 是否会被识别。 - CQM
18
你可以简单地使用“View”作为虚拟视图。它们更轻巧,而且在XML文件中看到它会提醒你它不是用作可见元素。 - zienkikk
@RD。很高兴听到这个好消息 :) 。我曾经用RelativeLayout做过类似的技巧,它不仅是一种灵活的布局方式。 - iTurki
使虚拟视图也可点击,否则它将从其后面的视图中获取点击。 - Shahab Rauf

39
找到视图矩形,然后检测点击事件是否在视图范围之外。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Rect viewRect = new Rect();
    mTooltip.getGlobalVisibleRect(viewRect);
    if (!viewRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
        setVisibility(View.GONE);
    }
    return true;
}

如果你想在其他地方使用触摸事件,请尝试

return super.dispatchTouchEvent(ev);

3
请注意,dispatchTouchEvent() 应该返回一个布尔值,但在你的示例中没有返回任何内容。此外,你没有定义也没有解释什么是 viewRect - CaptJak
谢谢你,CaptJak。 - Kai Wang
3
mToolTip是什么? - ToMakPo
mToolTip 是可点击的视图,因此您正在比较事件是否在矩形范围之外。 - Shubham AgaRwal
可以这样改进:@Override public boolean dispatchTouchEvent(MotionEvent ev) { Rect viewRect = new Rect(); mTooltip.getGlobalVisibleRect(viewRect); if (!viewRect.contains((int) ev.getRawX(), (int) ev.getRawY())) { setVisibility(View.GONE); return true; } return super.dispatchTouchEvent(ev); } - SadeQ digitALLife

29

这是一个老问题,但我想给出一个不基于onTouch事件的答案。就像RedLeader所建议的那样,使用焦点事件也可以实现此功能。 我有一个案例需要在自定义弹出窗口中显示和隐藏一堆排列在同一ViewGroup中的按钮。您需要执行以下操作才能使其工作:

  1. 您希望隐藏的视图组需要设置View.setFocusableInTouchMode(true)。 这也可以使用android:focusableintouchmode在XML中设置。

  2. 您的视图根,即整个布局的根,可能是某种线性或相对布局,也需要能够作为#1上述进行聚焦

  3. 当显示视图组时,请调用View.requestFocus()来将其聚焦。

  4. 您的视图组需要覆盖View.onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)或实现自己的OnFocusChangeListener并使用View.setOnFocusChangeListener()

  5. 当用户点击您的视图之外时,焦点将转移到视图根(因为您已在#2中设置它为可聚焦)或直接转移到其他本质上可聚焦的视图(如EditText等)。

  6. 当您使用#4中的方法检测到焦点丢失时,您知道焦点已转移到视图组之外的某个位置,因此可以将其隐藏。

我猜这种解决方案并不能在所有场景下都起作用,但它适用于我的具体案例,并且听起来似乎也适用于OP。


1
小建议:setFocusableInTouchMode 确保 setFocusable 也被设置。 - Edison
你说得对,我没有仔细阅读文档。我会更新答案。 - britzl

17

我一直在寻找一种方法,当触摸屏幕外部时关闭我的视图,但是这些方法都不太适合我的需求。我找到了一个解决方案,在这里发布一下,以便有兴趣的人参考。

我有一个基础活动,几乎所有的活动都是继承自它。在这个基础活动中,我有:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (myViewIsVisible()){
            closeMyView();
        return true;
    }
    return super.dispatchTouchEvent(ev);
}

如果我的视图是可见的,它将会关闭;如果不可见,则会像普通的触摸事件一样处理。我不确定这是否是最好的方法,但这对我来说似乎可行。


10
当您的视图可见并单击它时会发生什么?看起来无论如何都会关闭它。 - Guillaume
1
非常有帮助。请参考以下链接,以确定触摸是否在视图内,当点击视图外部时该视图将消失:http://stackoverflow.com/questions/20700286/how-to-detect-a-motionevent-inside-dispatchtouchevent[link] - dazed
我同意@Guillaume的观点,这不是无用的吗?因为你永远无法点击你的视图。 - behelit

4

基于Kai Wang的建议:我建议首先检查您的视图的可见性。根据我的情况,当用户点击fab时,myView会变得可见,然后当用户再次点击外部时,myView会消失。

  @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Rect viewRect = new Rect();
    myView.getGlobalVisibleRect(viewRect);
    if (myView.getVisibility() == View.VISIBLE && !viewRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
        goneAnim(myView);
        return true;
    }
    return super.dispatchTouchEvent(ev);
}

什么是 goneAnim(myView)? - DragonFire
1
@DragonFire,如果您想将视图隐藏起来,也许您想用动画效果去实现它 :) - FxRi4
谢谢,使用myView.setVisibility(View.GONE)代替goneAnim(myView),因为后者是你的另一个函数。 - DragonFire

2
我需要特定的能力,不仅可以在点击视图之外时删除视图,还可以使点击正常地传递给活动。例如,我有一个单独的布局notification_bar.xml,在需要时需要动态填充并添加到当前活动中。
如果我创建一个覆盖整个屏幕的叠加视图来接收notification_bar视图之外的任何点击,并在点击时删除这两个视图,则父视图(活动的主视图)仍未接收到任何点击。这意味着当notification_bar可见时,需要点击两次按钮才能进行点击(一次用于关闭notification_bar视图,一次用于点击按钮)。
要解决此问题,您可以创建自己的DismissViewGroup,该视图组扩展了ViewGroup并覆盖了以下方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ViewParent parent = getParent();
    if(parent != null && parent instanceof ViewGroup) {
        ((ViewGroup) parent).removeView(this);
    }
    return super.onInterceptTouchEvent(ev);
}

然后你动态添加的视图将会像这样:
<com.example.DismissViewGroup android:id="@+id/touch_interceptor_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/transparent" ...
    <LinearLayout android:id="@+id/notification_bar_view" ...

这将允许您与视图进行交互,当您点击视图外部时,您既可以关闭视图,又可以正常地与活动进行交互。


1
包装器布局,当点击发生在给定视图之外时通知我们:
class OutsideClickConstraintLayout(context: Context, attrs: AttributeSet?) :
    ConstraintLayout(context, attrs) {

    private var viewOutsideClickListenerMap = mutableMapOf<View, () -> Unit>()

    fun setOnOutsideClickListenerForView(view: View, listener: () -> Unit) {
        viewOutsideClickListenerMap[view] = listener
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        viewOutsideClickListenerMap.forEach { (view, function) ->
            if (isMotionEventOutsideView(view, ev)) function.invoke()
        }
        return super.onInterceptTouchEvent(ev)
    }

    private fun isMotionEventOutsideView(view: View, motionEvent: MotionEvent): Boolean {
        val viewRectangle = Rect()
        view.getGlobalVisibleRect(viewRectangle)
        return !viewRectangle.contains(motionEvent.rawX.toInt(), motionEvent.rawY.toInt())
    }
}

使用方法:

....
        outsideClickContainerView.setOnOutsideClickListenerForView(someView) {
            // handle click outside someView
        }
....

1

实现onTouchListener()。检查触摸的坐标是否在视图的坐标之外。

可能有一些用onFocus()等方法实现的方式,但我不知道。


我如何动态确定视图的X、Y坐标?我使用的是像素还是我用来创建其大小的dip值? - CQM
https://dev59.com/E3E95IYBdhLWcg3wn_f2 - RedLeader

1
步骤1:使用Fragmelayout创建一个包装器视图,该视图将覆盖您的主布局。
 <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
     <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <!-- This is your main layout-->
     </RelativeLayout>
    
            <View
                android:id="@+id/v_overlay"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <!-- This is the wrapper layout-->
            </View>
        </FrameLayout>

步骤2:现在在您的Java代码中添加逻辑,如下所示 -

         View viewOverlay = findViewById(R.id.v_overlay);
         View childView = findViewByID(R.id.childView);
         Button button = findViewByID(R.id.button);
    
         viewOverlay.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        childView.setVisibility(View.GONE);
                        view.setVisibility(View.GONE);
                    }
                });
    
          
         button.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                       childView.setVisibility(View.VISIBLE);
   // Make the wrapper view visible now after making the child view visible for handling the 
  // main visibility task. 
                       viewOverlay.setVisibility(View.VISIBLE);
                        
                    }
                });

1
当单击视图之外时隐藏视图:
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
      if (isMenuVisible) {
          if (!isWithinViewBounds(ev.rawX.toInt(), ev.rawY.toInt())) {
               hideYourView()
               return true
          }
      }
   return super.dispatchTouchEvent(ev)
}

创建一个方法来获取视图的边界(高度和宽度),这样当你点击视图外部时,它会隐藏视图,而当你点击视图上时则不会隐藏。
private fun isWithinViewBounds(xPoint: Int, yPoint: Int): Boolean {
        val l = IntArray(2)
        llYourView.getLocationOnScreen(l)
        val x = l[0]
        val y = l[1]
        val w: Int = llYourView.width
        val h: Int = llYourView.height
        return !(xPoint < x || xPoint > x + w || yPoint < y || yPoint > y + h)
}

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