Android - Hold Button to Repeat Action
Asked Answered
P

12

68

I'll admit straight off that I'm new to development and trying my hand at Android. I've been trying to search the 'net to find advice on how to implement some "Hold Button to Repeat Action" - I've created a custom numpad from buttons and want a backspace-like behaviour. Having got so far, I called upon a friend who hasn't coded Android before, but done lots of C# / Java and seems to know what he's doing.

The code below works just fine, but I feel it could be done more neatly. I apologise if I've missed bits out, but hopefully this explains my approach. I think the onTouchListener is ok, but the way Threads are handled doesn't feel right.

Is there a better or more simple way to do this?

public class MyApp extends Activity {

private boolean deleteThreadRunning = false;
private boolean cancelDeleteThread = false;
private Handler handler = new Handler();
    
public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    
    //May have missed some declarations here...
        
    Button_Del.setOnTouchListener(new OnTouchListener() {
        public boolean onTouch(View v, MotionEvent event) {
            
           switch (event.getAction())
           {
               case MotionEvent.ACTION_DOWN:
               {
                   handleDeleteDown();
                   return true;
               }
               
               case MotionEvent.ACTION_UP:
               {
                   handleDeleteUp();
                   return true;
               }
               
               default:
                   return false;
           }
        }

        private void handleDeleteDown() {

            if (!deleteThreadRunning)
                startDeleteThread();
        }

        private void startDeleteThread() {

            Thread r = new Thread() {
                
                @Override
                public void run() {
                    try {
                        
                        deleteThreadRunning = true;
                        while (!cancelDeleteThread) {
                            
                            handler.post(new Runnable() {   
                                @Override
                                public void run() {
                                    deleteOneChar();
                                }
                            });
                            
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(
                                    "Could not wait between char delete.", e);
                            }
                        }
                    }
                    finally
                    {
                        deleteThreadRunning = false;
                        cancelDeleteThread = false;
                    }
                }
            };
            
            // actually start the delete char thread
            r.start();
        }
    });
}

private void handleDeleteUp() {
    cancelDeleteThread = true;
}

private void deleteOneChar()
{
    String result = getNumberInput().getText().toString();
    int Length = result.length();
    
    if (Length > 0)
        getNumberInput().setText(result.substring(0, Length-1));
        //I've not pasted getNumberInput(), but it gets the string I wish to delete chars from
}
Penicillin answered 26/11, 2010 at 9:58 Comment(2)
That does not really look like a question. The code looks o.k. though.Brio
Agreed, the question is whether there's a better, Android specific way to do it. It feels like a lot of code to acheive something so trivial.Penicillin
P
104

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
  }
}));
Palliate answered 9/10, 2012 at 8:6 Comment(7)
I like this implementation. There is one error in it however. The onTouch functin should return true, at least in the ACTION_DOWN case, otherwise no other touch events will be detected (the ACTION_UP in this case will never be called)Henkel
This is a really good solution. But make sure to implement ACTION_CANCEL as in sephiron's answer too.Witchery
To decouple even more you can use the normal View.setOnClickListener (or android:onClick), if you use pass (View view) -> view.performClick() in the RepeatListener constructor. As a bonus it'll fire an accessibility event (and the click sound) every time the repeat happens.Theona
Returning true actually call the original clicking method, so you should actually return true instead!Obsessive
@AlirezaNoorali onTouch already returns true for events it handles, for other events it returns false, which is correct IMO. Why do we need to return true for all events?Palliate
@Palliate why cann't we use LongClickListener?Carnahan
@SatishGhuge I believe that will deliver the event only once, not repeatedly at a certain rate.Palliate
S
15

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();
  }
}
Sultana answered 11/12, 2011 at 12:29 Comment(1)
I think this is a great solution. I changed the performClick() to performLongClick() and moved performClick() into the ACTION_UP condition. Only problem I have is that my buttons don't animate now.Doctrinaire
C
8

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;
}
Cestus answered 8/7, 2014 at 23:13 Comment(0)
E
7

