Double click blocking. Bicycle?

Where did it start



Once again, digging with the Legacy code and struggling with a context leak, I broke the double-click lock on the button in the application. I had to look for what exactly I broke and how it was implemented. Given the fact that basically for such a lock it is proposed to either disable the UI element itself or simply ignore subsequent clicks for a short period of time, the existing solution seemed to me rather interesting since linking code. But it still required to create button lists and write quite a lot of supporting code. Create an instance of the class that will store the list of elements, fill it in, call three methods in each click handler. In general, there are many places where you can forget or confuse something. And I don't like to remember anything. Every time I think I remember something, it turns outthat either I remember incorrectly, or someone has already redone it differently.



Let's leave out of the brackets the question of whether it is correct to do this or whether you just need to qualitatively move the reintegrable handlers into the background streams. We'll just make another bike, maybe a little more comfortable.



In general, natural laziness made me think, is it possible to do without all this? Well, to block the button and forget. And it will continue to work there as it should. At first I thought that there probably already exists some library that can be connected and that only one method of the type, sdelayMneHorosho (), will have to be called.



But again, I am a person in a certain sense of the old school and therefore do not like any unnecessary addictions. The zoo of libraries and code generation makes me despondent and frustrated in humanity. Well, surface googling found only typical options with timers or their variations.



For example:



One



Two



And so on ...



Also, you can probably just turn off the element with the first line in the handler, and then turn it on. The only problem is that this then does not always happen trivially, and in this case it is necessary to add a call to the β€œpower-on” code at the end of all options that a button press can cause. It is not surprising that such decisions were not googled right away. They are very complicated and it is extremely difficult to maintain them.



I wanted to make it simpler, more universal, and to remember it was necessary as little as possible.



Solution from project



As I said, the existing solution was interestingly put together, although it had all the disadvantages of existing solutions. At least it was a separate simple class. He also allowed to make different lists of disconnected items, although I’m not sure if this makes sense.



The original class for blocking double-clicking
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);
    }
}


Usage example:



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();
			}
		});

		<=======  ========>
	}
	
	<=======  ========>
}




The class is small and, in principle, it is clear what it does. In a nutshell, in order to block the button on any activity or fragment, you need to create an instance of the MultiClickFilter class and fill its list with UI elements that need to be blocked. You can make several lists, but in this case, the handler of each element must "know" which instance of the "clickfilter" to pull.



Plus, it doesn't let you just ignore the click. To do this, it is imperative to lock the entire list of elements, and then, therefore, it must be unblocked. This leads to additional code that must be added to each handler. Yes, and in the example, I would put the unlockButtons method in the finally block, but you never know ... In general, this decision raises questions.



New solution



In general, realizing that there will probably not be some kind of silver bullet, the following were accepted as initial premises:



  1. Separating lists of lockable buttons is not advisable. Well, I could not think of any example requiring such a separation.
  2. Do not disable the element (enabled / clickable) to preserve the animations and generally the liveness of the element
  3. Block a click in any handler that is intended for this, because it is assumed that an adequate user does not click anywhere as if from a machine gun, and to prevent accidental "bounce" it is enough to simply turn off click processing for several hundred milliseconds "for everyone"


So, ideally, we should have one point in the code where all processing takes place and one method that will jerk from anywhere in the project in any handler and will block processing of repeated clicks. Let's assume that our UI does not imply that the user clicks more than twice a second. Not, if it is necessary, then, apparently, you will have to pay special attention to performance, but our case is simple, so that fingers trembling with delight cannot drop the application on a non-re-enterable function. And also, so that you don’t have to take a steam bath every time over optimizing the performance of a simple transition from one activity to another or flashing a progress dialog every time.



All this will work for us in the main thread, so we do not need to worry about synchronization. Also, in fact, we can transfer the check on whether we need to handle the click or ignore it in this very method. Well, if possible, then it would be possible to make the blocking interval customizable. So that in a completely bad case, you could increase the interval for a particular processor.



Is it possible?



The implementation turned out to be surprisingly simple and concise.
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);
    }
}


Usage example:



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;

		<=======  ========>
	}

	<=======  ========>
}




By and large, now you just need to add the MultiClickFilter class to the project and at the beginning of each click handler check whether it is blocked:



        if (MultiClickFilter.clickIsLocked())
            return;


If a click is to be processed, a block will be set for a specified time (or by default). The method will allow you not to think about lists of elements, not to build complex checks and not to control the availability of UI elements manually. I suggest discussing this implementation in the comments, maybe there are better options?



All Articles