2010-11-26 4 views
44

Matin tous,Android - Tenez le bouton pour répéter l'action

Je vais admettre tout de suite que je suis nouveau au développement et en essayant ma main sur Android. J'ai essayé de chercher sur le net pour trouver des conseils sur la façon d'implémenter certains "Hold Button pour répéter l'action" - J'ai créé un pavé numérique personnalisé à partir des boutons et je veux un comportement de retour arrière. Ayant eu jusqu'ici, j'ai fait appel à un ami qui n'a pas codé Android auparavant, mais qui a fait beaucoup de C#/Java et semble savoir ce qu'il fait.

Le code ci-dessous fonctionne très bien, mais je pense que cela pourrait être fait plus soigneusement. Je m'excuse si j'ai raté des morceaux, mais j'espère que cela explique mon approche. Je pense que le onTouchListener est correct, mais la façon dont les threads sont gérés ne se sent pas bien.

Existe-t-il une façon meilleure 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 
} 
+0

Cela ne ressemble pas vraiment à une question. Le code semble o.k. bien que. –

+0

D'accord, la question est de savoir s'il existe une meilleure façon spécifique pour Android de le faire. Il se sent comme beaucoup de code pour réaliser quelque chose d'aussi trivial. – Mark

Répondre

69

Ceci est la mise en œuvre plus indépendante, utilisable avec n'importe quelle vue prenant en charge l'événement tactile

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 Runnable handlerRunnable = new Runnable() { 
     @Override 
     public void run() { 
      handler.postDelayed(this, normalInterval); 
      clickListener.onClick(downView); 
     } 
    }; 

    private View downView; 

    /** 
    * @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); 
      downView = view; 
      downView.setPressed(true); 
      clickListener.onClick(view); 
      return true; 
     case MotionEvent.ACTION_UP: 
     case MotionEvent.ACTION_CANCEL: 
      handler.removeCallbacks(handlerRunnable); 
      downView.setPressed(false); 
      downView = null; 
      return true; 
     } 

     return false; 
    } 

} 

Utilisation:

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

J'aime cette implémentation. Il y a cependant une erreur. Le functin onTouch doit renvoyer true, au moins dans le cas ACTION_DOWN, sinon aucun autre événement tactile ne sera détecté (ACTION_UP dans ce cas ne sera jamais appelé) – bbedward

+0

Ceci est une très bonne solution. Mais assurez-vous de mettre en œuvre ACTION_CANCEL comme dans la réponse de sephiron aussi. –

+0

Merci pour les commentaires, je l'ai mis à jour. – Oliv

7

Votre implémentation de base est saine. Cependant, je voudrais encapsuler cette logique dans une autre classe afin que vous puissiez l'utiliser dans d'autres endroits sans dupliquer le code. Voir par exemple this implémentation de la classe "RepeatListener" qui fait la même chose que vous voulez faire, sauf pour une barre de recherche.

Voici another thread with an alternative solution, mais c'est très similaire au premier.

+0

Merci I82Much, leur code semble faire un travail similaire. Je vais voir si je peux le prendre à bord :) – Mark

+0

Le répéteur d'écoute est une excellente option. La mise en œuvre d'Oliv est plus complète. –

14

Voici une classe simple appelée AutoRepeatButton qui peut, dans bien des cas, être utilisé en remplacement de drop-in pour la classe standard Button:

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

Je pense que c'est une excellente solution. J'ai changé le performClick() pour effectuer LongClick() et déplacé performClick() dans la condition ACTION_UP. Le seul problème que j'ai, c'est que mes boutons ne s'animent pas maintenant. – SparkyNZ

4

classe de Carl est autonome et fonctionne très bien.

Je voudrais que le délai initial et l'intervalle de répétition soient configurables. 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(); 
} 

vous pouvez utiliser la classe comme ce

 <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" 
      /> 
2

Carl's class est assez bonne, voici une modification qui va permettre speedup (plus vous maintenez la fonction est exécutée plus rapidement cliquez sur:

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

Cela aurait été encore mieux si c'était pour toutes les vues, non seulement Buttton. –

7

Oliv's RepeatListenerClass est assez Bien, mais il ne gère pas "MotionEvent.ACTION_CANCEL", le gestionnaire ne supprime pas le rappel 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; 
} 
3

Voici une réponse basée sur de Oliv avec les réglages suivants:

  • Au lieu de prendre un écouteur de clic et appeler onClick directement, il appelle performClick ou performLongClick sur la vue. Cela déclenchera un comportement de clic standard, comme un retour haptique sur un clic long.
  • Il peut être configuré pour déclencher le onClick immédiatement (comme l'original), ou seulement sur ACTION_UP et seulement si aucun événement de clic n'a été déclenché (plus comme la norme onClick fonctionne).
  • Constructeur sans argument alternatif qui définit immediateClick sur false et utilise le standard système long press timeout pour les deux intervalles. Pour moi, cela ressemble le plus à ce que serait une «presse à répétition» standard, si elle existait.

Ici, il 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 (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; 
    } 

} 
+0

mec c'est la réponse parfaite. Parce qu'il me permet d'effectuer un seul clic avec * une seule action * et long clic avec * répéter l'action * Merci +1 –