Interceptar el botón de retroceso del teclado virtual

84

Tengo la actividad con varios campos de entrada. Cuando se inicia la actividad, se muestra el teclado virtual. Cuando se presiona el botón Atrás, el teclado suave se cierra y para cerrar la actividad, necesito presionar el botón Atrás una vez más.

Entonces, la pregunta: ¿es posible interceptar el botón Atrás para cerrar el teclado virtual y finalizar la actividad con solo presionar el botón Atrás sin crear una función personalizada InputMethodService?

PD: Sé cómo interceptar el botón de retroceso en otros casos: onKeyDown()o onBackPressed()pero no funciona en este caso: solo se intercepta la segunda pulsación del botón de retroceso.

Sergey Glotov
fuente

Respuestas:

76

Sí, es completamente posible mostrar y ocultar el teclado e interceptar las llamadas al botón Atrás. Es un pequeño esfuerzo adicional, ya que se ha mencionado que no hay una forma directa de hacerlo en la API. La clave es anular boolean dispatchKeyEventPreIme(KeyEvent)dentro de un diseño. Lo que hacemos es crear nuestro diseño. Elegí RelativeLayout ya que era la base de mi actividad.

<?xml version="1.0" encoding="utf-8"?>
<com.michaelhradek.superapp.utilities.SearchLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.michaelhradek.superapp"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/white">

Dentro de nuestra Actividad configuramos nuestros campos de entrada y llamamos a la setActivity(...)función.

private void initInputField() {
    mInputField = (EditText) findViewById(R.id.searchInput);        

    InputMethodManager imm = 
        (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); 
    imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 
            InputMethodManager.HIDE_IMPLICIT_ONLY);

    mInputField.setOnEditorActionListener(new OnEditorActionListener() {

        @Override
        public boolean onEditorAction(TextView v, int actionId,
                KeyEvent event) {
            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                performSearch();
                return true;
            }

            return false;
        }
    });

    // Let the layout know we are going to be overriding the back button
    SearchLayout.setSearchActivity(this);
}

Obviamente, la initInputField()función configura el campo de entrada. También habilita la tecla enter para ejecutar la funcionalidad (en mi caso una búsqueda).

@Override
public void onBackPressed() {
    // It's expensive, if running turn it off.
    DataHelper.cancelSearch();
    hideKeyboard();
    super.onBackPressed();
}

Entonces, cuando onBackPressed()se llama dentro de nuestro diseño, podemos hacer lo que queramos, como ocultar el teclado:

private void hideKeyboard() {
    InputMethodManager imm = (InputMethodManager) 
        getSystemService(Context.INPUT_METHOD_SERVICE);
    imm.hideSoftInputFromWindow(mInputField.getWindowToken(), 0);
}

De todos modos, aquí está mi anulación del RelativeLayout.

package com.michaelhradek.superapp.utilities;

import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.RelativeLayout;

/**
 * The root element in the search bar layout. This is a custom view just to 
 * override the handling of the back button.
 * 
 */
public class SearchLayout extends RelativeLayout {

    private static final String TAG = "SearchLayout";

    private static Activity mSearchActivity;;

    public SearchLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SearchLayout(Context context) {
        super(context);
    }

    public static void setSearchActivity(Activity searchActivity) {
        mSearchActivity = searchActivity;
    }

    /**
     * Overrides the handling of the back key to move back to the 
     * previous sources or dismiss the search dialog, instead of 
     * dismissing the input method.
     */
    @Override
    public boolean dispatchKeyEventPreIme(KeyEvent event) {
        Log.d(TAG, "dispatchKeyEventPreIme(" + event + ")");
        if (mSearchActivity != null && 
                    event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            KeyEvent.DispatcherState state = getKeyDispatcherState();
            if (state != null) {
                if (event.getAction() == KeyEvent.ACTION_DOWN
                        && event.getRepeatCount() == 0) {
                    state.startTracking(event, this);
                    return true;
                } else if (event.getAction() == KeyEvent.ACTION_UP
                        && !event.isCanceled() && state.isTracking(event)) {
                    mSearchActivity.onBackPressed();
                    return true;
                }
            }
        }

        return super.dispatchKeyEventPreIme(event);
    }
}

