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 view

public 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;
}

Leave a Comment