Bloqueo de doble clic. ¿Bicicleta?

Cómo comenzó



Una vez más cavando con el código heredado y luchando con una fuga de contexto, rompí el bloqueo de doble clic en el botón de la aplicación. Tuve que buscar qué rompí exactamente y cómo se implementó. Teniendo en cuenta que, básicamente, para dicho bloqueo se propone deshabilitar el elemento de la interfaz de usuario o simplemente ignorar los clics posteriores durante un corto período de tiempo, la solución existente me pareció bastante interesante desde el punto de vista. código de enlace Pero aún así requería crear listas de botones y escribir bastante código de soporte. Cree una instancia de la clase que almacenará la lista de elementos, complétela, llame a tres métodos en cada controlador de clics. En general, hay muchos lugares donde puedes olvidar o confundir algo. Y no me gusta recordar nada. Cada vez que me parece que recuerdo algo, resultaque recuerdo mal o que alguien ya lo rehizo de manera diferente.



Dejamos de lado la pregunta de si es correcto hacer esto o si solo necesitamos transferir eficientemente los manejadores reinterables a los flujos de fondo. Simplemente haremos otra bicicleta, quizás un poco más cómoda.



En general, la pereza natural me hizo pensar, ¿es posible prescindir de todo esto? Bueno, para bloquear el botón y olvidar. Y allí continuará funcionando como debería. Al principio, se pensó que probablemente ya había algún tipo de biblioteca que podría conectarse y que solo un método del tipo debería llamarse: sdelayMneHorosho ().



Pero, de nuevo, soy un hombre en cierto sentido de la vieja escuela y, por lo tanto, no me gustan todo tipo de adicciones innecesarias. El zoológico de bibliotecas y generación de código me hace sentir abatido y decepcionado con la humanidad. Bueno, el google superficial solo encontró opciones típicas con temporizadores o sus variaciones.



Por ejemplo:



uno



dos



y así sucesivamente ...



Aún así, probablemente pueda apagar el elemento en la primera línea del controlador y luego encenderlo. El único problema es que esto no siempre ocurre de manera trivial, y en este caso es necesario agregar una llamada al código de "encendido" al final de todas las opciones que puede causar un botón. No es sorprendente que tales decisiones no se hayan buscado en Google de inmediato. Son muy complicados y es extremadamente difícil mantenerlos.



Quería hacerlo más simple, más universal, y recordar que era necesario lo menos posible.



Solución del proyecto.



Como dije, la solución existente estaba interesantemente organizada, aunque tenía todas las deficiencias de las soluciones existentes. Al menos era una clase simple separada. También permitió que se deshabilitaran diferentes listas de elementos, aunque no estoy seguro de si eso tiene sentido.



La clase original para bloquear el doble clic
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();
			}
		});

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




La clase es pequeña y, en principio, está claro lo que hace. En pocas palabras, para bloquear el botón en cualquier actividad o fragmento, debe crear una instancia de la clase MultiClickFilter y completar su lista con elementos de la interfaz de usuario que deben bloquearse. Puede hacer varias listas, pero en este caso, el controlador de cada elemento debe "saber" qué instancia del "filtro de clic" debe extraer.



Además, no te permite ignorar el clic. Para hacer esto, es imprescindible bloquear toda la lista de elementos y, por lo tanto, debe desbloquearse. Esto lleva a que se agregue código adicional a cada controlador. Y en el ejemplo, pondría el método unlockButtons en el bloque finalmente, pero nunca se sabe ... En general, esta decisión plantea preguntas.



Nueva solución



En general, al darse cuenta de que probablemente no habrá algún tipo de bala de plata, se aceptó como premisa inicial:



  1. No es aconsejable separar listas de botones bloqueables. Bueno, no se me ocurre ningún ejemplo que requiera tal separación.
  2. No deshabilite el elemento (habilitado / se puede hacer clic) para preservar las animaciones y, en general, la vida del elemento.
  3. Bloquee un clic en cualquier controlador destinado a esto, porque se supone que un usuario adecuado no hace clic en ninguna parte como si fuera una ametralladora, y para evitar un "rebote" accidental es suficiente simplemente desactivar el procesamiento de clics durante varios cientos de milisegundos "para todos"


Entonces, idealmente, deberíamos tener un punto en el código donde se lleva a cabo todo el procesamiento y un método que se sacudirá desde cualquier parte del proyecto en cualquier controlador y bloqueará el procesamiento de clics repetidos. Supongamos que nuestra IU no implica que el usuario haga clic más de dos veces por segundo. No, si es necesario, entonces, aparentemente, tendrá que prestar especial atención al rendimiento, pero nuestro caso es simple, de modo que con dedos temblorosos no sería posible soltar la aplicación en una función no reinterable. Y también, para que no tenga que tomar un baño de vapor cada vez que optimice el rendimiento de una transición simple de una actividad a otra o muestre un diálogo de progreso cada vez.



Todo esto funcionará para nosotros en el hilo principal, por lo que no debemos preocuparnos por la sincronización. Además, de hecho, podemos transferir el cheque a si necesitamos procesar el clic o ignorarlo, en este mismo método. Bueno, si funciona, podríamos personalizar el intervalo de bloqueo. De modo que, en un caso muy malo, puede aumentar el intervalo para un controlador en particular.



¿Es posible?



La implementación resultó ser sorprendentemente simple y concisa.
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;

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

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




En general, ahora solo necesita agregar la clase MultiClickFilter al proyecto y, al comienzo de cada controlador de clic, verifique si está bloqueado:



        if (MultiClickFilter.clickIsLocked())
            return;


Si se va a procesar el clic, se establecerá un bloqueo para el tiempo especificado (o de forma predeterminada). El método le permitirá no pensar en listas de elementos, no crear comprobaciones complejas y no controlar la disponibilidad de elementos de la interfaz de usuario manualmente. Sugiero discutir esta implementación en los comentarios, ¿tal vez hay mejores opciones?



All Articles