implementing debounce in Java

前端 未结 8 590
心在旅途
心在旅途 2020-11-30 04:09

For some code I\'m writing I could use a nice general implementation of debounce in Java.

public interface Callback {
  public void call(Object          


        
相关标签:
8条回答
  • 2020-11-30 04:32

    My implementation, very easy to use, 2 util methods for debounce and throttle, pass your runnable into it to get the debounce/throttle runnable

    package basic.thread.utils;
    
    public class ThreadUtils {
        /** Make a runnable become debounce
         * 
         * usage: to reduce the real processing for some task
         * 
         * example: the stock price sometimes probably changes 1000 times in 1 second,
         *  but you just want redraw the candlestick of k-line chart after last change+"delay ms"
         * 
         * @param realRunner Runnable that has something real to do
         * @param delay milliseconds that realRunner should wait since last call
         * @return
         */
        public static Runnable debounce (Runnable realRunner, long delay) {
            Runnable debounceRunner = new Runnable() {
                // whether is waiting to run
                private boolean _isWaiting = false;
                // target time to run realRunner
                private long _timeToRun;
                // specified delay time to wait
                private long _delay = delay;
                // Runnable that has the real task to run
                private Runnable _realRunner = realRunner;
                @Override
                public void run() {
                    // current time
                    long now;
                    synchronized (this) {
                        now = System.currentTimeMillis();
                        // update time to run each time
                        _timeToRun = now+_delay;
                        // another thread is waiting, skip
                        if (_isWaiting) return;
                        // set waiting status
                        _isWaiting = true;
                    }
                    try {
                        // wait until target time
                        while (now < _timeToRun) {
                            Thread.sleep(_timeToRun-now);
                            now = System.currentTimeMillis();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // clear waiting status before run
                        _isWaiting = false;
                        // do the real task
                        _realRunner.run();
                    }
                }};
            return debounceRunner;
        }
        /** Make a runnable become throttle
         * 
         * usage: to smoothly reduce running times of some task
         * 
         * example: assume the price of a stock often updated 1000 times per second
         * but you want to redraw the candlestick of k-line at most once per 300ms
         * 
         * @param realRunner
         * @param delay
         * @return
         */
        public static Runnable throttle (Runnable realRunner, long delay) {
            Runnable throttleRunner = new Runnable() {
                // whether is waiting to run
                private boolean _isWaiting = false;
                // target time to run realRunner
                private long _timeToRun;
                // specified delay time to wait
                private long _delay = delay;
                // Runnable that has the real task to run
                private Runnable _realRunner = realRunner;
                @Override
                public void run() {
                    // current time
                    long now;
                    synchronized (this) {
                        // another thread is waiting, skip
                        if (_isWaiting) return;
                        now = System.currentTimeMillis();
                        // update time to run
                        // do not update it each time since
                        // you do not want to postpone it unlimited
                        _timeToRun = now+_delay;
                        // set waiting status
                        _isWaiting = true;
                    }
                    try {
                        Thread.sleep(_timeToRun-now);
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // clear waiting status before run
                        _isWaiting = false;
                        // do the real task
                        _realRunner.run();
                    }
                }};
            return throttleRunner;
        }
    }
    
    0 讨论(0)
  • 2020-11-30 04:36

    Here is my working implementation:

    Execution Callback:

    public interface cbDebounce {
    
    void execute();
    
    }
    

    Debouncer:

    public class Debouncer {
    
    private Timer timer;
    private ConcurrentHashMap<String, TimerTask> delayedTaskMap;
    
    public Debouncer() {
        this.timer = new Timer(true); //run as daemon
        this.delayedTaskMap = new ConcurrentHashMap<>();
    }
    
    public void debounce(final String key, final cbDebounce debounceCallback, final long delay) {
        if (key == null || key.isEmpty() || key.trim().length() < 1 || delay < 0) return;
    
        cancelPreviousTasks(); //if any
    
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                debounceCallback.execute();
                cancelPreviousTasks();
                delayedTaskMap.clear();
                if (timer != null) timer.cancel();
            }
        };
    
        scheduleNewTask(key, timerTask, delay);
    }
    
    private void cancelPreviousTasks() {
        if (delayedTaskMap == null) return;
    
        if (!delayedTaskMap.isEmpty()) delayedTaskMap
                .forEachEntry(1000, entry -> entry.getValue().cancel());
    
        delayedTaskMap.clear();
    }
    
    private void scheduleNewTask(String key, TimerTask timerTask, long delay) {
        if (key == null || key.isEmpty() || key.trim().length() < 1 || timerTask == null || delay < 0) return;
    
        if (delayedTaskMap.containsKey(key)) return;
    
        timer.schedule(timerTask, delay);
    
        delayedTaskMap.put(key, timerTask);
    }
    

    }

    Main (to test)

    public class Main {
    
    private static Debouncer debouncer;
    
    public static void main(String[] args) throws IOException, InterruptedException {
        debouncer = new Debouncer();
        search("H");
        search("HE");
        search("HEL");
        System.out.println("Waiting for user to finish typing");
        Thread.sleep(2000);
        search("HELL");
        search("HELLO");
    }
    
