Bonjour à tous,
Je reconnais d'emblée que je suis novice en développement et que je m'essaye à Android. J'ai essayé de chercher sur le réseau pour trouver des conseils sur la façon de mettre en œuvre un "bouton de maintien pour répéter l'action" - j'ai créé un pavé numérique personnalisé à partir de boutons et je souhaite un comportement semblable à un retour arrière. À ce jour, j’ai fait appel à un ami qui n’a pas codé Android auparavant, mais qui a fait beaucoup de C #/Java et qui semble savoir ce qu’il fait.
Le code ci-dessous fonctionne très bien, mais j'estime que cela pourrait être fait de manière plus nette. Je m'excuse si j'ai raté quelques passages, mais j'espère que cela explique mon approche. Je pense que l’onTouchListener est bon, mais la façon dont les threads sont gérés ne se sent pas bien.
Y a-t-il un moyen meilleur ou plus simple de faire cela?
Merci,
M
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
}
Ceci est une implémentation plus indépendante, utilisable avec n'importe quelle vue, qui prend en charge les événements tactiles.
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
}
}));
Voici une classe simple appelée AutoRepeatButton qui peut, dans de nombreux cas, être utilisée comme remplacement immédiat de la classe Button standard:
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();
}
}
Votre implémentation de base est saine. Cependant, j'encapsulerais cette logique dans une autre classe afin que vous puissiez l'utiliser ailleurs sans dupliquer le code. Voir par exemple this implémentation de la classe "RepeatListener" qui fait la même chose que vous voulez, sauf une barre de recherche.
Voici un autre thread avec une solution alternative , mais il est très similaire à votre premier.
RepeatListenerClass d'Oliv est plutôt bon, mais ne gère pas "MotionEvent.ACTION_CANCEL", le gestionnaire ne supprime donc pas les rappels dans cette action. Cela crée des problèmes dans PagerAdapter , et ainsi de suite. J'ai donc ajouté ce cas d'événement.
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 classe de Carl est autonome et fonctionne bien.
Je voudrais rendre le retard initial et répétez intervalle configurable . Pour ce faire,
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();
}
alors vous pouvez utiliser la classe comme ça
<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"
/>
Voici une réponse basée sur celle d'Oliv avec les modifications suivantes:
onClick
, il appelle performClick
ou performLongClick
dans la vue. Cela déclenchera un comportement de clic standard, comme un retour haptique sur un clic long.onClick
immédiatement (comme l'original) ou uniquement sur ACTION_UP
et uniquement si aucun événement de clic n'a été déclenché (plus exactement comment fonctionne la variable onClick
standard).immediateClick
sur false et utilise la norme système long délai d'attente de la presse pour les deux intervalles. Pour moi, cela ressemble le plus à ce que serait une "pression longue répétée" standard, si elle existait.C'est ici:
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 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 classe de Carl est assez bonne, voici une modification qui permettra d'accélérer (plus vous maintenez la fonction de clic rapide exécutée:
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();
}
}
Voici une solution légèrement différente sans utiliser un écouteur de clic imbriqué.
Usage:
view.setOnTouchListener(new LongTouchIntervalListener(1000) {
@Override
public void onTouchInterval() {
// do whatever you want
}
});
Et l'auditeur lui-même:
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);
}
}
};
}
Le cours de Carl est bon pour moi . Mais il y a un problème lorsque vous appuyez sur un bouton et que vous le faites glisser . Si vous sortez de la zone des boutons, continuez de cliquer sur un événement.
S'il vous plaît ajouter un code sur ACTION_MOVE comme " Android: Détecter si l'utilisateur touche et traîne hors de la région du bouton? '