Android - Hold Button to Repeat Action
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 }}));
Here is a simple class called AutoRepeatButton which can, in many instances, be used as a drop-in replacement for the standard Button class:
package com.yourdomain.yourlibrary;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.Button;public class AutoRepeatButton extends Button { private long initialRepeatDelay = 500; private long repeatIntervalInMilliseconds = 100; private Runnable repeatClickWhileButtonHeldRunnable = new Runnable() { @Override public void run() { //Perform the present repetition of the click action provided by the user // in setOnClickListener(). performClick(); //Schedule the next repetitions of the click action, using a faster repeat // interval than the initial repeat delay interval. postDelayed(repeatClickWhileButtonHeldRunnable, repeatIntervalInMilliseconds); } }; private void commonConstructorCode() { this.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); if(action == MotionEvent.ACTION_DOWN) { //Just to be sure that we removed all callbacks, // which should have occurred in the ACTION_UP removeCallbacks(repeatClickWhileButtonHeldRunnable); //Perform the default click action. performClick(); //Schedule the start of repetitions after a one half second delay. postDelayed(repeatClickWhileButtonHeldRunnable, initialRepeatDelay); } else if(action == MotionEvent.ACTION_UP) { //Cancel any repetition in progress. removeCallbacks(repeatClickWhileButtonHeldRunnable); } //Returning true here prevents performClick() from getting called // in the usual manner, which would be redundant, given that we are // already calling it above. return true; } }); } public AutoRepeatButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); commonConstructorCode(); } public AutoRepeatButton(Context context, AttributeSet attrs) { super(context, attrs); commonConstructorCode(); } public AutoRepeatButton(Context context) { super(context); commonConstructorCode(); }}
Oliv's RepeatListenerClass is pretty good, but it does not handle "MotionEvent.ACTION_CANCEL", so handler does not remove call back in that action. This makes problems in PagerAdapter, and so on. So I added that event case.
private Rect rect; // Variable rect to hold the bounds of the viewpublic boolean onTouch(View view, MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: handler.removeCallbacks(handlerRunnable); handler.postDelayed(handlerRunnable, initialInterval); downView = view; rect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); clickListener.onClick(view); break; case MotionEvent.ACTION_UP: handler.removeCallbacks(handlerRunnable); downView = null; break; case MotionEvent.ACTION_MOVE: if (!rect.contains(view.getLeft() + (int) motionEvent.getX(), view.getTop() + (int) motionEvent.getY())) { // User moved outside bounds handler.removeCallbacks(handlerRunnable); downView = null; Log.d(TAG, "ACTION_MOVE...OUTSIDE"); } break; case MotionEvent.ACTION_CANCEL: handler.removeCallbacks(handlerRunnable); downView = null; break; } return false;}