Your basic implementation is sound. However, I would encapsulate that logic into another class so that you can use it in other places without duplicating code. See e.g. this implementation of "RepeatListener" class that does the same thing you want to do, except for a seek bar.

Here's another thread with an alternative solution, but it's very similar to your first one.

Ethbinium answered 26/11, 2010 at 13:41 Comment(2)
Thanks I82Much, their code looks to do a similar job. I'll see if I can take it on board :)Penicillin
The Repeat Listener is a great option. Oliv's implementation of it is more complete.Clipboard
S
5

Here's an answer based on Oliv's with the following tweaks:

  • Instead of taking a click listener and calling onClick directly, it calls performClick or performLongClick on the view. This will trigger standard clicking behavior, like haptic feedback on long click.
  • It can be configured to either fire the onClick immediately (like the original), or only on ACTION_UP and only if no click events have fired (more like how standard onClick works).
  • Alternate no-arg constructor that sets immediateClick to false and uses the system-standard long press timeout for both intervals. To me this feels the most like what a standard "repeat long press" would be, if it existed.

Here it is:

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 either calls performClick once, or performLongClick repeatedly on an interval.
 * The performClick can be fired either immediately or on ACTION_UP if no clicks have
 * fired.  The performLongClick is fired once after initialInterval and then repeatedly
 * after 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.
 *
 * Based on https://mcmap.net/q/280629/-android-hold-button-to-repeat-action
 */
public class RepeatListener implements OnTouchListener {

    private Handler handler = new Handler();

    private final boolean immediateClick;
    private final int initialInterval;
    private final int normalInterval;
    private boolean haveClicked;

    private Runnable handlerRunnable = new Runnable() {
        @Override
        public void run() {
            haveClicked = true;
            handler.postDelayed(this, normalInterval);
            downView.performLongClick();
        }
    };

    private View downView;

    /**
     * @param immediateClick Whether to call onClick immediately, or only on ACTION_UP
     * @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(
        boolean immediateClick,
        int initialInterval,
        int normalInterval)
    {
        if (initialInterval < 0 || normalInterval < 0)
            throw new IllegalArgumentException("negative interval");

        this.immediateClick = immediateClick;
        this.initialInterval = initialInterval;
        this.normalInterval = normalInterval;
    }

    /**
     * Constructs a repeat-listener with the system standard long press time
     * for both intervals, and no immediate click.
     */
    public RepeatListener()
    {
        immediateClick = false;
        initialInterval = android.view.ViewConfiguration.getLongPressTimeout();
        normalInterval = initialInterval;
    }

    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
        case MotionEvent.ACTION_DOWN:
            handler.removeCallbacks(handlerRunnable);
            handler.postDelayed(handlerRunnable, initialInterval);
            downView = view;
            if (immediateClick)
                downView.performClick();
            haveClicked = immediateClick;
            return true;
        case MotionEvent.ACTION_UP:
            // If we haven't clicked yet, click now
            if (!haveClicked)
                downView.performClick();
            // Fall through
        case MotionEvent.ACTION_CANCEL:
            handler.removeCallbacks(handlerRunnable);
            downView = null;
            return true;
        }

        return false;
    }

}
Stomy answered 10/7, 2015 at 2:1 Comment(1)
dude this is the perfect answer. Because it let me perform single Click with single action and long click with repeat action Thank you +1Riva
S
3

Carl's class is self-contained and works fine.

I would make initial delay and repeat interval configurable. To do so,

attrs.xml

<resources>
<declare-styleable name="AutoRepeatButton">
    <attr name="initial_delay"  format="integer" />
    <attr name="repeat_interval"  format="integer" />
</declare-styleable>
</resources>

