Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров4.5K

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


В очередной раз копаясь с легаси кодом и борясь с утечкой контекста я сломал в приложении блокировку двойного клика на кнопке. Пришлось искать, что именно я сломал и как это было реализовано. С учетом того, что в основном для подобной блокировки предлагается либо отключать сам 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 элементов вручную. Предлагаю обсудить в комментариях эту реализацию, возможно есть лучшие варианты?
Теги:
Хабы:
Всего голосов 13: ↑10 и ↓3+7
Комментарии34

Публикации

Истории

Работа

Java разработчик
358 вакансий

Ближайшие события