Can't handle both click and touch events simultaneously
I am trying to handle touch events and click events on a button. I do the following:
button.setOnClickListener(clickListener);
button.setOnTouchListener(touchListener);
开发者_如何学编程
When any one listener is registered things work fine but when I try to use them both only the touch events are fired. Any workaround? What am I doing wrong?
Its a little tricky.
If you set onTouchListener
you need to return true
in ACTION_DOWN
, to tell the system that I have consumed the event and it won't trickle down to other listeners.
But then OnClickListener
won't be fired.
So you might think, I will just do my thing there and return false
so I can receive clicks too.
If you do so, it will work, but you won't be subscribed to other upcoming touch events (ACTION_MOVE
, ACTION_UP
)
Therefore, the only option is to return true
there, but then you won't receive any click events as we said previously.
So you need to perform the click manually in the ACTION_UP
with view.performClick()
This will work.
There is a subtle, yet very important difference between the ClickListener
and the TouchListener
. The TouchListener
is executed before the view can respond to the event. The ClickListener
will receive its event only after the view has handled it.
So when you touch your screen, the TouchListener
is executed first and when you return true
for your event, the ClickListener
will never get it. But if you press the trackball of your device, the ClickListener
should be fired because the TouchListener
will not respond to it.
Thanks to @urSus for great answer
But in that case every touch will perform click, Even ACTION_MOVE
Assuming you want to separate move
event and click
event you can use a little trick
define a boolean
field and use like this:
@Override
public boolean onTouch(View view, MotionEvent motionEvent)
{
switch (motionEvent.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
shouldClick = true;
.
.
break;
case MotionEvent.ACTION_UP:
if (shouldClick)
view.performClick();
break;
case MotionEvent.ACTION_POINTER_DOWN:
break;
case MotionEvent.ACTION_POINTER_UP:
break;
case MotionEvent.ACTION_MOVE:
//Do your stuff
shouldClick = false;
break;
}
rootLayout.invalidate();
return true;
}
You should return false in your OnTouchListener
then your OnClickListener
will be also handled.
I suppose you're returning true
in your OnTouchListener
? That will consume the event so it won't be sent down for any further processing.
On a side note - what's the point of having both a click and touch listener?
This is an example of TouchListener that doesn't shadow ClickListener.
import android.graphics.PointF
import android.view.MotionEvent
import android.view.MotionEvent.*
import android.view.View
import kotlin.math.abs
object TouchAndClickListener : View.OnTouchListener {
/** Those are factors you can change as you prefer */
private const val touchMoveFactor = 10
private const val touchTimeFactor = 200
private var actionDownPoint = PointF(0f, 0f)
private var previousPoint = PointF(0f, 0f)
private var touchDownTime = 0L
override fun onTouch(v: View, event: MotionEvent) = when (event.action) {
ACTION_DOWN -> PointF(event.x, event.y).let {
actionDownPoint = it // Store it to compare position when ACTION_UP
previousPoint = it // Store it to compare position when ACTION_MOVE
touchDownTime = now() // Store it to compare time when ACTION_UP
/* Do other stuff related to ACTION_DOWN you may whant here */
true
}
ACTION_UP -> PointF(event.x, event.y).let {
val isTouchDuration = now() - touchDownTime < touchTimeFactor // short time should mean this is a click
val isTouchLength = abs(it.x - actionDownPoint.x) + abs(it.y - actionDownPoint.y) < touchMoveFactor // short length should mean this is a click
val shouldClick = isTouchLength && isTouchDuration // Check both
if (shouldClick) yourView.performClick() //Previously define setOnClickListener{ } on yourView, then performClick() will call it
/* Do other stuff related to ACTION_UP you may whant here */
true
}
ACTION_MOVE -> PointF(event.x, event.y).let {
/* Do other stuff related to ACTION_MOVE you may whant here */
previousPoint = it
true
}
else -> false // Nothing particular with other event
}
private fun now() = System.currentTimeMillis()
}
Thanks to @Nicolas Duponchel this is how i achieved both onClick and onTouch events
//Define these globally e.g in your MainActivity class
private short touchMoveFactor = 10;
private short touchTimeFactor = 200;
private PointF actionDownPoint = new PointF(0f, 0f);
private long touchDownTime = 0L;
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN: {
actionDownPoint.x = event.getX();
actionDownPoint.y = event.getY();
touchDownTime = System.currentTimeMillis();
break;
}
case MotionEvent.ACTION_UP: {
//on touch released, check if the finger was still close to the point he/she clicked
boolean isTouchLength = (Math.abs(event.getX() - actionDownPoint.x)+ Math.abs(event.getY() - actionDownPoint.y)) < touchMoveFactor;
boolean isClickTime = System.currentTimeMillis() - touchDownTime < touchTimeFactor;
//if it's been more than 200ms since user first touched and, the finger was close to the same place when released, consider it a click event
//Please note that this is a workaround :D
if (isTouchLength && isClickTime){
//call this method on the view, e.g ivPic.performClick();
performClick();}
break;
}
}
}
All above answer is said that we can not handle both setOnTouchListener
and setOnClickListener
.
However, I see we can handle both by return false
in setOnTouchListener
Example
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.button)
button.setOnClickListener {
Log.i("TAG", "onClick")
}
button.setOnTouchListener { v, event ->
Log.i("TAG", "onTouch " + event.action)
false
}
}
When I click at Button
, logcat will display like
I/TAG: onTouch 0
I/TAG: onTouch 1
I/TAG: onClick
button.setOnTouchListener(this);
Implement interface and the code here:
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (view.getId()) {
case R.id.send:
switch(motionEvent.getAction()){
case MotionEvent.ACTION_DOWN:
//when the user has pressed the button
//do the needful here
break;
case MotionEvent.ACTION_UP:
//when the user releases the button
//do the needful here
break;
}
break;
}
return false;
}
To make both events possible in gridview,only by making return of touch listener"false" as follows,this worked for me.
**GridView myView = findViewById(R.id.grid_view);
myView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
// ... Respond to touch events
return false;
}
});**
in this way both events can be achieved
## Exact working solution for both click action and touch listener(dragging) ##
private int initialX;
private int initialY;
private float initialTouchX;
private float initialTouchY;
private float CLICK_ACTION_THRESHOLD = 0.5f;
private float startX;
private float startY;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (view.getId()) {
case R.id.chat_head_profile_iv:
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//remember the initial position.
initialX = params.x;
initialY = params.y;
startX = event.getX();
startY = event.getY();
//get the touch location
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
return true;
case MotionEvent.ACTION_UP:
float endX = event.getX();
float endY = event.getY();
if (shouldClickActionWork(startX, endX, startY, endY)) {
openScreen();// WE HAVE A CLICK!!
}
return true;
case MotionEvent.ACTION_MOVE:
//Calculate the X and Y coordinates of the view.
params.x = initialX + (int) (event.getRawX() - initialTouchX);
params.y = initialY + (int) (event.getRawY() - initialTouchY);
//Update the layout with new X & Y coordinate
mWindowManager.updateViewLayout(mChatHeadView, params);
return true;
}
break;
}
return true;
}
private boolean shouldClickActionWork(float startX, float endX, float startY, float endY) {
float differenceX = Math.abs(startX - endX);
float differenceY = Math.abs(startY - endY);
if ((CLICK_ACTION_THRESHOLD > differenceX) && (CLICK_ACTION_THRESHOLD > differenceY))
return true;
else
return false;
}
In ACTION_UP
perform onClick
manually using condition
boolean shouldClick = event.eventTime - event.downTime <= 200 // compares the time with some threshold
So, try within MotionEvent.ACTION_UP
if(event.eventTime - event.downTime <= 200) { // case or when statement of action Touch listener
view.performClick();
}
I knows its too late, but if someone is struggling with this for a clean solution, here it is.
These are used for measuring the time between touching and removing the finger.
private long clickTime = 0;
public static final long CLICK_TIMEOUT = 200; // 200ms
This my onTouchListner
. Works like a charm
private final View.OnTouchListener onTouchListener = (v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
clickTime = System.currentTimeMillis();
return true;
} else if(event.getAction() == MotionEvent.ACTION_UP) {
if(System.currentTimeMillis()-clickTime < Constants.CLICK_TIMEOUT)
{
Toast.makeText(getContext(), "clicked", Toast.LENGTH_SHORT).show();
return true;
}
return false;
}
else if(event.getAction() == MotionEvent.ACTION_MOVE){
if(System.currentTimeMillis()-clickTime > Constants.CLICK_TIMEOUT)
{
ClipData data = ClipData.newPlainText("" , "");
View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v);
v.startDrag(data , shadowBuilder , v , 0);
return false;
}
return false;
}
return false;
};
精彩评论