AutoRepeatButton.java

    public AutoRepeatButton(Context context, AttributeSet attrs) {
    super(context, attrs);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoRepeatButton);
    int n = a.getIndexCount();
    for (int i = 0; i < n; i++) {
        int attr = a.getIndex(i);

        switch (attr) {
        case R.styleable.AutoRepeatButton_initial_delay:
            initialRepeatDelay = a.getInt(attr, DEFAULT_INITIAL_DELAY);
            break;
        case R.styleable.AutoRepeatButton_repeat_interval:
            repeatIntervalInMilliseconds = a.getInt(attr, DEFAULT_REPEAT_INTERVAL);
            break;
        }
    }
    a.recycle();
    commonConstructorCode();
}

then you can use the class like this

        <com.thepath.AutoRepeatButton
            xmlns:repeat="http://schemas.android.com/apk/res/com.thepath"
            android:id="@+id/btn_delete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/selector_btn_delete"
            android:onClick="onBtnClick"
            android:layout_weight="1"
            android:layout_margin="2dp"

            repeat:initial_delay="1500"
            repeat:repeat_interval="150"
            />
Silvia answered 14/12, 2011 at 8:58 Comment(0)
B
3

Carl's class is pretty good, here is modification that'll allow speedup (the longer you hold the faster click function is executed:

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;

    // speedup
    private long repeatIntervalCurrent = repeatIntervalInMilliseconds;
    private long repeatIntervalStep = 2;
    private long repeatIntervalMin = 10;

    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, 
            // faster and faster until it reaches repeaterIntervalMin
            if (repeatIntervalCurrent > repeatIntervalMin)
                repeatIntervalCurrent = repeatIntervalCurrent - repeatIntervalStep;

            postDelayed(repeatClickWhileButtonHeldRunnable, repeatIntervalCurrent);
        }
    };

    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.
                    repeatIntervalCurrent = repeatIntervalInMilliseconds;
                    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();
    }
}
Bill answered 8/2, 2014 at 9:41 Comment(1)
This would have been even better if it was for all views, not only buttton.Nica
J
1

Oliv's and sephiron's answers are pretty good, but I wanted using repeat action with regular View.OnClickListener together, with move action handling.

View view = //take somewhere    
view.setOnClickListener(v -> { /*do domething*/ });
view.setOnTouchListener(new RepeatListener(/*almost the same as in Oliv's answer*/); 

//if you want to use touch listener without click listener,  
//make sure that view has setClickable(true)

So it can be done like this without conflicts:

import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

public class RepeatListener implements OnTouchListener {

    private final static int TOUCH_OFFSET = 20;

    private final Handler handler = new Handler(Looper.getMainLooper());

    private final int initialInterval;
    private final int normalInterval;
    private final Runnable startListener;
    private final Runnable actionListener;

    private final Rect touchHoldRect = new Rect();

    private View touchedView;
    private boolean calledAtLeastOnce = false;

    private final Runnable handlerRunnable = new Runnable() {
        @Override
        public void run() {
            if (touchedView.isEnabled()) {
                handler.postDelayed(this, normalInterval);
                actionListener.run();
                if (!calledAtLeastOnce && startListener != null) {
                    startListener.run();
                }
                calledAtLeastOnce = true;
            } else {
                handler.removeCallbacks(handlerRunnable);
                touchedView.setPressed(false);
                touchedView = null;
                calledAtLeastOnce = false;
            }
        }
    };

    public RepeatListener(int initialInterval,
                          int normalInterval,
                          Runnable startListener,
                          Runnable actionListener) {
        if (actionListener == null) {
            throw new IllegalArgumentException("null runnable");
        }
        if (initialInterval < 0 || normalInterval < 0) {
            throw new IllegalArgumentException("negative interval");
        }

        this.initialInterval = initialInterval;
        this.normalInterval = normalInterval;
        this.startListener = startListener;
        this.actionListener = actionListener;
    }

    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                handler.removeCallbacks(handlerRunnable);
                calledAtLeastOnce = false;
                handler.postDelayed(handlerRunnable, initialInterval);
                touchedView = view;
                touchHoldRect.set(view.getLeft() - TOUCH_OFFSET,
                        view.getTop() - TOUCH_OFFSET,
                        view.getRight() + TOUCH_OFFSET,
                        view.getBottom() + TOUCH_OFFSET);
                return false;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: {
                handler.removeCallbacks(handlerRunnable);
                if (calledAtLeastOnce) {
                    touchedView.setPressed(false);
                }
                touchedView = null;
                boolean processed = calledAtLeastOnce;

                calledAtLeastOnce = false;
                return processed;
            }
            case MotionEvent.ACTION_MOVE: {
                if (!touchHoldRect.contains(
                        view.getLeft() + (int) motionEvent.getX(),
                        view.getTop() + (int) motionEvent.getY())) {
                    handler.removeCallbacks(handlerRunnable);
                }
                break;
            }
        }
        return false;
    }
}
Joint answered 12/11, 2020 at 16:4 Comment(0)
J
1

