С чего началось
В очередной раз копаясь с легаси кодом и борясь с утечкой контекста я сломал в приложении блокировку двойного клика на кнопке. Пришлось искать, что именно я сломал и как это было реализовано. С учетом того, что в основном для подобной блокировки предлогается либо отключать сам 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 элементов вручную. Предлагаю обсудить в комментариях эту реализацию, возможно есть лучшие варианты?
ссылка на оригинал статьи https://habr.com/ru/post/508908/