    private static void search(String searchPhrase) {
        System.out.println("Search for: " + searchPhrase);
        cbDebounce debounceCallback = () -> System.out.println("Now Executing search for: "+searchPhrase);
        debouncer.debounce(searchPhrase, debounceCallback, 4000); //wait 4 seconds after user's last keystroke
    }
    
    }
    

    Output

    • Search for: H
    • Search for: HE
    • Search for: HEL
    • Waiting for user to finish typing
    • Search for: HELL
    • Search for: HELLO
    • Now Executing search for: HELLO
    0 讨论(0)
  • 2020-11-30 04:40

    I've updated @Eyal's answer to be able to configure debouncing time in each call, and use runnable code block instead of callback:

    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class Debouncer<T> {
    
        private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
        private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>();
    
        public Debouncer() {
        }
    
        public void call(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
            TimerTask task = new TimerTask(key, runnable, interval, timeUnit);
    
            TimerTask prev;
            do {
                prev = delayedMap.putIfAbsent(key, task);
                if (prev == null)
                    sched.schedule(task, interval, timeUnit);
            } while (prev != null && !prev.extend());
        }
    
        public void terminate() {
            sched.shutdownNow();
        }
    
        private class TimerTask implements Runnable {
            private final T key;
            private final Runnable runnable;
            private final int interval;
            private final TimeUnit timeUnit;
            private long dueTime;
            private final Object lock = new Object();
    
            public TimerTask(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
                this.key = key;
                this.runnable = runnable;
                this.interval = interval;
                this.timeUnit = timeUnit;
                extend();
            }
    
            public boolean extend() {
                synchronized (lock) {
                    if (dueTime < 0)
                        return false;
                    dueTime = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(interval, timeUnit);
                    return true;
                }
            }
    
            public void run() {
                synchronized (lock) {
                    long remaining = dueTime - System.currentTimeMillis();
                    if (remaining > 0) { // Re-schedule task
                        sched.schedule(this, remaining, TimeUnit.MILLISECONDS);
                    } else { // Mark as terminated and invoke callback
                        dueTime = -1;
                        try {
                            runnable.run();
                        } finally {
                            delayedMap.remove(key);
                        }
                    }
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-30 04:42

    I don't know if it exists but it should be simple to implement.

    class Debouncer implements Callback {
    
      private CallBack c;
      private volatile long lastCalled;
      private int interval;
    
      public Debouncer(Callback c, int interval) {
         //init fields
      }
    
      public void call(Object arg) { 
          if( lastCalled + interval < System.currentTimeMillis() ) {
            lastCalled = System.currentTimeMillis();
            c.call( arg );
          } 
      }
    }
    

    Of course this example oversimplifies it a bit, but this is more or less all you need. If you want to keep separate timeouts for different arguments, you'll need a Map<Object,long> instead of just a long to keep track of the last execution time.

    0 讨论(0)
  • 2020-11-30 04:44

    This looks like it could work:

    class Debouncer implements Callback {
        private Callback callback;
        private Map<Integer, Timer> scheduled = new HashMap<Integer, Timer>();
        private int delay;
    
        public Debouncer(Callback c, int delay) {
            this.callback = c;
            this.delay = delay;
        }
    
        public void call(final Object arg) {
            final int h = arg.hashCode();
            Timer task = scheduled.remove(h);
            if (task != null) { task.cancel(); }
    
            task = new Timer();
            scheduled.put(h, task);
    
            task.schedule(new TimerTask() {
                @Override
                public void run() {
                    callback.call(arg);
                    scheduled.remove(h);
                }
            }, this.delay);
        }
    }
    
    0 讨论(0)
  • 2020-11-30 04:47

    The following implementation works on Handler based threads (e.g. the main UI thread, or in an IntentService). It expects only to be called from the thread on which it is created, and it will also run it's action on this thread.

    public class Debouncer
    {
        private CountDownTimer debounceTimer;
        private Runnable pendingRunnable;
    
        public Debouncer() {
    
        }
    
        public void debounce(Runnable runnable, long delayMs) {
            pendingRunnable = runnable;
            cancelTimer();
            startTimer(delayMs);
        }
    
        public void cancel() {
            cancelTimer();
            pendingRunnable = null;
        }
    
        private void startTimer(final long updateIntervalMs) {
    
            if (updateIntervalMs > 0) {
    
                // Debounce timer
                debounceTimer = new CountDownTimer(updateIntervalMs, updateIntervalMs) {
    
                    @Override
                    public void onTick(long millisUntilFinished) {
                        // Do nothing
                    }
    
                    @Override
                    public void onFinish() {
                        execute();
                    }
                };
                debounceTimer.start();
            }
            else {
    
                // Do immediately
                execute();
            }
        }
    
        private void cancelTimer() {
            if (debounceTimer != null) {
                debounceTimer.cancel();
                debounceTimer = null;
            }
        }
    
        private void execute() {
            if (pendingRunnable != null) {
                pendingRunnable.run();
                pendingRunnable = null;
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题