Here is a Kotlin version that:

  1. combines the answers by benkc and nikib3ro (to speed-up the action on a continuous long click);

  2. prevents the runnable to run indefinitely if the view (e.g. button) is disabled during the process.

    import android.os.Handler
    import android.os.Looper
    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 either calls performClick once, or performLongClick repeatedly on an interval.
     * The performClick can be fired either immediately or on ACTION_UP if no clicks have
     * fired.  The performLongClick is fired once after repeatInterval.
     *
     * Continuous LongClick can be speed-up by setting intervalAcceleration
     *
     * Interval is scheduled after the onClick completes, so it has to run fast.
     * If it runs slow, it does not generate skipped onClicks.
     *
     * Based on:
     * https://mcmap.net/q/280629/-android-hold-button-to-repeat-action
     * https://mcmap.net/q/280629/-android-hold-button-to-repeat-action
     *
     * @param immediateClick Whether to call onClick immediately, or only on ACTION_UP
     * @param interval The interval after first click event
     * @param intervalAcceleration The amount of time reduced from interval to speed-up the action
     *
     */
    class RepeatListener(private val immediateClick: Boolean, private val interval: Int, private val intervalAcceleration: Int) : OnTouchListener {
    
        private var activeView: View? = null
        private val handler = Handler(Looper.getMainLooper())
        private var handlerRunnable: Runnable
        private var haveClicked = false
    
        private var repeatInterval: Int = interval
        private val repeatIntervalMin: Int = 100
    
        init {
            handlerRunnable = object : Runnable {
                override fun run() {
    
                    if(activeView!!.isEnabled) {
                        haveClicked = true
    
                        // Schedule the next repetitions of the click action,
                        // faster and faster until it reaches repeaterIntervalMin
                        if (repeatInterval > repeatIntervalMin) {
                            repeatInterval -= intervalAcceleration
                        }
    
                        handler.postDelayed(this, repeatInterval.toLong())
                        activeView!!.performLongClick()
    
                    }else{
                        clearHandler() //stop the loop if the view is disabled during the process
                    }
                }
            }
        }
    
        override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
            when (motionEvent.action) {
                MotionEvent.ACTION_DOWN -> {
                    handler.removeCallbacks(handlerRunnable)
                    handler.postDelayed(handlerRunnable, repeatInterval.toLong())
                    activeView = view
                    if (immediateClick) activeView!!.performClick()
                    haveClicked = immediateClick
                    return true
                }
                MotionEvent.ACTION_UP -> {
                    // If we haven't clicked yet, click now
                    if (!haveClicked) activeView!!.performClick()
                    clearHandler()
                    return true
                }
                MotionEvent.ACTION_CANCEL -> {
                    clearHandler()
                    return true
                }
            }
            return false
        }
    
        private fun clearHandler(){
            handler.removeCallbacks(handlerRunnable)
            activeView = null
    
            //reset the interval to avoid starting with the sped up interval
            repeatInterval = interval
        }
    }
    
Jeannettajeannette answered 8/4, 2021 at 21:55 Comment(0)
S
1

It worked, but I found it needed some small tweaks. I added a counter. If the counter reaches 5,the interval speed up. If the counter reaches 15, it goes faster

--

import android.app.Activity;
import android.os.Bundle;
import android.content.Context;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.Gravity;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.LinearLayout;
import android.view.View;

