Блокировка двойного клика. Велосипед?

С чего началось

В очередной раз копаясь с легаси кодом и борясь с утечкой контекста я сломал в приложении блокировку двойного клика на кнопке. Пришлось искать, что именно я сломал и как это было реализовано. С учетом того, что в основном для подобной блокировки предлогается либо отключать сам 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, а то мало ли… В общем, это решение вызывает вопросы.

Новое решение

В общем, понимая, что наверное не будет какой-то серебряной пули, в качестве исходных посылок было принято:

  1. Разделять списки блокируемых кнопок не целесообразно. Ну не смог я придумать никакого примера, требующего такого разделения.
  2. Не отключать элемент (enabled/clickable) чтобы сохранить анимации и вообще живость элемента
  3. Блокировать клик в любом обработчике, который для этого предназначен, т.к. предполагается, что адекватный пользователь не кликает куда попало как из пулемета, а для предотвращения случайного «дребезга» достаточно просто отключить обработку кликов на несколько сотен миллисекунд «для всех»

Значит, в идеале, у нас должна быть одна точка в коде, где происходит вся обработка и один метод, который будет дергаться из любого места проекта в любом обработчике и будет блокировать обработку повторных нажатий. Предположим, что наш 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/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *