С чего началось
В очередной раз копаясь с легаси кодом и борясь с утечкой контекста я сломал в приложении блокировку двойного клика на кнопке. Пришлось искать, что именно я сломал и как это было реализовано. С учетом того, что в основном для подобной блокировки предлагается либо отключать сам UI элемент либо просто игнорировать последующие клики в течении небольшого промежутка времени, существующее решение показалось мне довольно интересным с т.з. компоновки кода. Но оно все равно требовало создавать списки кнопок и писать довольно много поддерживающего кода. Создать экземпляр класса, который будет хранить список элементов, заполнить его, в каждом обработчике клика вызывать три метода. В общем, много мест, где можно что-нибудь забыть или перепутать. А я не люблю ничего помнить. Каждый раз когда мне кажется что я что-то помню, оказывается, что либо я помню неправильно, либо кто-то это уже переделал по другому.
Оставим за скобками вопрос о том, правильно ли так делать или надо просто качественно выносить реинтерабельные обработчики в бэкграундные потоки. Просто будем делать очередной велосипед, может быть немного более удобный.
В общем, природная лень заставила задуматься, а можно ли сделать без всего этого? Ну чтобы заблокировал кнопку и забыл. А оно там само дальше будет работать как надо. Сначала появилась мысль, что наверняка уже есть какая-нибудь библиотека, которую можно подключить и надо будет вызывать всего один метод типа — sdelayMneHorosho().
Но опять же, человек я в определенном смысле старой закалки и поэтому не люблю всякие лишние зависимости. Зоопарк библиотек и кодогенерации вызывает у меня уныние и разочарование в человечестве. Ну и поверхностное гугление находило только типичные варианты с таймерами или их вариациями.
Например:
Раз
Два
И так далее...
Еще, наверное можно просто выключать элемент первой строкой в обработчике, а потом включать. Проблема только в том, что это потом наступает не всегда тривиально и в этом случае необходимо добавлять вызов кода «включения» в конце всех вариантов исполнения, к которым может привести нажатие кнопки. Неудивительно, что такие решения у меня с ходу не нагуглились. Очень уж запутанные они и поддерживать их крайне сложно.
Захотелось сделать проще, универсальнее, и чтобы помнить надо было как можно меньше.
Решение из проекта
Как я уже говорил, существующее решение было интересно скомпоновано, хотя и обладало всеми недостатками существующих решений. По крайней мере это было отдельный простой класс. Также он позволял делать разные списки отключаемых элементов, хотя я и не уверен, что в этом есть смысл.
Оригинальный класс для блокировки двойного нажатия
Пример использования:
public class MultiClickFilter {
private static final long TEST_CLICK_WAIT = 500;
private ArrayList<View> buttonList = new ArrayList<>();
private long lastClickMillis = -1;
// User is responsible for setting up this list before using
public ArrayList<View> getButtonList() {
return buttonList;
}
public void lockButtons() {
lastClickMillis = System.currentTimeMillis();
for (View b : buttonList) {
disableButton(b);
}
}
public void unlockButtons() {
for (View b : buttonList) {
enableButton(b);
}
}
// function to help prevent execution of rapid multiple clicks on drive buttons
//
public boolean isClickedLately() {
return (System.currentTimeMillis() - lastClickMillis) < TEST_CLICK_WAIT; // true will block execution of button function.
}
private void enableButton(View button) {
button.setClickable(true);
button.setEnabled(true);
}
private void disableButton(View button) {
button.setClickable(false);
button.setEnabled(false);
}
}
Пример использования:
public class TestFragment extends Fragment {
<======= Кусь ========>
private MultiClickFilter testMultiClickFilter = new MultiClickFilter();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
<======= Кусь ========>
testMultiClickFilter.getButtonList().add(testButton);
testMultiClickFilter.getButtonList().add(test2Button);
<======= Кусь ========>
testButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (testMultiClickFilter.isClickedLately()) {
return;
}
testMultiClickFilter.lockButtons();
startTestPlayback(v);
testMultiClickFilter.unlockButtons();
}
});
test2Button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (testMultiClickFilter.isClickedLately()) {
return;
}
testMultiClickFilter.lockButtons();
loadTestProperties(v);
testMultiClickFilter.unlockButtons();
}
});
<======= Кусь ========>
}
<======= Кусь ========>
}
Класс небольшой и в принципе понятно что он делает. В двух словах, для того, чтобы заблокировать кнопку на какой-нибудь активити или фрагменте, нужно создать экземпляр класса MultiClickFilter и заполнить его список UI элементами, которые надо блокировать. Можно сделать несколько списков, но в этом случае обработчик каждого элемента должен «знать» какой экземпляр «кликфильтра» дергать.
Кроме того, он не позволяет просто проигнорировать клик. Для этого обязательно надо заблокировать весь список элементов, а потом, следовательно, его обязательно надо разблокировать. Это приводит к дополнительному коду который надо добавить в каждый обработчик. Да и в примере я бы метод unlockButtons поместил в блок finally, а то мало ли… В общем, это решение вызывает вопросы.
Новое решение
В общем, понимая, что наверное не будет какой-то серебряной пули, в качестве исходных посылок было принято:
- Разделять списки блокируемых кнопок не целесообразно. Ну не смог я придумать никакого примера, требующего такого разделения.
- Не отключать элемент (enabled/clickable) чтобы сохранить анимации и вообще живость элемента
- Блокировать клик в любом обработчике, который для этого предназначен, т.к. предполагается, что адекватный пользователь не кликает куда попало как из пулемета, а для предотвращения случайного «дребезга» достаточно просто отключить обработку кликов на несколько сотен миллисекунд «для всех»
Значит, в идеале, у нас должна быть одна точка в коде, где происходит вся обработка и один метод, который будет дергаться из любого места проекта в любом обработчике и будет блокировать обработку повторных нажатий. Предположим, что наш UI не подразумевает, что пользователь кликает чаще чем два раза в секунду. Не, если это необходимо, то видимо придется отдельно уделять внимание производительности, у нас же случай простой, чтобы дрожащими от восторга пальцами нельзя было уронить приложение на нереинтерабельной функции. А также, чтобы не надо было каждый раз париться над оптимизацией производительности простого перехода с одной активити на другую или каждый раз мелькать диалогом прогресса.
Все это у нас будет работать в главном потоке, так что нам не надо беспокоится о синхронизации. Также, фактически, мы можем перенести проверку на то, надо ли нам обработать клик или проигнорировать его, в этот самый метод. Ну, если получится, то можно было бы и сделать интервал блокировки настраиваемым. Так чтобы в совсем нехорошем случае можно было увеличить интервал для конкретного обработчика.
Возможно ли это?
Реализация оказалась на удивление простой и лаконичной
Пример использования:
package com.ai.android.common;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.MainThread;
public abstract class MultiClickFilter {
private static final int DEFAULT_LOCK_TIME_MS = 500;
private static final Handler uiHandler = new Handler(Looper.getMainLooper());
private static boolean locked = false;
@MainThread
public static boolean clickIsLocked(int lockTimeMs) {
if (locked)
return true;
locked = true;
uiHandler.postDelayed(() -> locked = false, lockTimeMs);
return false;
}
@MainThread
public static boolean clickIsLocked() {
return clickIsLocked(DEFAULT_LOCK_TIME_MS);
}
}
Пример использования:
public class TestFragment {
<======= Кусь ========>
private ListView devicePropertiesListView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
devicePropertiesListView = view.findViewById(R.id.list_view);
devicePropertiesListView.setOnItemClickListener(this::doOnItemClick);
<======= Кусь ========>
return view;
}
private void doOnItemClick(AdapterView<?> adapterView, View view, int position, long id) {
if (MultiClickFilter.clickIsLocked(1000 * 2))
return;
<======= Кусь ========>
}
<======= Кусь ========>
}
По большому счету теперь надо просто добавить в проект класс MultiClickFilter и в начале каждого обработчика клика проверять не заблокирован ли он:
if (MultiClickFilter.clickIsLocked())
return;
Если клик подлежит обработке, то установится блокировка на заданное время (или по умолчанию). Метод позволят не думать о списках элементов, не строить сложные проверки и не управлять доступностью UI элементов вручную. Предлагаю обсудить в комментариях эту реализацию, возможно есть лучшие варианты?