Desafortunadamente, no puedo llevarme todo el crédito. Si comprueba la fuente de Android para el cuadro de diálogo de búsqueda rápido , verá de dónde vino la idea.

mhradek
fuente
2
Esto funcionó muy bien y no fue demasiado difícil de implementar (a pesar de parecer un poco aterrador).
Cesar
Muchas gracias por esto, solo desearía tener una forma de hacer que algo así funcione con versiones anteriores de Android.
zidarsk8
3
El código se puede simplificar simplemente lanzando getContext()a Activity. Siempre que el contexto del diseño sea la actividad en cuestión, por supuesto. Pero no sé si no podría ser.
mxcl
3
static mSearchActivity - que huele mal por un problema. ¿Qué pasa si tienes dos de estos en el diseño? En su lugar, utilice una variable no estática.
Puntero nulo
Funciona de maravilla. Pero, creo que es mejor usar la interfaz estática local y configurar su implementación desde la actividad interna en lugar de usar la actividad estática ...
SpiralDev
80

onKeyDown () y onBackPressed () no funcionan para este caso. Tienes que usar onKeyPreIme .

Inicialmente, debe crear un texto de edición personalizado que amplíe EditText. Y luego debe implementar el método onKeyPreIme que controla KeyEvent.KEYCODE_BACK . Después de esto, presione una vez hacia atrás lo suficiente para resolver su problema. Esta solución me funciona perfectamente.

CustomEditText.java

public class CustomEditText extends EditText {

    public CustomEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            // User has pressed Back key. So hide the keyboard
            InputMethodManager mgr = (InputMethodManager)         

           getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            mgr.hideSoftInputFromWindow(this.getWindowToken(), 0);
            // TODO: Hide your view as you do it in your activity
        }
        return false;
}

En tu XML

<com.YOURAPP.CustomEditText
     android:id="@+id/CEditText"
     android:layout_height="wrap_content"
     android:layout_width="match_parent"/> 

En tu actividad

public class MainActivity extends Activity {
   private CustomEditText editText;

   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      editText = (CustomEditText) findViewById(R.id.CEditText);
   }
}
Alican Temel
fuente
4
Amo mucho esta respuesta. Usando esto también con un fragmento (dentro de una barra de búsqueda que ni siquiera aparece siempre), y sugiriendo anular el editText, esto encapsula fácilmente este comportamiento solo cuando sea necesario
Jawad
2
Nota para todos: agregar una interfaz rápida a esto hace la vida mucho mejor, y afortunadamente es bastante fácil de hacer
Jawad
¿Cómo llamar a un método en onKeyPreIme () que está en mainActivity.java?
Abhigyan
No es necesario llamar a onKeyPreIme en actividad. Si usa CustomEditText en su actividad, el método se activa cuando presiona cualquier botón del teclado.
Alican Temel
1
Creo que esta es la mejor respuesta, porque es mucho más simple de implementar. de hecho, solo tiene que copiar la clase "CustomEditText" (y cambiar el nombre de su texto de edición en XML) y hacer lo que desee en el área // TODO. Y simplemente agregue "return true" en el área TODO si no desea cerrar su actividad. Gracias !
Chrysotribax
10

Descubrí que anular el método dispatchKeyEventPreIme de Layout Class también funciona bien. Simplemente configure su Actividad principal como un atributo y ejecute un método predefinido.

public class LinearLayoutGradient extends LinearLayout {
    MainActivity a;

    public void setMainActivity(MainActivity a) {
        this.a = a;
    }

    @Override
    public boolean dispatchKeyEventPreIme(KeyEvent event) {
        if (a != null) {
            InputMethodManager imm = (InputMethodManager) a
                .getSystemService(Context.INPUT_METHOD_SERVICE);

            if (imm.isActive() && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                a.launchMethod;
            }
        }

        return super.dispatchKeyEventPreIme(event);
    }
}
Kirill Rakhman
fuente
¿puedes mostrarme tu xml para este diseño? Estoy tratando de verificar este enfoque y tengo una vista de superficie en mi diseño personalizado, pero no intercepta nada
ZZZ
Básicamente, la vista personalizada es el elemento circundante en el xml. <?xml version="1.0" encoding="utf-8"?> <view class="package.LinearLayoutGradient"...
Kirill Rakhman
también funciona si simplemente ampliamos una vista única y anulamos su dispatchKeyEventPreIme (). No es necesario ampliar el diseño en sí.
faizal
7

