Android: mantenga presionado el botón para repetir la acción

Buenos días a todos,

De entrada, admitiré que soy nuevo en el desarrollo y estoy probando Android. He estado tratando de buscar en la red para encontrar consejos sobre cómo implementar algunos “Mantener pulsado para repetir la acción”: he creado un teclado numérico personalizado a partir de botones y quiero un comportamiento similar al de retroceso. Habiendo llegado tan lejos, llamé a un amigo que no había codificado Android antes, pero hice muchos C # / Java y parece saber lo que está haciendo.

El siguiente código funciona bien, pero creo que podría hacerse de forma más clara. Me disculpo si me he perdido fragmentos, pero espero que esto explique mi enfoque. Creo que el onTouchListener está bien, pero la forma en que se manejan los hilos no se siente bien.

¿Hay una manera mejor o más simple de hacer esto?

Gracias,

METRO

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 } 

Esta es una implementación más independiente, utilizable con cualquier Vista, que admita el evento táctil

 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 (eg 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. * * 

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

Uso:

 Button button = new Button(context); button.setOnTouchListener(new RepeatListener(400, 100, new OnClickListener() { @Override public void onClick(View view) { // the code to execute repeatedly } })); 

Aquí hay una clase simple llamada AutoRepeatButton que, en muchos casos, se puede usar como un reemplazo directo para la clase de botón estándar:

 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(); } } 

Tu implementación básica es sólida. Sin embargo, encapsularía esa lógica en otra clase para que pueda usarla en otros lugares sin duplicar el código. Véase, por ejemplo, esta implementación de la clase “RepeatListener” que hace lo mismo que desea hacer, excepto por una barra de búsqueda.

Aquí hay otro hilo con una solución alternativa , pero es muy similar a la primera.

El RepeatListenerClass de Oliv es bastante bueno, pero no maneja “MotionEvent.ACTION_CANCEL”, por lo que el manejador no elimina la callback en esa acción. Esto crea problemas en PagerAdapter , y así sucesivamente. Así que agregué ese caso de evento.

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

La clase de Carl es autónoma y funciona bien.

Haría que el retraso inicial y el intervalo de repetición sean configurables. Para hacerlo,

attrs.xml

       

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(); } 

entonces puedes usar la clase como esta

   

Aquí hay una respuesta basada en Oliv con los siguientes ajustes:

  • En lugar de tomar un oyente de clic y llamar a onClick directamente, llama a performClick o performLongClick performClick performLongClick en la vista. Esto activará el comportamiento de clics estándar, como los comentarios hápticos en clics largos.
  • Se puede configurar para activar el onClick inmediato (como el original), o solo en ACTION_UP y solo si no se han ACTION_UP eventos de clic (más parecido a cómo funciona el onClick estándar).
  • Constructor alterno no arg que establece immediateClick en falso y utiliza el tiempo de espera de impresión prolongado estándar del sistema para ambos intervalos. Para mí, esto se siente más como lo que sería una “repetición larga” estándar, si existiera.

Aquí está:

 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 (eg 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. * * 

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 http://stackoverflow.com/a/12795551/642160 */ 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; } }

La clase de Carl es bastante buena, aquí hay una modificación que permitirá la aceleración (cuanto más tiempo tenga, más rápido se ejecutará la función de clic:

 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(); } } 

La clase de Carl es buena para mí. Pero es un problema cuando presionas el botón y arrastras. En caso de salir del área de los botones, continúa el evento de clic.

Agregue un código sobre ACTION_MOVE como ‘ Android ‘ : ¿Detecta si el usuario toca y sale de la región de los botones? ‘