import android.os.Handler;
import android.view.MotionEvent;
import android.widget.Toast;

public class MainActivity extends Activity {
int tt=0;

int interval =500;//how fast to repeat
int interval_base=500;
int repeat=0;//counted to switch speeds

Context context;

LinearLayout linearLayout;

TextView Info;

@Override
protected void onCreate(Bundle savedInstanceState) {
    
super.onCreate(savedInstanceState);
    
setContentView(R.layout.activity_main);
context = this;
linearLayout = findViewById(R.id.rootLayout);

Info = new TextView(this);
Info.setText("0");

TextView textView = new TextView(this);
    textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    
textView.setGravity(Gravity.CENTER);
    textView.setText("Click me");


textView.setOnTouchListener(new LongTouchIntervalListener(interval) {
    @Override
    public void onTouchInterval() {
        // do whatever you want
        repeat++;
        tt++;
        if(repeat==5){
            interval =250;
            Toast("Fast");
        }
        if(repeat==15){
            interval =125;
            Toast("Faster");
        }

        Info.setText(tt+"");
    }
 }); 

linearLayout.addView(textView);
linearLayout.addView(Info);
}


//Standard Toast message
public void Toast(String msg){ 
    Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show(); 
}

//create on touch long repeating 
public abstract class LongTouchIntervalListener implements View.OnTouchListener {

private final long touchIntervalMills;
private long touchTime;
private Handler handler = new Handler();

public LongTouchIntervalListener(final long touchIntervalMills) {
    if (touchIntervalMills <= 0) {
        throw new IllegalArgumentException("Touch interval must be more than zero");
    }
    this.touchIntervalMills = touchIntervalMills;
}

public abstract void onTouchInterval();

@Override
public boolean onTouch(final View v, final MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            onTouchInterval();
            touchTime = System.currentTimeMillis();

handler.postDelayed(touchInterval, 
touchIntervalMills);

            return true;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
interval=interval_base;//reset to base
repeat=0;//reset counter
touchTime = 0;
handler.removeCallbacks(touchInterval);
return true;
        default:
            break;
    }
    return false;
}

private final Runnable touchInterval = new Runnable() {
    @Override
    public void run() {
        onTouchInterval();
        if (touchTime > 0) {
handler.postDelayed(this, interval);
        }
    }
};
}

}
Saponaceous answered 11/4, 2023 at 19:15 Comment(0)
L
0

Carl's class is good for me. But it is some issue when press button and dragging. In case getting out of button area, still going on click event.

Please add a code about ACTION_MOVE like as ' Android: Detect if user touches and drags out of button region?'

Lithium answered 2/3, 2015 at 3:49 Comment(0)
A
0

Here is a little different solution without using of a nested click listener.

Usage:

 view.setOnTouchListener(new LongTouchIntervalListener(1000) {
        @Override
        public void onTouchInterval() {
            // do whatever you want
        }
 });

And the listener itself:

public abstract class LongTouchIntervalListener implements View.OnTouchListener {

    private final long touchIntervalMills;
    private long touchTime;
    private Handler handler = new Handler();

    public LongTouchIntervalListener(final long touchIntervalMills) {
        if (touchIntervalMills <= 0) {
            throw new IllegalArgumentException("Touch touch interval must be more than zero");
        }
        this.touchIntervalMills = touchIntervalMills;
    }

    public abstract void onTouchInterval();

    @Override
    public boolean onTouch(final View v, final MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                onTouchInterval();
                touchTime = System.currentTimeMillis();
                handler.postDelayed(touchInterval, touchIntervalMills);
                return true;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                touchTime = 0;
                handler.removeCallbacks(touchInterval);
                return true;
            default:
                break;
        }
        return false;
    }

    private final Runnable touchInterval = new Runnable() {
        @Override
        public void run() {
            onTouchInterval();
            if (touchTime > 0) {
                handler.postDelayed(this, touchIntervalMills);
            }
        }
    };
}
Annals answered 9/7, 2018 at 13:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.