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?
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.
Developing Accessible Apps for Blind and Visually-Impaired Users
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.
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
Accessibility overview
Build accessible custom views (especially the Handle custom touch events section)
Make apps more accessible
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.
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
Related
How would I create a dialog which does not consume touch events. Ie. I want to be able to interact with the underlying activity as normal.
public class LoadingDialog extends Dialog {
public LoadingDialog(Context context) {
super(context);
setContentView(R.layout.loading_dialog);
setCanceledOnTouchOutside(false);
getWindow().setBackgroundDrawable(new
ColorDrawable(android.graphics.Color.TRANSPARENT));
}
}
I tried the following with no success
#Override
public boolean dispatchTouchEvent(#NonNull MotionEvent ev) {
return false;
}
Thanks
Rather than use a dialogue, you can use a view that covers the whole screen with a tint and another view that looks like a dialogue on top of the tint and pass all touch events to the view below. Remove and show the dialogue by toggling the visibility of the view.
I am developing a custom UI on top of ExoPlayer, and I noticed that the controls (PlaybackControlView) hide when I touch the screen, not when I click.
I wanted to change to a click and checked how I can change the event listener, but so far could not find an easy solution. I checked the source SimpleExoPlayerView.java and I noticed that it actually is hardcoded:
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
return false;
}
if (controller.isVisible()) {
controller.hide();
} else {
maybeShowController(true);
}
return true;
}
So far I could think of two solutions. One is to change the ExoPlayer's source code, but I do not like it since I will have to make modifications every time I update the ExoPlayer.
The second solution I could think of is simply to try to handle it myself, for example, to add my own listeners, and show and hide the controls myself. I have not tried this yet, but it seems possible.
Is there another better solution, like overriding the listeners, etc?
Update: I am using custom UI by inflating exo_playback_control_view.xml
By looking at this answer you can see that an OnTouchListener#onTouch is called BEFORE the View#onTouchEvent so you can set an OnTouchListener to the view, consume the MotionEvent and it will not be passed onto the onTouchEvent method.
For example, using this code only "onTouch: LISTENER!!!" is logged when touching the view, and not "onTouchEvent: onTouchEvent!!!":
EDIT - to add your request for a click event handling I added the use of GestureDetector, using this answer - so now upon click "onSingleTapUp: TAP DETECTED" is logged as well.
public class TouchingView extends View {
private final static String TAG="TouchingView";
private OnTouchListener touchListener;
private GestureDetector gestureDetector;
public TouchingView(Context context) {
super(context);
touchListener = new TouchListener();
gestureDetector = new GestureDetector(getContext(),
(GestureDetector.OnGestureListener) touchListener);
setOnTouchListener(touchListener);
}
public TouchingView(Context context, AttributeSet attrs) {
super(context, attrs);
touchListener = new TouchListener();
gestureDetector = new GestureDetector(getContext(),
(GestureDetector.OnGestureListener) touchListener);
setOnTouchListener(touchListener);
}
public TouchingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
touchListener = new TouchListener();
gestureDetector = new GestureDetector(getContext(),
(GestureDetector.OnGestureListener) touchListener);
setOnTouchListener(touchListener);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: onTouchEvent!!!"); //not logged
return super.onTouchEvent(event);
}
private class TouchListener extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener{
#Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "onTouch: LISTENER!!!"); //logged upon every touch event. twice upon click (for UP and for DOWN)
gestureDetector.onTouchEvent(event);
return true; //preventing the view's onTouchEvent from firing
}
#Override
public boolean onSingleTapUp(MotionEvent e) { //you can override onSingleTapConfirmed if you don't want doubleClick to fire it
Log.d(TAG, "onSingleTapUp: TAP DETECTED"); //logged only upon click
return true;
}
}
}
I am facing same problem but i resolved this problem by flowing set controllerHideOnTouch property of exoplayerview
i used by following line of code
mExoPlayerView!!.controllerHideOnTouch=false
its working for me
To always show controls on exo player use hide_on_touch property:
<com.google.android.exoplayer2.ui.PlayerView
android:id="#+id/video_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:controller_layout_id="#layout/layout_exo_player"
app:show_timeout="0"
app:hide_on_touch="false"/>
I'm working on a game and ran into a bit of trouble, I might not be doing this correctly as I'm new to the graphic side of Android.
I have a SurfaceView and an ArrayList of my own Card object which extends View. I override the onDraw method for the Card object, and then in the SurfaceView's onDraw I draw everything. The drawing part works as it should.
I now try to detect when an individual card is touched using the onTouchListener, I set the listener for each card, but it detects the touch as if the view that being touched is the SurfaceView. It's possible that my whole way of thinking of this is wrong, so I'm asking for your advice.
Some code:
public GameSurfaceView(Context context) {
super(context);
GAME_STATE = GameState.LOADING;
this.context = context;
holder = getHolder();
holder.addCallback(new SurfaceHolder.Callback() {
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
Canvas c = holder.lockCanvas(null);
onDraw(c);
holder.unlockCanvasAndPost(c);
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
});
}
SurfaceView onDraw():
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRGB(30, 180, 30);
for (Card card : user.getTableCards()) {
card.draw(canvas);
}
}
Card's onDraw():
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(getBitmap(), x, y, null);
}
and onTouch
#Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TOUCH", "Class: " + v.getClass().toString());
if (v.getClass() == Card.class) {
Log.d("CARD", "Touched: " + ((Card) v).getValue());
}
return true;
}
I'm always getting the SurfaceView class being logged.
Is this even the right way of doing something like that? I need to detect touch on individual objects of the same class..
I currently do this by adding a member of type Rect to each card and then looping over and checking if the x and y of the touch are contained in that rect, but it seems like an expensive way, iterating over each card (even with max of 52) and checking it..
Thanks
You're mixing up two different concepts, I'll explain both in A) and B).
You have two options:
A) If you want to continue using SurfaceView (which might be the right choice for game, since it gives you more control over how things are drawn), your Card object should extend Drawable, not View. Drawable is a dumb class that you just draw to a canvas or pass to widgets as their background etc. Android doesn't need to know much more about it. In this case you will need to check what card was hit yourself, like you described. Android can not handle this for you. You're doing this with a View instead of Drawable, this is wrong! Or at least a huge waste. See below for how to do it properly with views.
B) Use views, which will handle touch events and many other things for you.
View is a complex class that is supposed to exist in android's view hierarchy. The way you're using it you're throwing away most of it's functionality - when you draw a view straight to a canvas android isn't really aware of the view at all, so it can't handle touch events.
A proper view-based implementation of your cards could look like this: Use a viewgroup as your base, this is what you set in setContentView(). This ViewGroup could just be a LinearLayout, or if you want the ability to set an arbitrary position and size on your cards of your cards, you could use AbsoluteView, although that's deprecated.
Your cards are then added to the ViewGroup using addView(). The positioning of your Card view within this is governed by a LayoutParams object that you pass to addView(). This way android is aware of it's existence, and will handle touch events like this:
The touch event first goes to your ViewGroup
The ViewGroup onTouchEvent will send the event down to down to it's children's onTouchEvent (which have been added via addView()). So you just override this method in your Card class. You inherently know which Card's onTouchEvent was called since it gets called on the object itself.
In your Card's onTouchEvent, you also let the parent know that you've handled the touch event by returning true. If you don't do this, the parent can handle the event instead.
To be perfectly clear: You were NOT adding the views to SurfaceView, in fact, you can't since it is a not a ViewGroup. It's just a complex View object that allows you to do arbitrary drawing operations within it. But it is always a leaf of the View hierarchy, and can only handle it's own onTouchEvents, it will not send it down to children because it can't have any.
your are drawing cards on a SurfaceView, it's like drawing image on a wall when you come to touch the image you're basically touching the wall, the image is now a part of the wall you can't separate them.
Here's an idea that may help:
you have a Card which extends View, why don't you just Use a RelativeLayout instead of your SurfaceView and ADD your cards to it (side by side maybe). This way you have a concrete view for each card that you can register an OnClickListener to each and handle your touch event...
I know how to do this within the activity class, however this doesn't fit my need. Within my class that extends View, once a method is called i would like the screen orientation to be locked. And then once another method is called i would like it to be unlocked.
Is there a way i can do this within my class that extends View?
Thanks.
#Override
public void onConfigurationChanged(Configuration newConfig) {
// TODO Auto-generated method stub
super.onConfigurationChanged(newConfig);
setContentView(this.gameView);
if (this.gameView.isOrientationChange() == false) {
// Stop the screen orientation changing during an event
switch (this.getResources().getConfiguration().orientation) {
case Configuration.ORIENTATION_PORTRAIT:
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
break;
case Configuration.ORIENTATION_LANDSCAPE:
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
break;
}
}
else {
// allow screen rotations
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
}
In the class extending View, there is an orientationChange boolean field. this is changed by method indicating whether the devices orientation can be changed. This worked but the screen size also changes when the orientation is changed. So if there is a lot of code dependent on the size and is timed, then this may cause more problems than solve.
i think what you are looking for, is configChanges see doc -> http://developer.android.com/reference/android/R.attr.html#configChanges
Maybe I've got a method, just write down that:
set layoutParams.screenOrientation value into: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED or ActivityInfo.SCREEN_ORIENTATION_LOCKED
Then, call addView or updateViewLayout to update this LayoutParams.
I have an application that is just a surfaceview. All i do is draw stuff on the surfaceview and whatnot. So one functionality i want is if the user touches a corner of the surfaceview it shows the keyboard and then they can type into it. Note that there are no EditTexts or Textboxes in my app anywhere. How do i call the keyboard to pop up and then how do i get all the keys that the user is pressing? I don't want the keys necessarily, i just want the string that they typed. How do i go about accomplishing this?
This is not a trivial task.
First of all, you'll need to override the method onCreateInputConnection()
#Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return new MyInputConnection(this, surfaceView, false);
}
Then you'll have to implement this input connection class, deriving from BaseInputConnection.
class MyInputConnection extends BaseInputConnection{
private MyActivity activity;
public MyInputConnection(MyActivity activity, View targetView, boolean fullEditor)
{
super( targetView, fullEditor );
mActivity = activity;
}
public boolean commitText(CharSequence text, int newCursorPosition){
myActivity.drawText((String) text);
return true;
}
There are more methods you'll want to override (see the reference), but start by focusing on commitText(). DrawText() is a method that renders the text on your surface, you'll have to come up with an implementation that suits your needs.