Java – Learn why onClick() is called even after onDispatchTouchEvent() returns True

Learn why onClick() is called even after onDispatchTouchEvent() returns True… here is a solution to the problem.

Learn why onClick() is called even after onDispatchTouchEvent() returns True

For example, in an Android app, we want to be able to temporarily and reliably ignore all user touches at any time.

Based on my research on Stack Overflow and here, here, and here , The agreed solution seems to be this:

(MainActivity.java code):

// returning true should mean touches are ignored/blocked
@Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {

if (disableTouches) {
        return true;
    } else {
        return super.dispatchTouchEvent(pEvent);
    }

}

However, when we introduced Android Monkey Exerciser Tool and quickly sent touch events to the application, it became clear that pigs began to fly at the quantum level – We can call onClick() even after/during “blockTouches” that has been set to true.

My question is: why is that? — Is this normal Android behavior, or is my code wrong? 🙂

Note: I’ve ruled out the possibility that onClick() is called by user input instead of touch (and therefore not controlled by onDispatchTouchEvent() > method)… By adding “—-pct-touch 100” to the monkey command.

Here is the code I used for this test:

Main Activity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

View rootView;  turns black when "touch rejection" is in progress

View allowedButton;
    View notAllowedButton;

 Used to decide whether to process touch events.
     Set true temporarily when notAllowedButton is clicked.
    boolean touchRejectionAnimationInProgress = false;

int errorCount = 0;  counting "unexpected/impossible" click calls

@Override
    protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

rootView = findViewById(R.id.rootView);

allowedButton = findViewById(R.id.allowedButton);
        notAllowedButton = findViewById(R.id.notAllowedButton);

allowedButton.setOnClickListener(this);
        notAllowedButton.setOnClickListener(this);

allowedButton.setBackgroundColor(Color.GREEN);
        notAllowedButton.setBackgroundColor(Color.RED);

}

 returning true should mean touches are ignored/blocked
    @Override
    public boolean dispatchTouchEvent(MotionEvent pEvent) {

if (touchRejectionAnimationInProgress) {
            Log.i("XXX", "touch rejected in dispatchTouchevent()");
            return true;
        } else {
            return super.dispatchTouchEvent(pEvent);
        }

}

@Override
    public void onClick(View viewThatWasClicked){

Log.i("XXX", "onClick() called.  View clicked: " + viewThatWasClicked.getTag());

checking for unexpected/"impossible"(?) calls to this method
        if (touchRejectionAnimationInProgress) {
            Log.i("XXX!", "IMPOSSIBLE(?) call to onClick() detected.");
            errorCount ++;
            Log.i("XXX!", "Number of unexpected clicks: " + errorCount);
            return;
        } // else proceed...

if (viewThatWasClicked == allowedButton) {
             Irrelevant
        } else if (viewThatWasClicked == notAllowedButton) {
             user did something that is not allowed.
            touchRejectionAnimation();
        }

}

 When the user clicks on something "illegal,"
     all user input is ignored temporarily for 200 ms.
     (arbitrary choice of duration, but smaller is better for testing)
    private void touchRejectionAnimation() {

Log.i("XXX", "touchRejectionAnimation() called.");

touchRejectionAnimationInProgress = true;
        rootView.setBackgroundColor(Color.BLACK);

 for logging/debugging purposes...
        final String rejectionID = (new Random().nextInt() % 9999999) + "";
        Log.i("XXX", "rejection : " + rejectionID + " started.");

Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try { Thread.sleep(200); } catch (Exception e) {
                    Log.e("XXX", "exception in touchRejection() BG thread!");
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Log.i("XXX", "rejection " + rejectionID + " ending");
                        rootView.setBackgroundColor(Color.WHITE);
                        touchRejectionAnimationInProgress = false;
                    }
                });
            }
        });

thread.start();

}

}

Layout .xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rootView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=". MainActivity">

<View
        android:id="@+id/allowedButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="32dp"
        android:layout_marginBottom="32dp"
        android:tag="allowedButton"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/notAllowedButton"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

<View
        android:id="@+id/notAllowedButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="32dp"
        android:layout_marginBottom="32dp"
        android:tag="view2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/allowedButton"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Solution

If you don’t want your onClick() to be triggered when any View is clicked.

Here are the steps to be aware of.

Create custom viewGroup eg: MyConstraintLayout and add all child
inside it.

Override onInterceptTouchEvent(MotionEvent ev) and return it has true.

public class MyConstraintLayout extends ConstraintLayout {

private boolean mIsViewsTouchable;

public ParentView(Context context) {
        super(context);
    }

public ParentView(Context context, AttributeSet attrs) {
        super(context, attrs);
        inflate(context, R.layout.custom_view, this);
    }

public ParentView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

public void setViewsTouchable(boolean isViewTouchable) {
        mIsViewsTouchable = isViewTouchable;
    }

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mIsViewsTouchable;
    }
 }

Note: Using the setViewsTouchable() method according to your requirements, if you pass the parameter to true, all views are not clickable, if false, your View will be clickable.

Related Problems and Solutions