Search code examples
androidspinnerontouchlistenertouch-event

How can I listen for a touch outside of an open spinner in Android?


I am doing a project that requires me to write to the log when different types of touches occur on screen. When I touch outside an open spinner drop down, it closes. I can't figure out how to detect this touch.

This code does not catch it, whereas it seems to catch all the other touches outside widgets:

mFullView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    touchCounter++;
                    Log.d(TAG, "Touch #" + touchCounter + ", no button touch registered.");
                }
                return false;
            }
        });

where mFullView is the parent RelativeLayout I have and is set like this:

mFullView = findViewById(R.id.full_view);

I also tried using onTouchEvent like this:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_DOWN) {
            Log.d(TAG, "screen was touched outside of open spinner dropdown");
        }
        return super.onTouchEvent(event);
    }

I have this code outside of onCreate and am not too confident about the placement or implementation.

I can't find anything about how to implement this, thanks for any help!


Solution

  • when clicked, the Spinner will show either a Dialog or a PopupWindow. neither of these will be attached to the same Window as your Activity so you won't be able to intercept touch events from there.

    maybe one could hack his way subclassing a Spinner

    I found a way to do this, it is very much hacky.

    1- we need to override public void onWindowFocusChanged(boolean hasFocus) this method will be called when the Activity's Window loses its focus because the PopupWindow's View had been attached to a new Window on top of the Activity's one

    2- get a list of all Windows root Views, this answer has a very dirty hacky method to do it

    3- one of these root Views will be a PopupDecorView, which is a private non-static class of PopupWindow. we need to get the instance of PopupWindow via reflection

    4- once we have the instance of PopupWindow, we need to get the OnTouchListener, wrap it around one of our own and set it back to the PopupWindow

    the overridden method looks like this:

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                for(View view : getWindowManagerViews()){
                    try {
                        Class clazz = view.getClass();
                        Field outerField = clazz.getDeclaredField("this$0");
                        outerField.setAccessible(true);
                        Object popupWindow = outerField.get(view);
    
                        Field field = popupWindow.getClass().getDeclaredField("mTouchInterceptor");
                        field.setAccessible(true);
                        final View.OnTouchListener innerOnTouchListener = (View.OnTouchListener) field.get(popupWindow);
                        View.OnTouchListener outerOnTOuchListener = new View.OnTouchListener() {
                            @Override
                            public boolean onTouch(View v, MotionEvent event) {
                                Log.d(MainActivity.class.getSimpleName(), String.format("popupwindow event %s at %s-%s", event.getAction(), event.getX(), event.getY()));
                                return innerOnTouchListener.onTouch(v, event);
                            }
                        };
                        field.set(popupWindow, outerOnTOuchListener);
                    }catch (Exception e){
                        //e.printStackTrace();
                    }
                }
            }
        });
    }
    

    where getWindowManagerViews() is taken from the aforementioned answer, and it looks like this

    public static List<View> getWindowManagerViews() {
        try {
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
                    Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
    
                // get the list from WindowManagerImpl.mViews
                Class wmiClass = Class.forName("android.view.WindowManagerImpl");
                Object wmiInstance = wmiClass.getMethod("getDefault").invoke(null);
    
                return viewsFromWM(wmiClass, wmiInstance);
    
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    
                // get the list from WindowManagerGlobal.mViews
                Class wmgClass = Class.forName("android.view.WindowManagerGlobal");
                Object wmgInstance = wmgClass.getMethod("getInstance").invoke(null);
    
                return viewsFromWM(wmgClass, wmgInstance);
            }
    
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        return new ArrayList<View>();
    }
    
    private static List<View> viewsFromWM(Class wmClass, Object wmInstance) throws Exception {
    
        Field viewsField = wmClass.getDeclaredField("mViews");
        viewsField.setAccessible(true);
        Object views = viewsField.get(wmInstance);
    
        if (views instanceof List) {
            return (List<View>) viewsField.get(wmInstance);
        } else if (views instanceof View[]) {
            return Arrays.asList((View[])viewsField.get(wmInstance));
        }
    
        return new ArrayList<View>();
    }