Tuve éxito al anular dispatchKeyEvent :

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
        finish();
        return true;
    }
    return super.dispatchKeyEvent(event);
}

Oculta el teclado y finaliza la actividad.

jaseelder
fuente
esto parece funcionar solo en ciertos dispositivos como HTC Desire.
Abhijit
5
no funciona en samsung note 2. No se llama dispatchKeyEvent () cuando el teclado virtual está activo.
faizal
Bueno, probé esto con el Emulador Nexus 6 (Lollipop 5.0) -> funcionó y Note 4 (CM12 Lollipop 5.0.2) -> No funcionó, no es realmente la mejor solución - pero de todos modos .. Gracias :)
Martin Pfeffer
El clic del botón se consume al cerrar el teclado
slott
4

¿Cómo está mostrando el teclado virtual?

Si está utilizando InputMethodManager.showSoftInput(), puede intentar pasar a ResultReceivere implementar onReceiveResult()para manejarRESULT_HIDDEN

http://developer.android.com/reference/android/view/inputmethod/InputMethodManager.html

superusuario
fuente
Suena bien, pero showSoftInput()no muestra el teclado en mi teléfono ni en mi emulador, así que lo usotoggleSoftInput()
Sergey Glotov
1
No me funciona. onReceiveResult () se llama con RESULT_SHOWN, pero nunca más tarde con RESULT_HIDDEN
Yulia Rogovaya
2

Tuve el mismo problema, pero lo solucioné interceptando la pulsación de la tecla Atrás. En mi caso (HTC Desire, Android 2.2, API de aplicación Nivel 4) cierra el Teclado e inmediatamente finaliza la Actividad. No sé por qué esto no debería funcionar para usted también:

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        return true;
    }
    return super.onKeyDown(keyCode, event);
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        onBackPressed();
        return true;
    }
    return super.onKeyUp(keyCode, event);
}

/**
 * Called when the activity has detected the user's press of the back key
 */
private void onBackPressed() {
    Log.e(TAG, "back pressed");
    finish();
}
whlk
fuente
esto no funciona en samsung galaxy note 2. el onkeydown () o onbackpressed () no se activan cuando el teclado
virtual
1

Utilice el onKeyPreIme(int keyCode, KeyEvent event)método y verifique el KeyEvent.KEYCODE_BACKevento. Es muy simple sin necesidad de realizar ninguna codificación elegante.

Umair
fuente
0

Pruebe este código en su implementación BackPressed ( Block Back Button en Android ):

InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(myEditText.getWindowToken(), 0);

Te sugiero que eches un vistazo @ Cerrar / ocultar el teclado virtual de Android

100rabh
fuente
1
Ya probé esto, pero todavía necesito presionar dos veces el botón Atrás. Según tengo entendido, el primer clic es interceptado por el teclado virtual, por lo onBackPressed()que no funciona. Y solo después de que el segundo programa de prensa caiga en el onBackPressed().
Sergey Glotov
@Sergey Glotov Probé muchas sugerencias para el problema y finalmente encontré una solución no tan buena en la que tengo que implementar mi propio SoftKeyBoard. Pero espero que la comunidad de Android pronto salga con una solución mucho mejor.
100rabh
0

mi versión de la solución @mhradek:

Diseño

class BazingaLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    ConstraintLayout(context, attrs, defStyleAttr) {

var activity: Activity? = null

override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
    activity?.let {
        if (event.keyCode == KeyEvent.KEYCODE_BACK) {
            val state = keyDispatcherState
            if (state != null) {
                if (event.action == KeyEvent.ACTION_DOWN
                    && event.repeatCount == 0) {
                    state.startTracking(event, this)
                    return true
                } else if (event.action == KeyEvent.ACTION_UP && !event.isCanceled && state.isTracking(event)) {
                    it.onBackPressed()
                    return true
                }
            }
        }
    }
    return super.dispatchKeyEventPreIme(event)
}

}

archivo xml

<com... BazingaLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/grey">
 </com... BazingaLayout>

Fragmento

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        (view as BazingaLayout).activity = activity
        super.onViewCreated(view, savedInstanceState)
}
tasjapr
fuente