26

I want to implement repeat action on pressing and holding a button. Example: When user click on a button and hold it,it should call a similar method again and again on a fixed interval until the user remove his finger from the button.

  • 3
    But its the need of my application. I want to move cursor constantly while user holds the button. It is working fine for every touch.I want just repeat action while user holds the button. –  May 09 '12 at 07:21
  • 2
    @JoxTraex: Please detail your point of view in an answer, and see how many upvotes it gets. – Nicolas Raoul Oct 28 '14 at 02:28

6 Answers6

79

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.

MH.
  • 45,303
  • 10
  • 103
  • 116
  • if i want to do something extra when user holds the button for let say 5 second,how can i recognize that user is holding the button for 5 seconds. –  May 09 '12 at 08:49
  • You could just push another delayed `Runnable` to the `Handler`. If it should be a one-off event, simply don't post it again from the `Runnable` itself. Also, don't forget to remove it in the 'up' action, in case the button is released before those 5 seconds have passed. – MH. May 09 '12 at 09:26
  • 1
    Down vote as lifts outside the button area will continue to fire the handler. ACTION_CANCEL is required at a minimum as a fall through on ACTION_UP. Unfortunately if the button is in a scrollview or another scrollable widget this code can make it seem that the touched widget is still being held even though it might be off screen or well away from the users finger. – ian.shaun.thomas Mar 04 '15 at 20:44
  • 3
    @tencent: I think you misunderstood when I said *"In its **most basic** form ... "*. The snippet above is and was never meant to be production-ready code, but rather a minimal example illustrating a concept on how to approach solving the problem at hand. Instead of downvoting, a simple edit would've been a more constructive and community-supportive course of action, I'd say. But then again, your choice of course. – MH. Mar 04 '15 at 21:26
  • I'd add MotionEvent.ACTION_MOVE to the second case: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_UP: If the user hold de button and move his finger to another position/widget, we need to stop the action as well. – Cícero Moura Nov 11 '18 at 13:30
  • Returning `true` actually call the original clicking method, so you should return `true` instead! – Alireza Noorali May 14 '19 at 06:55
  • Adding @SuppressLint("ClickableViewAccessibility"), made this work for me. – Brian M Nov 27 '21 at 22:34
  • Thank you @MH. Works nicely! Just a quick question: How do you remove the initial delay? – Abdel Aleem Apr 27 '22 at 18:38
  • 1
    @AbdelAleem You'll probably want to replace `postDelayed` in the down action with a plain `post` (without delay). – MH. May 09 '22 at 07:35
4

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

Faisal Shaikh
  • 3,900
  • 5
  • 40
  • 77
3

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
    }
}
Kenneth Argo
  • 1,697
  • 12
  • 19
2

Here is another approach in case you use the normal view click. If so, when you release the view, the click listener is called. So I take advantage of the long click listener to do the first part.

button.setOnLongClickListener(new OnLongClickListener() {

            private Handler mHandler;

            @Override
            public boolean onLongClick(View view) {
                final Runnable mAction = new Runnable() {
                    @Override
                    public void run() {
                        //do something here 
                        mHandler.postDelayed(this, 1000);
                    }
                };

                mHandler = new Handler();
                mHandler.postDelayed(mAction, 0);

                button.setOnTouchListener(new View.OnTouchListener() {

                    @SuppressLint("ClickableViewAccessibility")
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        switch (event.getAction()) {
                            case MotionEvent.ACTION_CANCEL:
                            case MotionEvent.ACTION_MOVE:
                            case MotionEvent.ACTION_UP:
                                if (mHandler == null) return true;
                                mHandler.removeCallbacks(mAction);
                                mHandler = null;
                                button.setOnTouchListener(null);
                                return false;
                        }
                        return false;
                    }

                });


                return true;
            }
        });
Cícero Moura
  • 2,027
  • 1
  • 24
  • 36
1

Although not a great idea. It could be accomplished by starting a timer on onKeyDown to fire at an interval during which you move the cursor one step and restart the timer. You could then cancel the timer on the onKeyUp event. The way this works on other systems is to typically to move on the first key down then wait a bit to ensure that the user is definitly holding the button... then the repeat can be a bit faster. Think of a keyboard auto repeating. This should work and should not affect the ui thread adversely.

cstrutton
  • 5,667
  • 3
  • 25
  • 32
0

You can register a View.OnKeyListener for that View. Please take into an account that it is better to debounce such callbacks, otherwise - in case your method doing something even slightly "heavy" - the UI won't be smooth.

avimak
  • 1,628
  • 11
  • 18