繁体   English   中英

Android 通过单击外部使视图消失

[英]Android make view disappear by clicking outside of it

我有一些视图可以在按下按钮时显示。 如果我在这些视图之外单击,我希望它们消失。

这将如何在 Android 上完成?

此外,我意识到“后退按钮”还可以帮助 Android 用户使用此功能 - 我可能会将其用作关闭视图的辅助方式 - 但有些平板电脑甚至不再使用“物理”后退按钮了,它已经非常不重视了。

一个简单/愚蠢的方法:

  • 创建一个虚拟的空视图(假设 ImageView 没有源),使其填充父视图

  • 如果它被点击,然后做你想做的事。

您需要将 XML 文件中的根标签作为相对布局。 它将包含两个元素:您的虚拟视图(设置其 position 以align the Parent Top )。 另一个是包含视图和按钮的原始视图(此视图可能是 LinearLayout 或您制作的任何内容。不要忘记将其 position 设置为align the Parent Top

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

找到视图矩形,然后检测点击事件是否在视图之外。

@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);

这是一个老问题,但我想我会给出一个不基于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。

我一直在寻找一种在触摸外部时关闭视野的方法,但这些方法都不能很好地满足我的需求。 我确实找到了解决方案,如果有人感兴趣,我会在这里发布。

我有一个基本活动,几乎我所有的活动都在扩展。 在里面我有:

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

因此,如果我的视图可见,它就会关闭,如果不可见,它的行为就像正常的触摸事件一样。 不确定这是否是最好的方法,但它似乎对我有用。

基于王凯回答:我建议首先检查您的视图的可见性,根据我的场景,当用户点击 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);
}

我需要特定的能力,不仅可以在单击外部视图时删除视图,而且还需要允许单击正常传递到活动。 例如,我有一个单独的布局,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 步:通过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);
                        
                    }
                });

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

可能有某种方法可以使用onFocus()等 - 但我不知道。

我创建了自定义 ViewGroup 来显示锚定到另一个视图(弹出气球)的信息框。 Child view 是实际的信息框,BalloonView 是全屏的,用于绝对定位孩子,并拦截触摸。

public BalloonView(View anchor, View child) {
    super(anchor.getContext());
    //calculate popup position relative to anchor and do stuff
    init(...);
    //receive child via constructor, or inflate/create default one
    this.child = child;
    //this.child = inflate(...);
    //this.child = new SomeView(anchor.getContext());
    addView(child);
    //this way I don't need to create intermediate ViewGroup to hold my View
    //but it is fullscreen (good for dialogs and absolute positioning)
    //if you need relative positioning, see @iturki answer above 
    ((ViewGroup) anchor.getRootView()).addView(this);
}

private void dismiss() {
    ((ViewGroup) getParent()).removeView(this);
}

处理子项内部的点击:

child.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //write your code here to handle clicks inside
    }
});

要通过在外部单击而不将触摸委派给底层视图来关闭我的视图:

BalloonView.this.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        dismiss();
    }
});

要通过在外部单击将触摸委派给底层视图来关闭我的视图:

@Override
public boolean onTouchEvent(MotionEvent event) {
    dismiss();
    return false; //allows underlying View to handle touch
}

要在按下后退按钮时关闭:

//do this in constructor to be able to intercept key
setFocusableInTouchMode(true);
requestFocus();

@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        dismiss();
        return true;
    }
    return super.onKeyPreIme(keyCode, event);
}

我想分享我的解决方案,我认为它在以下情况下会很有用:

  • 您可以添加自定义 ViewGroup 作为根布局
  • 您想要消失的视图也可以是自定义视图。

首先,我们创建一个自定义的 ViewGroup 来拦截触摸事件:

class OutsideTouchDispatcherLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private val rect = Rect()

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (ev.action == MotionEvent.ACTION_DOWN) {
            val x = ev.x.roundToInt()
            val y = ev.y.roundToInt()
            traverse { view ->
                if (view is OutsideTouchInterceptor) {
                    view.getGlobalVisibleRect(rect)
                    val isOutside = rect.contains(x, y).not()
                    if (isOutside) {
                        view.interceptOutsideTouch(ev)
                    }
                }
            }
        }
        return false
    }

    interface OutsideTouchInterceptor {
        fun interceptOutsideTouch(ev: MotionEvent)
    }
}

fun ViewGroup.traverse(process: (View) -> Unit) {
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        process(child)
        if (child is ViewGroup) {
            child.traverse(process)
        }
    }
}

如您所见, OutsideTouchDispatcherLayout拦截触摸事件并通知每个实现OutsideTouchInterceptor的后代视图在该视图之外发生了某些触摸事件。

以下是后代视图如何处理此事件。 请注意,它必须实现OutsideTouchInterceptor接口:

class OutsideTouchInterceptorView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr),
    OutsideTouchDispatcherLayout.OutsideTouchInterceptor {

    override fun interceptOutsideTouch(ev: MotionEvent) {
        visibility = GONE
    }

}

然后,您只需通过子父关系轻松进行外部触摸检测:

<?xml version="1.0" encoding="utf-8"?>
<com.example.touchinterceptor.OutsideTouchDispatcherLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.example.touchinterceptor.OutsideTouchInterceptorView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#eee"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</com.example.touchinterceptor.OutsideTouchDispatcherLayout>

这是完成工作的简单方法:

第 1 步:为要为其生成点击外部事件的元素的外部容器创建一个 ID。

就我而言,它是一个线性布局,我将其 id 设为“outsideContainer”

第 2 步:为该外部容器设置一个 onTouchListener,它只会充当您内部元素的外部点击事件!

outsideContainer.setOnTouchListener(new View.OnTouchListener() {
                                        @Override
                                        public boolean onTouch(View v, MotionEvent event) {
                                            // perform your intended action for click outside here
                                            Toast.makeText(YourActivity.this, "Clicked outside!", Toast.LENGTH_SHORT).show();
                                            return false;
                                        }
                                    }
);

在视图外执行单击时隐藏视图:

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)
}

感谢@ituki 的想法

FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/search_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000"
android:clickable="true">

<LinearLayout
    android:clickable="true" // not trigger
    android:layout_width="match_parent"
    android:layout_height="300dp" 
    android:background="#FFF"
    android:orientation="vertical"
    android:padding="20dp">

    ...............

</LinearLayout>
</FrameLayout>

和 java 代码

mContainer = (View) view.findViewById(R.id.search_container);
    mContainer.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if(event.getAction() == MotionEvent.ACTION_DOWN){
                Log.d("aaaaa", "outsite");
                return true;
            }
            return false;
        }
    });

在 LinearLayout 之外触摸时有效

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM