Android – 按住重复动作
早上一切,
我会承认,我是刚开发的新手,并试图在Android上工作。 我一直在试图search'networkingfind如何实现一些“保持button重复操作”的build议 – 我已经创build了一个button自定义数字键盘,并希望像退格般的行为。 到目前为止,我曾经呼吁过一个没有编码过Android的朋友,但是做了很多C#/ Java,似乎知道他在做什么。
下面的代码工作得很好,但我觉得它可以做得更整齐。 我很抱歉,如果我错过了点点滴滴,但希望这解释了我的方法。 我认为onTouchListener是好的,但线程处理的方式感觉不对。
有没有更好或更简单的方法来做到这一点?
谢谢,
中号
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 }
这是更独立的实现,可用于支持触摸事件的任何视图
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. * * <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; } }
用法:
Button button = new Button(context); button.setOnTouchListener(new RepeatListener(400, 100, new OnClickListener() { @Override public void onClick(View view) { // the code to execute repeatedly } }));
这里有一个简单的叫做AutoRepeatButton的类,在许多情况下,它们可以用作标准Button类的embedded式替代品:
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(); } }
您的基本实施是健全的。 但是,我会将这个逻辑封装到另一个类中,以便您可以在其他地方使用它,而无需重复代码。 除了查找栏之外,请参阅例如“RepeatListener”类的实现,该类执行与要执行的操作相同的操作。
这是另外一个可选解决scheme ,但是和第一个非常相似。
Oliv的RepeatListenerClass非常好,但它不处理“MotionEvent.ACTION_CANCEL”,所以处理程序不会删除该动作中的callback。 这会在PagerAdapter中造成问题,等等。 所以我添加了这个事件案例。
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; }
卡尔的class是独立的,工作正常。
我会做初始延迟和重复间隔可configuration。 为此,
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(); }
那么你可以像这样使用这个类
<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" />
卡尔的class是相当不错的,这是修改,将允许加速(你持有更快的点击function执行的时间越长:
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(); } }
以下是基于Oliv's的一个答案,它有以下几个方面的调整:
- 不是直接调用
performClick
,而是直接调用onClick
,它调用performClick
或者执行performLongClick
。 这将触发标准的点击行为,如长时间点击的触觉反馈。 - 它可以configuration为立即触发
onClick
(如原始),或者仅在ACTION_UP
上ACTION_UP
并且只有在没有点击事件触发的情况下(更像是onClick
标准工作方式)。 - 替代的无参数构造函数将
immediateClick
设置为false,并使用系统标准的长按超时时间间隔。 对我来说,这感觉最像是一个标准的“重复长按”,如果它存在的话。
这里是:
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. * * <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; } }
卡尔的课对我很好。 但按下button并拖动时会出现一些问题。 在离开button区域的情况下,仍然继续点击事件。
请添加一个关于ACTION_MOVE的代码,如“ Android:检测用户是否触摸并拖出button区域? “