android repeat action on pressing and holding a button
There are multiple ways to accomplish this, but a pretty straightforward one would be to post a Runnable
on a Handler
with a certain delay. In it's most basic form, it will look somewhat like this:
Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new View.OnTouchListener() {
private Handler mHandler;
@Override public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mHandler != null) return true;
mHandler = new Handler();
mHandler.postDelayed(mAction, 500);
break;
case MotionEvent.ACTION_UP:
if (mHandler == null) return true;
mHandler.removeCallbacks(mAction);
mHandler = null;
break;
}
return false;
}
Runnable mAction = new Runnable() {
@Override public void run() {
System.out.println("Performing action...");
mHandler.postDelayed(this, 500);
}
};
});
The idea is pretty simple: post a Runnable
containing the repeated action on a Handler
when the 'down' touch action occurs. After that, don't post the Runnable
again until the 'up' touch action has passed. The Runnable
will keep posting itself to the Handler
(while the 'down' touch action is still happening), until it gets removed by the touch up action - that's what enables the 'repeating' aspect.
Depending on the actual behaviour of the button and its onclick/ontouch you're after, you might want to do the initial post without a delay.
This is more independent implementation, usable with any View, that supports touch event.
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
/**
* A class, that can be used as a TouchListener on any view (e.g. a Button).
* It cyclically runs a clickListener, emulating keyboard-like behaviour. First
* click is fired immediately, next one after the initialInterval, and subsequent
* ones after the normalInterval.
*
* <p>Interval is scheduled after the onClick completes, so it has to run fast.
* If it runs slow, it does not generate skipped onClicks. Can be rewritten to
* achieve this.
*/
public class RepeatListener implements OnTouchListener {
private Handler handler = new Handler();
private int initialInterval;
private final int normalInterval;
private final OnClickListener clickListener;
private View touchedView;
private Runnable handlerRunnable = new Runnable() {
@Override
public void run() {
if(touchedView.isEnabled()) {
handler.postDelayed(this, normalInterval);
clickListener.onClick(touchedView);
} else {
// if the view was disabled by the clickListener, remove the callback
handler.removeCallbacks(handlerRunnable);
touchedView.setPressed(false);
touchedView = null;
}
}
};
/**
* @param initialInterval The interval after first click event
* @param normalInterval The interval after second and subsequent click
* events
* @param clickListener The OnClickListener, that will be called
* periodically
*/
public RepeatListener(int initialInterval, int normalInterval,
OnClickListener clickListener) {
if (clickListener == null)
throw new IllegalArgumentException("null runnable");
if (initialInterval < 0 || normalInterval < 0)
throw new IllegalArgumentException("negative interval");
this.initialInterval = initialInterval;
this.normalInterval = normalInterval;
this.clickListener = clickListener;
}
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
handler.removeCallbacks(handlerRunnable);
handler.postDelayed(handlerRunnable, initialInterval);
touchedView = view;
touchedView.setPressed(true);
clickListener.onClick(view);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
handler.removeCallbacks(handlerRunnable);
touchedView.setPressed(false);
touchedView = null;
return true;
}
return false;
}
}
Usage:
Button button = new Button(context);
button.setOnTouchListener(new RepeatListener(400, 100, new OnClickListener() {
@Override
public void onClick(View view) {
// the code to execute repeatedly
}
}));
Original Answer
A compatible Kotlin version and example based on Faisal Shaikh answer:
package com.kenargo.compound_widgets
import android.os.Handler
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
/**
* A class, that can be used as a TouchListener on any view (e.g. a Button).
* It cyclically runs a clickListener, emulating keyboard-like behaviour. First
* click is fired immediately, next one after the initialInterval, and subsequent
* ones after the initialRepeatDelay.
*
* @param initialInterval The interval after first click event
* @param initialRepeatDelay The interval after second and subsequent click events
*
* @param clickListener The OnClickListener, that will be called
* periodically
*
* Interval is scheduled after the onClick completes, so it has to run fast.
* If it runs slow, it does not generate skipped onClicks. Can be rewritten to
* achieve this.
*
* Usage:
*
* someView.setOnTouchListener(new RepeatListener(400, 100, new OnClickListener() {
* @Override
* public void onClick(View view) {
* // the code to execute repeatedly
* }
* }));
*
* Kotlin example:
* someView.setOnTouchListener(RepeatListener(defaultInitialTouchTime, defaultRepeatDelayTime, OnClickListener {
* // the code to execute repeatedly
* }))
*
*/
class RepeatListener(
initialInterval: Int,
initialRepeatDelay: Int,
clickListener: View.OnClickListener
) : OnTouchListener {
private val handler = Handler()
private var initialInterval: Int
private var initialRepeatDelay: Int
private var clickListener: View.OnClickListener
private var touchedView: View? = null
init {
require(!(initialInterval < 0 || initialRepeatDelay < 0)) { "negative intervals not allowed" }
this.initialInterval = initialRepeatDelay
this.initialRepeatDelay = initialInterval
this.clickListener = clickListener
}
private val handlerRunnable: Runnable = run {
Runnable {
if (touchedView!!.isEnabled) {
handler.postDelayed(handlerRunnable, initialRepeatDelay.toLong())
clickListener.onClick(touchedView)
} else {
// if the view was disabled by the clickListener, remove the callback
handler.removeCallbacks(handlerRunnable)
touchedView!!.isPressed = false
touchedView = null
}
}
}
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
handler.removeCallbacks(handlerRunnable)
handler.postDelayed(handlerRunnable, initialRepeatDelay.toLong())
touchedView = view
touchedView!!.isPressed = true
clickListener.onClick(view)
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
handler.removeCallbacks(handlerRunnable)
touchedView!!.isPressed = false
touchedView = null
return true
}
}
return false
}
}