Android make view disappear by clicking outside of it

Viewed 45365

I have some views that I make visible upon a button press. I want them to disappear if I click outside of those views.

How would this be done on Android?

Also, I realize that the "back button" can also assist Android users with this - I might use that as a secondary way to close the views - but some of the tablets aren't even using a 'physical' back button anymore, it has been very de-emphasized.

14 Answers

base on Kai Wang answer : i suggest first check visibility of Your view , base on my scenario when user clicked on fab myView become visible and then when user click outside myView disappears

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

Step 1: Make a wrapper view by Fragmelayout which will cover your main layout.

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

Step 2: Now add logic in your java code like that -

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

I want to share my solution which I think it could be useful if :

  • you are able to add a custom ViewGroup as root layout
  • also the view which you want to disappear can be a custom one.

First, we create a custom ViewGroup to intercept touch events:

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

As you see, OutsideTouchDispatcherLayout intercepts touch events and informs each descendent view which implenets OutsideTouchInterceptor that some touch event occured outside of that view.

Here is how the descendent view could handle this event. Notice that it must implement OutsideTouchInterceptor interface:

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
    }

}

Then you have outside touch detection easily just by a child-parent relation:

<?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>

Here's a simple approach to get your work done:

Step 1: Create an ID for the outside container of your element for which you want to generate a click outside event.

In my case, it is a Linear Layout for which I've given id as 'outsideContainer'

Step 2: Set an onTouchListener for that outside container which will simply act as a click outside event for your inner elements!

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

To hide the view when click performs outside the view:

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
      if (isMenuVisible) {
          if (!isWithinViewBounds(ev.rawX.toInt(), ev.rawY.toInt())) {
               hideYourView()
               return true
          }
      }
   return super.dispatchTouchEvent(ev)
}

create a method to get the bounds(height & width) of your view, so when you click outside of your view it will hide the view and when click on the view will not hide:

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

Wrapper layout that notifies us when a click occurred outside a given view:

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

Usage:

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