Custom view ... overrides onTouchEvent but not performClick
Asked Answered
C

4

67

I get this warning (from the question title) in a custom Android view I am developing.

Why do I get warned? What's the logic behind it i.e. why is it a good
practice to also override performClick when you override onTouchEvent?

Centner answered 13/12, 2014 at 18:54 Comment(3)
#24952812 , android-er.blogspot.fr/2014/09/…Preceptor
@shayanpourvatan I saw these links. But they are not about the same thing as my question.Centner
@Centner They are exactly the same thing. And they both have the same useless answers - there is nothing to handle and performClick() seems to do nothing useful. I decided now to just suppress the Lint warning for this.Acriflavine
S
105

What's the purpose?

In some of the other answers you can see ways to make the warning go away, but it is important to understand why the system wants you to override performClick() in the first place.

There are millions of blind people in the world. Maybe you don't normally think about them much, but you should. They use Android, too. "How?" you might ask. One important way is through the TalkBack app. It is a screen reader that gives audio feedback. You can turn it on in your phone by going to Settings > Accessibility > TalkBack. Go through the tutorial there. It is really interesting. Now try to use your app with your eyes closed. You'll probably find that your app is extremely annoying at best and completely broken at worst. That's a fail for you and a quick uninstall by anyone's who's visually impaired.

Watch this excellent video by Google for an introduction into making your app accessible.

How to override performClick()

Let's look at a example custom view to see how overriding performClick() actually works. We'll make a simple missile launching app. The custom view will be the button to fire it.

enter image description here

It sounds a lot better with TalkBack enabled, but animated gifs don't allow audio, so you will just have to try it yourself.

Code

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <net.example.customviewaccessibility.CustomView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:contentDescription="Activate missile launch"
        android:layout_centerInParent="true"
        />

</RelativeLayout>

Notice that I set the contentDescription. This allows TalkBack to read out what the custom view is when the user feels over it.

CustomView.java

public class CustomView extends View {

    private final static int NORMAL_COLOR = Color.BLUE;
    private final static int PRESSED_COLOR = Color.RED;

    public CustomView(Context context) {
        super(context);
        init();
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setBackgroundColor(NORMAL_COLOR);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                setBackgroundColor(PRESSED_COLOR);
                return true;

            case MotionEvent.ACTION_UP:
                setBackgroundColor(NORMAL_COLOR);

                // For this particular app we want the main work to happen
                // on ACTION_UP rather than ACTION_DOWN. So this is where
                // we will call performClick(). 
                performClick();
                return true;
        }
        return false;
    }

    // Because we call this from onTouchEvent, this code will be executed for both
    // normal touch events and for when the system calls this using Accessibility 
    @Override
    public boolean performClick() {
        super.performClick();

        launchMissile();

        return true;
    }

    private void launchMissile() {
        Toast.makeText(getContext(), "Missile launched", Toast.LENGTH_SHORT).show();
    }
}

Notes

  • The documentation also uses an mDownTouch variable which appears to be used to filter out extra touch up events, but since it isn't well explained or strictly necessary for our app, I left it out. If you make a real missile launcher app, I suggest you look more into this.
  • The primary method that launches the missile (launchMissile()) is just called from performClick(). Be careful not to call it twice if you also have it in onTouchEvent. You will need to decide exactly how and when to call your business logic method depending on the specifics of your custom view.
  • Don't override performClick() and then do nothing with it just to get rid of the warning. If you want to ignore the millions of blind people in the world, then you can suppress the warning. At least that way you are honest about your heartlessness.

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) { ... }
    

Further study

Sagacity answered 15/5, 2018 at 6:24 Comment(7)
but the warning also appears if you with: SwitchCompat.setOnTouchListener(this); and in this case I cannot override performClick, or how could I?Flagelliform
@David, I'm sorry, I don't have experience with that one. If you find the answer, though, please leave another comment.Sagacity
Reader questions: You said, "The primary method that launches the missile (launchMissile()) is just called from performClick(). Be careful not to call it twice if you also have it in onTouchEvent." I am not sure if I get your point here - Can you please help me understand what you meant by "not to call it twice if you also have it in onTouchEvent". Where else were you referring to? Also, you are calling performClick() only from inside onTouchEvent(), right? Or, did you mean to say, in onTouchEvent(), since you can get ACTION_UP and ACTION_DOWN, don't call it for both?Sagacity
@Reader, The system calls performClick() for accessibility events. In the code above I am calling performClick() for ACTION_UP events. That means its possible for both the system and me to call performClick(), thus resulting in two calls. I think recall coming across that situation when subclassing a button or something. Anyway, if you test your custom view you should be able to tell how many times performClick() is getting called.Sagacity
I love this answer, and I love the preaching. Thank you for playing your part in making the world more accessibleBathrobe
what if I am dong stuff with the X and Y coordinates from motionevent? PerformClick will not have access to this needed info, so it would be a useless overrideWarranty
Android Studio warns to override performClick() and call performClick() from onTouchEvent. You cannot not do either to remove the warning. However, as mentioned above, if you call performClick() in your onTouchEvent you will end-up having two entries in performClick(), one from your code and one from the system. That is completely useless, and I personally think the warning to override performClick() on your custom view is invalid. If the system is going to call it anyway, why call it as well.Agnesagnese
A
19

This warning tells you to override performClick

@Override
 public boolean performClick() {
  // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any
        super.performClick();

        // Handle the action for the custom click here

        return true;
 }

But it is not compulsory. As I have created a custom knobView and it is working quite good where I am also facing this warning.

Aliform answered 21/3, 2015 at 15:49 Comment(1)
The crux is the part where you only have the comment "Handle the action for the custom click here". What should I handle there? There's nothing useful to do I think. I don't want to write useless code just toshut up Lint.Acriflavine
B
4

The onTouchEvent is not called by some Accessibility services, as explained by clicking the "more..." link in the warning details.

It recommends that you override performClick for your desired action, or at least override it alongside your onTouchEvent.

If your code is more fitting for the touch event, you can use something similar to:

@Override
public boolean performClick() {
    if (actionNotAlreadyExecuted) {
        MotionEvent myEvent = MotionEvent.obtain(long downTime, long eventTime, int action, float x, float y, int metaState);
        onTouch(myView, myEvent);
    }
    return true; // register it has been handled
}

More information on accessing touch events through code is available at trigger ontouch event programmatically

Bonanno answered 7/4, 2018 at 2:59 Comment(0)
A
0

For those inheriting an ImageView or AppCompatImageView (dont know if it applies to other views as well) you should note that the system will also trigger performClick() even if you handling it inside your onTouchEvent() and returning true. This will cause performClick to be called from your code in onTouchEvent() as well from the system:

The only way to force it to work as normal is to forceably set the view to non-clickable and non-focuseable:

    setClickable(false);

So for reference:

@Override
public boolean onTouchEvent(final MotionEvent event) {
    super.onTouchEvent(event);

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:

            // To prevent getting double clicks 
            // we need to disable clickable
            setClickable(false);

            mPressed = true;
            invalidate();
            return true;
        case MotionEvent.ACTION_UP:
            if (mPressed) {
                invalidate();
                performClick();

                // Optionally return back to clickable
                setClickable(true);
            }
            return true;
    }


    return false;
}

@Override
public boolean performClick() {
    super.performClick();
    return true;
}
Agnesagnese answered 21/3 at 15:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.