2010-11-26 8 views
44

Morgen alleAndroid - Halten Sie die Taste zum Wiederholen der Aktion

Ich werde sofort zugeben, dass ich neu in der Entwicklung bin und meine Hand bei Android versuchen. Ich habe versucht, das "Netz zu finden, um Rat zu finden, wie man etwas" Hold-Knopf-zu-Wiederholen-Aktion "implementiert - ich habe ein kundenspezifisches Numpad von Knöpfen geschaffen und möchte ein Backspace-ähnliches Verhalten. Nachdem ich so weit gekommen bin, habe ich einen Freund angerufen, der vorher noch kein Android programmiert hat, aber viel C#/Java gemacht hat und zu wissen scheint, was er macht.

Der untenstehende Code funktioniert gut, aber ich denke, es könnte besser erledigt werden. Ich entschuldige mich, wenn ich etwas verpasst habe, aber das erklärt hoffentlich meinen Ansatz. Ich denke, der onTouchListener ist in Ordnung, aber die Art, wie Threads behandelt werden, fühlt sich nicht richtig an.

Gibt es einen besseren oder einfacheren Weg, dies zu tun?

Danke,

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

Das ist nicht wirklich wie eine Frage aussieht. Der Code sieht o.k. obwohl. –

+0

Einverstanden, die Frage ist, ob es eine bessere, Android-spezifische Möglichkeit gibt, es zu tun. Es fühlt sich an wie viel Code, um etwas so Triviales zu erreichen. – Mark

Antwort

69

verwenden Diese unabhängige Implementierung ist, verwendbar mit jeder Ansicht, die Touch-Ereignis

unterstützt
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; 
    } 

} 

Verbrauch:

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

Ich mag diese Implementierung. Es gibt jedoch einen Fehler darin. Die onTouch-Funktion sollte zumindest im Fall ACTION_DOWN den Wert true zurückgeben, sonst werden keine anderen Berührungsereignisse erkannt (die ACTION_UP wird in diesem Fall niemals aufgerufen) – bbedward

+0

Dies ist eine wirklich gute Lösung. Aber stellen Sie sicher, ACTION_CANCEL wie in Sephirons Antwort zu implementieren. –

+0

Danke für Kommentare, ich habe es aktualisiert. – Oliv

7

Ihre grundlegende Implementierung ist Klang. Allerdings würde ich diese Logik in eine andere Klasse kapseln, so dass Sie sie an anderen Stellen verwenden können, ohne Code zu duplizieren. Siehe z.B. this Implementierung der "RepeatListener" -Klasse, die das gleiche tut, was Sie tun möchten, außer für eine Suchleiste.

Hier ist another thread with an alternative solution, aber es ist sehr ähnlich zu Ihrem ersten.

+0

Danke I82Much, ihr Code sieht aus, einen ähnlichen Job zu machen. Ich werde sehen, ob ich es an Bord nehmen kann :) – Mark

+0

Der Wiederholungshörer ist eine gute Option. Olivs Umsetzung ist vollständiger. –

14

Hier ist eine einfache Klasse namens AutoRepeatButton, die können, in vielen Fällen als Drop-in-Ersatz für die Standard-Button-Klasse verwendet werden:

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

Ich denke, das ist eine großartige Lösung. Ich habe das performClick() in performLongClick() geändert und performClick() in die ACTION_UP-Bedingung verschoben. Das einzige Problem, das ich habe, ist, dass meine Tasten jetzt nicht animieren. – SparkyNZ

4

Carl Klasse ist in sich geschlossen und funktioniert einwandfrei.

Ich würde initiale Verzögerung und Wiederholungsintervall konfigurierbar machen. so zu tun,

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

dann können Sie die Klasse wie diese

 <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 ist ziemlich gut, hier ist die Modifikation, die Beschleunigung erlauben werden (je länger Sie halten die schneller klicken Funktion ausgeführt wird:

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

Dies wäre noch besser gewesen, wenn es für alle Ansichten, nicht nur Buttonnon war. –

7

Oliv's RepeatListenerClass ist ziemlich Gut, aber "MotionEvent.ACTION_CANCEL" wird nicht behandelt, sodass der Handler den Rückruf in dieser Aktion nicht entfernt. Dies macht Probleme in PagerAdapter, und so weiter. Also habe ich diesen Ereignisfall hinzugefügt.

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

Hier ist eine Antwort basierend auf Oliv ist mit den folgenden Verbesserungen:

  • Anstatt einen Klick Hörer des Nehmens und ruft onClick direkt, ruft performClick oder performLongClick auf der Ansicht. Dies löst das Standard-Klickverhalten aus, wie haptisches Feedback beim langen Klick.
  • Es kann konfiguriert werden, entweder die onClick sofort (wie das Original) oder nur auf ACTION_UP und nur, wenn keine Klickereignisse ausgelöst haben (mehr wie Standard onClick funktioniert).
  • Alternativer No-Arg-Konstruktor, der immediateClick auf false setzt und für beide Intervalle den Systemstandard long press timeout verwendet. Für mich fühlt sich das am ähnlichsten an, was ein Standard "repeat long press" wäre, wenn er existiert.

Hier ist sie:

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

Mann, das ist die perfekte Antwort. Weil es mich Einzelklick mit * single action * ausführen lassen und lange klicken mit * repeat action * Danke +1 –

Verwandte Themen