3

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!

Chandler
  • 35
  • 5

1 Answers1

4

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>();
}
lelloman
  • 13,883
  • 5
  • 63
  • 85
  • Do you know of a way to access the PopupWindow that is created? And maybe an onClickListener could be set for it? – Chandler Jun 27 '17 at 20:24
  • Or, I made a workaround where I have a boolean that tracks if the spinner drop down is open (sets to true when spinner is clicked, sets to false when an option is chosen), then I can check on the next touch if it's still set to open, meaning the spinner was closed by touching outside of the drop down. Can you think of a way to do this check before the user touches again? Is there some function that is called every frame I could add the check to? – Chandler Jun 27 '17 at 20:27
  • I found a way to get the Views of the top window and log all touch events from there. it requires to get your hands dirty :D – lelloman Jun 27 '17 at 20:39
  • It will not let me give you an upvote since I just joined, but thank you you are a lifesaver!!! – Chandler Jun 27 '17 at 21:38
  • Sorry I actually have one more question... it works great for touching outside the spinner, but when I touch a selection on the spinner it will also get called (I want only onItemSelectedListener to be called here). Is there a way to make your listener not listen for touches on the actual spinner options? – Chandler Jun 28 '17 at 13:54
  • it is probably doable, the question is, at what cost? if the only thing you want to detect is the touch outside the spinner list, the touch that closes the popup window, shouldn't `onNothingSelected()` of `AdapterView.OnItemSelectedListener` do the job? – lelloman Jun 28 '17 at 14:06
  • I am not sure if we're talking about the same thing. onNothingSelected() is not called when you touch outside the pop up window, that was my original issue, but you provided a way to write to the log when you touch outside the pop up window. Now I'm wondering if there is a way to make the onTouch listener you wrote (the one that logs "popupwindow event") only be called when anywhere _but_ the spinner options are selected. I'm not sure if it's possible, and sorry to keep asking all these questions, and thanks again – Chandler Jun 28 '17 at 15:03
  • at least, it doesn't look like onNothingSelected() calls... although it seems like it should. Here is what it looks like, `@Override public void onNothingSelected(AdapterView> parent) { Log.d(TAG, "on nothing selected called"); }` – Chandler Jun 28 '17 at 15:06
  • i see, so if i may give you 2 advices. instead of logging everything, use the debugger. and, if you look at the steps of my solution and understand all of them, you can jump into the source code of Spinner and (maybe with the help of the debugger) hack your way through this. if you really can't find a way get back to me in a few days – lelloman Jun 28 '17 at 15:21