按住按钮时在Android上重复执行操作

26

我希望在按住按钮时实现重复操作。例如:当用户点击并按住一个按钮时,它应该在固定时间间隔内反复调用类似的方法,直到用户松开按钮。


3
这是我的应用需求。我希望在用户按住按钮时不断移动光标。对于每次触摸,它都可以正常工作。我只想让动作在用户按住按钮时重复执行。 - user631854
2
@JoxTraex:请在回答中详细阐述您的观点,并看看它能得到多少赞。 - Nicolas Raoul
6个回答

79

有多种方法可以实现这一点,但比较直接的一种方法是在 Handler 上发布一个带有一定延迟的 Runnable。在其最基本的形式中,它看起来有些像这样:

Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new View.OnTouchListener() {

    private Handler mHandler;

    @Override public boolean onTouch(View v, MotionEvent event) {
        switch(event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if (mHandler != null) return true;
            mHandler = new Handler();
            mHandler.postDelayed(mAction, 500);
            break;
        case MotionEvent.ACTION_UP:
            if (mHandler == null) return true;
            mHandler.removeCallbacks(mAction);
            mHandler = null;
            break;
        }
        return false;
    }

    Runnable mAction = new Runnable() {
        @Override public void run() {
            System.out.println("Performing action...");
            mHandler.postDelayed(this, 500);
        }
    };

});

这个想法非常简单:在“down”触摸动作发生时,将包含重复动作的Runnable发布到Handler上。之后,在“up”触摸动作经过之前不再发布Runnable。只要“down”触摸动作仍在发生,Runnable将会一直发布自己到Handler上,直到被触摸的“up”动作移除 - 这就实现了“重复”的功能。
根据按钮的实际行为和所需的onclick/ontouch事件,您可能希望在没有延迟的情况下进行初始发布。

如果我想在用户按住按钮5秒钟时执行额外的操作,我该如何识别用户是否按住按钮5秒钟。 - user631854
1
向下投票(down vote)将在按钮区域之外继续触发处理程序。至少需要使用ACTION_CANCEL作为ACTION_UP的穿透效果。不幸的是,如果按钮位于ScrollView或另一个可滚动的小部件中,则此代码可能会使它看起来像触摸的小部件仍在被按住,尽管它可能已经超出屏幕或远离用户的手指。 - ian.shaun.thomas
3
@tencent:我觉得你误解了我的意思,当我说“在其最基本形式中…”时。上面的代码片段从来没有被用来作为生产代码,而是一个最简化的例子,用来展示如何解决手头问题的概念。如果你不想向下投票,修改一下代码可能会更有建设性和社区支持性。当然,这取决于你的选择。 - MH.
添加 @SuppressLint("ClickableViewAccessibility"),这对我起作用了。 - Brian M
1
@AbdelAleem 你可能想在 down 操作中将 postDelayed 替换为普通的 post(无延迟)。 - MH.
显示剩余4条评论

4

这是一种更为独立的实现方式,适用于任何视图,并支持触摸事件。

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

}

使用方法:

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

Original Answer


3

基于 Faisal Shaikh 的回答,提供与之兼容的 Kotlin 版本及示例:

package com.kenargo.compound_widgets

import android.os.Handler
import android.view.MotionEvent
import android.view.View
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 initialRepeatDelay.
 *
 * @param initialInterval The interval after first click event
 * @param initialRepeatDelay The interval after second and subsequent click events
 *
 * @param clickListener The OnClickListener, that will be called
 * periodically
 *
 * 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.
 *
 * Usage:
 *
 * someView.setOnTouchListener(new RepeatListener(400, 100, new OnClickListener() {
 *  @Override
 *  public void onClick(View view) {
 *      // the code to execute repeatedly
 *  }
 * }));
 *
 * Kotlin example:
 *  someView.setOnTouchListener(RepeatListener(defaultInitialTouchTime, defaultRepeatDelayTime, OnClickListener {
 *      // the code to execute repeatedly
 *  }))
 *
 */
class RepeatListener(
    initialInterval: Int,
    initialRepeatDelay: Int,
    clickListener: View.OnClickListener
) : OnTouchListener {

    private val handler = Handler()

    private var initialInterval: Int
    private var initialRepeatDelay: Int

    private var clickListener: View.OnClickListener
    private var touchedView: View? = null

    init {
        require(!(initialInterval < 0 || initialRepeatDelay < 0)) { "negative intervals not allowed" }

        this.initialInterval = initialRepeatDelay
        this.initialRepeatDelay = initialInterval

        this.clickListener = clickListener
    }

    private val handlerRunnable: Runnable = run {
        Runnable {
            if (touchedView!!.isEnabled) {

                handler.postDelayed(handlerRunnable, initialRepeatDelay.toLong())
                clickListener.onClick(touchedView)
            } else {

                // if the view was disabled by the clickListener, remove the callback
                handler.removeCallbacks(handlerRunnable)
                touchedView!!.isPressed = false
                touchedView = null
            }
        }
    }

    override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {

        when (motionEvent.action) {
            MotionEvent.ACTION_DOWN -> {
                handler.removeCallbacks(handlerRunnable)
                handler.postDelayed(handlerRunnable, initialRepeatDelay.toLong())
                touchedView = view
                touchedView!!.isPressed = true
                clickListener.onClick(view)
                return true
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                handler.removeCallbacks(handlerRunnable)
                touchedView!!.isPressed = false
                touchedView = null
                return true
            }
        }

        return false
    }
}

2

如果您使用普通视图点击,则可以采用另一种方法。在释放视图时,将调用单击侦听器。因此,我利用长按侦听器来执行第一部分。

button.setOnLongClickListener(new OnLongClickListener() {

            private Handler mHandler;

            @Override
            public boolean onLongClick(View view) {
                final Runnable mAction = new Runnable() {
                    @Override
                    public void run() {
                        //do something here 
                        mHandler.postDelayed(this, 1000);
                    }
                };

                mHandler = new Handler();
                mHandler.postDelayed(mAction, 0);

                button.setOnTouchListener(new View.OnTouchListener() {

                    @SuppressLint("ClickableViewAccessibility")
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        switch (event.getAction()) {
                            case MotionEvent.ACTION_CANCEL:
                            case MotionEvent.ACTION_MOVE:
                            case MotionEvent.ACTION_UP:
                                if (mHandler == null) return true;
                                mHandler.removeCallbacks(mAction);
                                mHandler = null;
                                button.setOnTouchListener(null);
                                return false;
                        }
                        return false;
                    }

                });


                return true;
            }
        });

1

虽然不是一个好主意,但可以通过在onKeyDown上启动计时器,在移动光标一步并重新启动计时器的间隔期间触发计时器来实现。然后,您可以在onKeyUp事件上取消计时器。在其他系统上,这种方法通常是在第一次按下键后移动,然后等待一段时间以确保用户肯定按住按钮...然后重复速度可以更快。想象一下键盘自动重复。这应该可以工作,并且不应对UI线程产生负面影响。


为什么你说这不是一个好主意? - Suragch

0
您可以为该视图注册一个 View.OnKeyListener。请注意最好对这些回调进行去抖动处理,否则 - 如果您的方法执行的操作稍微有点“重” - UI 将不会很流畅。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接