Understanding why onClick() is Called Even After onDispatchTouchEvent() Returns True

Viewed 274

Lets say, in an Android app, we want to have the ability to temporarily and reliably ignore all user touches at any moment.

From the research I have done on stack-overflow as well as here, here, and here, the agreed-upon solution seems to be something like this:

(Code of MainActivity.java):

// 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 introduce the Android Monkey Exerciser Tool and send touch events to the app at a rapid rate, it becomes apparent that pigs begin to fly at the quantum level -- we can get calls to onClick() even after/during times where "blockTouches" has been set to true.

MY QUESTION IS: Why is that? -- Is this a normal Android behavior, or did I make a mistake in my code? :)

Note: I have already ruled out the possibility of onClick() being called by user input other than touches (and therefore being uncontrolled by the onDispatchTouchEvent() method)... by adding "—-pct-touch 100" to the monkey command.

Here is the code I am using for this test:

MainActivity:

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

If you don't want your onClick() to be triggered on any view click.

Following are the steps which need to take care.

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: Use setViewsTouchable() method as per your requirement, If you pass the parameter as true all views are not clickable if false your views will be clickable.

Related