windowsoftinputmode studio edittext developer custom android view keyboard

studio - keyboard hide edittext android



Android: no se puede capturar retroceso/borrar presionar suavemente. teclado (11)

Estoy anulando el método onKeyDown de la vista (vista de superficie OpenGL) para capturar todas las pulsaciones de teclas. El problema es que en varios dispositivos no se captura KEYCODE_DEL. He intentado agregar un onKeyListener a la vista, y eso capturó todo excepto la tecla de retroceso.

Tiene que haber una manera de escuchar este evento de prensa clave, pero ¿cómo?


11/12/2014 ACTUALIZACIÓN: Se modificó el alcance de la corrección para que no se limite a <API nivel 19, ya que en un teclado de un tercero todavía tiene el error más allá de 19.

1/9/2014 ACTUALIZACIÓN: He ideado un enfoque, con código, para resolver todos los problemas KEYCODE_DEL de Google Keyboard (LatinIME), específicamente los números 42904 y 62306.

La mejora en la respuesta de Turix se ha incorporado, con permiso, a mi propio código aquí . Las mejoras de Turix eliminadas necesitan inyectar caracteres basura en el búfer editable al encontrar una forma incremental para garantizar que haya exactamente un carácter siempre en ese búfer.

He usado código (similar) para esto en una aplicación implementada que puedes probar:
https://play.google.com/store/apps/details?id=com.goalstate.WordGames.FullBoard.trialsuite]

INTRODUCCIÓN:

La solución que se presenta a continuación está diseñada para funcionar con todas las versiones del Teclado de Google, tanto pasadas como futuras, en lo que respecta a estos dos errores. Esta solución alternativa no requiere que una aplicación permanezca bloqueada para apuntar al nivel de API 15 o inferior, que algunas aplicaciones se han restringido para aprovechar el código de compatibilidad que soluciona el problema 42904.

Estos problemas solo están presentes como errores para una vista que ha implementado la anulación para onCreateInputConnection (), y que devuelve TYPE_NULL al IME invocado (en el miembro inputType del argumento EditorInfo pasado a ese método por el IME). Es solo de esta manera que una vista puede esperar razonablemente que los eventos clave (incluido KEYCODE_DEL) le sean devueltos desde un teclado virtual. En consecuencia, la solución presentada aquí requiere TYPE_NULL InputType.

Para las aplicaciones que no usan TYPE_NULL, hay varias sustituciones en el objeto derivado de BaseInputConnection devuelto por una vista desde su anulación onCreateInputConnection () que el IME invoca cuando el usuario realiza ediciones, en lugar de los eventos clave que generan IME. Este enfoque (no TYPE_NULL) suele ser superior, porque las capacidades del teclado suave ahora van más allá del simple toque de teclas, a cosas como entrada de voz, finalización, etc. Los eventos clave son un método anterior, y los que implementan LatinIME en Google han dicho que les gustaría ver el uso de TYPE_NULL (y eventos clave) desaparecer.

Si suspender el uso de TYPE_NULL es una opción, le insto a continuar con el enfoque recomendado de utilizar los métodos de anulación de InputConnection en lugar de los eventos clave (o, más simplemente, mediante el uso de una clase derivada de EditText, que hace eso por usted )

No obstante, el comportamiento de TYPE_NULL no se suspende oficialmente, y por lo tanto, la falla de LatinIME para generar eventos KEYCODE_DEL bajo ciertas circunstancias es de hecho un error. Ofrezco la siguiente solución para abordar este problema.

VISIÓN DE CONJUNTO:

Los problemas que han tenido las aplicaciones al recibir KEYCODE_DEL de LatinIME se deben a DOS errores conocidos, como se informa aquí :

https://code.google.com/p/android/issues/detail?id=42904 (aparece como WorkingAsIntended, pero el problema es que, mantengo, un error en la medida en que provoca un error al admitir la generación de eventos KEYCODE_DEL para aplicaciones que se dirigen API de nivel 16 y superior que ha enumerado específicamente un InputType de TYPE_NULL. El problema está solucionado en las últimas versiones de LatinIME, pero hay lanzamientos pasados ​​en la naturaleza que todavía muestran este error, y por lo tanto aplicaciones que usan TYPE_NULL y API de nivel 16 o arriba todavía necesitará una solución alternativa que se pueda realizar desde la aplicación.

y aquí :

http://code.google.com/p/android/issues/detail?id=62306 (actualmente listado como fijo pero aún no publicado - FutureRelease), pero incluso una vez que se publique, todavía necesitaremos una solución alternativa que se pueda realizar desde dentro de la aplicación para lidiar con versiones anteriores que persistirán "en la naturaleza").

De acuerdo con esta tesis (que los problemas experimentados con KEYCODE_DEL eventos se deben a errores en LatinIME), he encontrado que cuando se utiliza un teclado de hardware externo, y también cuando se utiliza el teclado de SwiftKey de terceros, estos problemas no ocurren, mientras que ocurren para versiones específicas de LatinIME.

Uno o el otro (pero no ambos a la vez) de estos problemas está presente en algunas versiones de LatinIME. En consecuencia, es difícil para los desarrolladores saber durante las pruebas si han solucionado todos los problemas de KEYCODE_DEL, y algunas veces cuando se realiza una actualización de Android (o Google Keyboard), un problema ya no será reproducible en las pruebas. No obstante, las versiones de LatinIME que causan el problema estarán presentes en una gran cantidad de dispositivos en uso. Esto me ha obligado a profundizar en el repo de GIT de AOSP LatinIME para determinar el alcance exacto de cada uno de los dos problemas (es decir, las versiones específicas de LatinIME y Android, para las cuales puede estar presente cada uno de los dos problemas). El siguiente código de solución se ha restringido a esas versiones específicas.

El código de solución presentado a continuación incluye comentarios extensos que deberían ayudarlo a comprender lo que está intentando lograr. Después de la presentación del código, proporcionaré algunos comentarios adicionales, que incluirán las confirmaciones específicas del Proyecto de Código Abierto de Android (AOSP) en las que se introdujeron cada uno de los dos errores, y en los que desaparecieron, y también las versiones de Android que podrían incluir las versiones afectadas de Google Keyboard.

Advertiría a cualquiera que esté pensando en usar este enfoque que realice sus propias pruebas para verificar que funcione para su aplicación particular. Creo que funcionará en general, y lo he probado en una serie de dispositivos y versiones de LatinIME, pero el razonamiento es complicado, así que proceda con precaución. Si encuentra algún problema, publique un comentario a continuación.

CÓDIGO :

Aquí, entonces, está mi solución para ambos problemas, con una explicación incluida en los comentarios al código:

Primero, incluya la siguiente clase (editada a gusto) en su aplicación, en su propio archivo fuente InputConnectionAccomodatingLatinIMETypeNullIssues.java:

import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.BaseInputConnection; /** * * @author Carl Gunther * There are bugs with the LatinIME keyboard''s generation of KEYCODE_DEL events * that this class addresses in various ways. These bugs appear when the app * specifies TYPE_NULL, which is the only circumstance under which the app * can reasonably expect to receive key events for KEYCODE_DEL. * * This class is intended for use by a view that overrides * onCreateInputConnection() and specifies to the invoking IME that it wishes * to use the TYPE_NULL InputType. This should cause key events to be returned * to the view. * */ public class InputConnectionAccomodatingLatinIMETypeNullIssues extends BaseInputConnection { //This holds the Editable text buffer that the LatinIME mistakenly *thinks* // that it is editing, even though the views that employ this class are // completely driven by key events. Editable myEditable = null; //Basic constructor public InputConnectionAccomodatingLatinIMETypeNullIssues(View targetView, boolean fullEditor) { super(targetView, fullEditor); } //This method is called by the IME whenever the view that returned an // instance of this class to the IME from its onCreateInputConnection() // gains focus. @Override public Editable getEditable() { //Some versions of the Google Keyboard (LatinIME) were delivered with a // bug that causes KEYCODE_DEL to no longer be generated once the number // of KEYCODE_DEL taps equals the number of other characters that have // been typed. This bug was reported here as issue 62306. // // As of this writing (1/7/2014), it is fixed in the AOSP code, but that // fix has not yet been released. Even when it is released, there will // be many devices having versions of the Google Keyboard that include the bug // in the wild for the indefinite future. Therefore, a workaround is required. // //This is a workaround for that bug which just jams a single garbage character // into the internal buffer that the keyboard THINKS it is editing even // though we have specified TYPE_NULL which *should* cause LatinIME to // generate key events regardless of what is in that buffer. We have other // code that attempts to ensure as the user edites that there is always // one character remaining. // // The problem arises because when this unseen buffer becomes empty, the IME // thinks that there is nothing left to delete, and therefore stops // generating KEYCODE_DEL events, even though the app may still be very // interested in receiving them. // //So, for example, if the user taps in ABCDE and then positions the // (app-based) cursor to the left of A and taps the backspace key three // times without any evident effect on the letters (because the app''s own // UI code knows that there are no letters to the left of the // app-implemented cursor), and then moves the cursor to the right of the // E and hits backspace five times, then, after E and D have been deleted, // no more KEYCODE_DEL events will be generated by the IME because the // unseen buffer will have become empty from five letter key taps followed // by five backspace key taps (as the IME is unaware of the app-based cursor // movements performed by the user). // // In other words, if your app is processing KEYDOWN events itself, and // maintaining its own cursor and so on, and not telling the IME anything // about the user''s cursor position, this buggy processing of the hidden // buffer will stop KEYCODE_DEL events when your app actually needs them - // in whatever Android releases incorporate this LatinIME bug. // // By creating this garbage characters in the Editable that is initially // returned to the IME here, we make the IME think that it still has // something to delete, which causes it to keep generating KEYCODE_DEL // events in response to backspace key presses. // // A specific keyboard version that I tested this on which HAS this // problem but does NOT have the "KEYCODE_DEL completely gone" (issue 42904) // problem that is addressed by the deleteSurroundingText() override below // (the two problems are not both present in a single version) is // 2.0.19123.914326a, tested running on a Nexus7 2012 tablet. // There may be other versions that have issue 62306. // // A specific keyboard version that I tested this on which does NOT have // this problem but DOES have the "KEYCODE_DEL completely gone" (issue // 42904) problem that is addressed by the deleteSurroundingText() // override below is 1.0.1800.776638, tested running on the Nexus10 // tablet. There may be other versions that also have issue 42904. // // The bug that this addresses was first introduced as of AOSP commit tag // 4.4_r0.9, and the next RELEASED Android version after that was // android-4.4_r1, which is the first release of Android 4.4. So, 4.4 will // be the first Android version that would have included, in the original // RELEASED version, a Google Keyboard for which this bug was present. // // Note that this bug was introduced exactly at the point that the OTHER bug // (the one that is addressed in deleteSurroundingText(), below) was first // FIXED. // // Despite the fact that the above are the RELEASES associated with the bug, // the fact is that any 4.x Android release could have been upgraded by the // user to a later version of Google Keyboard than was present when the // release was originally installed to the device. I have checked the // www.archive.org snapshots of the Google Keyboard listing page on the Google // Play store, and all released updates listed there (which go back to early // June of 2013) required Android 4.0 and up, so we can be pretty sure that // this bug is not present in any version earlier than 4.0 (ICS), which means // that we can limit this fix to API level 14 and up. And once the LatinIME // problem is fixed, we can limit the scope of this workaround to end as of // the last release that included the problem, since we can assume that // users will not upgrade Google Keyboard to an EARLIER version than was // originally included in their Android release. // // The bug that this addresses was FIXED but NOT RELEASED as of this AOSP // commit: //https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+ // /b41bea65502ce7339665859d3c2c81b4a29194e4/java/src/com/android // /inputmethod/latin/LatinIME.java // so it can be assumed to affect all of KitKat released thus far // (up to 4.4.2), and could even affect beyond KitKat, although I fully // expect it to be incorporated into the next release *after* API level 19. // // When it IS released, this method should be changed to limit it to no // higher than API level 19 (assuming that the fix is released before API // level 20), just in order to limit the scope of this fix, since poking // 1024 characters into the Editable object returned here is of course a // kluge. But right now the safest thing is just to not have an upper limit // on the application of this kluge, since the fix for the problem it // addresses has not yet been released (as of 1/7/2014). if(Build.VERSION.SDK_INT >= 14) { if(myEditable == null) { myEditable = new EditableAccomodatingLatinIMETypeNullIssues( EditableAccomodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER); Selection.setSelection(myEditable, 1); } else { int myEditableLength = myEditable.length(); if(myEditableLength == 0) { //I actually HAVE seen this be zero on the Nexus 10 with the keyboard // that came with Android 4.4.2 // On the Nexus 10 4.4.2 if I tapped away from the view and then back to it, the // myEditable would come back as null and I would create a new one. This is also // what happens on other devices (e.g., the Nexus 6 with 4.4.2, // which has a slightly later version of the Google Keyboard). But for the // Nexus 10 4.4.2, the keyboard had a strange behavior // when I tapped on the rack, and then tapped Done on the keyboard to close it, // and then tapped on the rack AGAIN. In THAT situation, // the myEditable would NOT be set to NULL but its LENGTH would be ZERO. So, I // just append to it in that situation. myEditable.append( EditableAccomodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER); Selection.setSelection(myEditable, 1); } } return myEditable; } else { //Default behavior for keyboards that do not require any fix return super.getEditable(); } } //This method is called INSTEAD of generating a KEYCODE_DEL event, by // versions of Latin IME that have the bug described in Issue 42904. @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { //If targetSdkVersion is set to anything AT or ABOVE API level 16 // then for the GOOGLE KEYBOARD versions DELIVERED // with Android 4.1.x, 4.2.x or 4.3.x, NO KEYCODE_DEL EVENTS WILL BE // GENERATED BY THE GOOGLE KEYBOARD (LatinIME) EVEN when TYPE_NULL // is being returned as the InputType by your view from its // onCreateInputMethod() override, due to a BUG in THOSE VERSIONS. // // When TYPE_NULL is specified (as this entire class assumes is being done // by the views that use it, what WILL be generated INSTEAD of a KEYCODE_DEL // is a deleteSurroundingText(1,0) call. So, by overriding this // deleteSurroundingText() method, we can fire the KEYDOWN/KEYUP events // ourselves for KEYCODE_DEL. This provides a workaround for the bug. // // The specific AOSP RELEASES involved are 4.1.1_r1 (the very first 4.1 // release) through 4.4_r0.8 (the release just prior to Android 4.4). // This means that all of KitKat should not have the bug and will not // need this workaround. // // Although 4.0.x (ICS) did not have this bug, it was possible to install // later versions of the keyboard as an app on anything running 4.0 and up, // so those versions are also potentially affected. // // The first version of separately-installable Google Keyboard shown on the // Google Play store site by www.archive.org is Version 1.0.1869.683049, // on June 6, 2013, and that version (and probably other, later ones) // already had this bug. // //Since this required at least 4.0 to install, I believe that the bug will // not be present on devices running versions of Android earlier than 4.0. // //AND, it should not be present on versions of Android at 4.4 and higher, // since users will not "upgrade" to a version of Google Keyboard that // is LOWER than the one they got installed with their version of Android // in the first place, and the bug will have been fixed as of the 4.4 release. // // The above scope of the bug is reflected in the test below, which limits // the application of the workaround to Android versions between 4.0.x and 4.3.x. // //UPDATE: A popular third party keyboard was found that exhibits this same issue. It // was not fixed at the same time as the Google Play keyboard, and so the bug in that case // is still in place beyond API LEVEL 19. So, even though the Google Keyboard fixed this // as of level 19, we cannot take out the fix based on that version number. And so I''ve // removed the test for an upper limit on the version; the fix will remain in place ad // infinitum - but only when TYPE_NULL is used, so it *should* be harmless even when // the keyboard does not have the problem... if((Build.VERSION.SDK_INT >= 14) // && (Build.VERSION.SDK_INT < 19) && (beforeLength == 1 && afterLength == 0)) { //Send Backspace key down and up events to replace the ones omitted // by the LatinIME keyboard. return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) && super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); } else { //Really, I can''t see how this would be invoked, given that we''re using // TYPE_NULL, for non-buggy versions, but in order to limit the impact // of this change as much as possible (i.e., to versions at and above 4.0) // I am using the original behavior here for non-affected versions. return super.deleteSurroundingText(beforeLength, afterLength); } } }

A continuación, tome cada clase derivada de View que necesite para recibir eventos clave desde el teclado suave LatinIME, y edítelo de la siguiente manera:

Primero, cree una anulación a onCreateInputConnection () en la vista que recibirá los eventos clave de la siguiente manera:

@Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { //Passing FALSE as the SECOND ARGUMENT (fullEditor) to the constructor // will result in the key events continuing to be passed in to this // view. Use our special BaseInputConnection-derived view InputConnectionAccomodatingLatinIMETypeNullIssues baseInputConnection = new InputConnectionAccomodatingLatinIMETypeNullIssues(this, false); //In some cases an IME may be able to display an arbitrary label for a // command the user can perform, which you can specify here. A null value // here asks for the default for this key, which is usually something // like Done. outAttrs.actionLabel = null; //Special content type for when no explicit type has been specified. // This should be interpreted (by the IME that invoked // onCreateInputConnection())to mean that the target InputConnection // is not rich, it can not process and show things like candidate text // nor retrieve the current text, so the input method will need to run // in a limited "generate key events" mode. This disables the more // sophisticated kinds of editing that use a text buffer. outAttrs.inputType = InputType.TYPE_NULL; //This creates a Done key on the IME keyboard if you need one outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; return baseInputConnection; }

En segundo lugar, realice los siguientes cambios en su controlador onKey () para la vista:

this.setOnKeyListener(new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if(event.getAction() != KeyEvent.ACTION_DOWN) { //We only look at ACTION_DOWN in this code, assuming that ACTION_UP is redundant. // If not, adjust accordingly. return false; } else if(event.getUnicodeChar() == (int)EditableAccomodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER.charAt(0)) { //We are ignoring this character, and we want everyone else to ignore it, too, so // we return true indicating that we have handled it (by ignoring it). return true; } //Now, just do your event handling as usual... if(keyCode == KeyEvent.KEYCODE_ENTER) { //Trap the Done key and close the keyboard if it is pressed (if that''s what you want to do) InputMethodManager imm = (InputMethodManager) mainActivity.getSystemService(Context.INPUT_METHOD_SERVICE)); imm.hideSoftInputFromWindow(LetterRack.this.getWindowToken(), 0); return true; } else if(keyCode == KeyEvent.KEYCODE_DEL) { //Backspace key processing goes here... return true; } else if((keyCode >= KeyEvent.KEYCODE_A) && (keyCode <= KeyEvent.KEYCODE_Z)) { //(Or, use event.getUnicodeChar() if preferable to key codes). //Letter processing goes here... return true; } //Etc. } };

Finalmente, necesitamos definir una clase para nuestro editable que garantice que siempre haya al menos un carácter en nuestro buffer editable:

import android.text.SpannableStringBuilder; public class EditableAccomodatingLatinIMETypeNullIssues extends SpannableStringBuilder { EditableAccomodatingLatinIMETypeNullIssues(CharSequence source) { super(source); } //This character must be ignored by your onKey() code. public static CharSequence ONE_UNPROCESSED_CHARACTER = "/"; @Override public SpannableStringBuilder replace(final int spannableStringStart, final int spannableStringEnd, CharSequence replacementSequence, int replacementStart, int replacementEnd) { if (replacementEnd > replacementStart) { //In this case, there is something in the replacementSequence that the IME // is attempting to replace part of the editable with. //We don''t really care about whatever might already be in the editable; // we only care about making sure that SOMETHING ends up in it, // so that the backspace key will continue to work. // So, start by zeroing out whatever is there to begin with. super.replace(0, length(), "", 0, 0); //We DO care about preserving the new stuff that is replacing the stuff in the // editable, because this stuff might be sent to us as a keydown event. So, we // insert the new stuff (typically, a single character) into the now-empty editable, // and return the result to the caller. return super.replace(0, 0, replacementSequence, replacementStart, replacementEnd); } else if (spannableStringEnd > spannableStringStart) { //In this case, there is NOTHING in the replacementSequence, and something is // being replaced in the editable. // This is characteristic of a DELETION. // So, start by zeroing out whatever is being replaced in the editable. super.replace(0, length(), "", 0, 0); //And now, we will place our ONE_UNPROCESSED_CHARACTER into the editable buffer, and return it. return super.replace(0, 0, ONE_UNPROCESSED_CHARACTER, 0, 1); } // In this case, NOTHING is being replaced in the editable. This code assumes that there // is already something there. This assumption is probably OK because in our // InputConnectionAccomodatingLatinIMETypeNullIssues.getEditable() method // we PLACE a ONE_UNPROCESSED_CHARACTER into the newly-created buffer. So if there // is nothing replacing the identified part // of the editable, and no part of the editable that is being replaced, then we just // leave whatever is in the editable ALONE, // and we can be confident that there will be SOMETHING there. This call to super.replace() // in that case will be a no-op, except // for the value it returns. return super.replace(spannableStringStart, spannableStringEnd, replacementSequence, replacementStart, replacementEnd); } }

Eso completa los cambios de fuente que he encontrado parecen manejar ambos problemas.

NOTAS ADICIONALES :

El problema descrito en el Issue 42904 se introdujo en la versión LatinIME entregada con el nivel 16 de API. Anteriormente, se generaron eventos KEYCODE_DEL independientemente de si se utilizó TYPE_NULL. En el LatinIME lanzado con Jelly Bean, esta generación se suspendió, pero no se hizo ninguna excepción para TYPE_NULL, por lo que el comportamiento TYPE_NULL se deshabilitó efectivamente para las aplicaciones orientadas por encima del nivel 16 de API. Sin embargo, se agregó código de compatibilidad que permitió a las aplicaciones que tenían un targetSdkVersion <16 para continuar recibiendo eventos KEYCODE_DEL, incluso sin TYPE_NULL. Vea esta confirmación de AOSP en la línea 1493:

https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/android-4.1.1_r1/java/src/com/android/inputmethod/latin/LatinIME.java

Por lo tanto, podría solucionar este problema estableciendo targetSdkVersion en su aplicación a 15 o menos.

A partir del compromiso 4.4_r0.9 (justo antes de la versión 4.4), este problema se solucionó agregando una prueba para isTypeNull () a las condiciones que protegen la generación de KEYCODE_DEL. Desafortunadamente, un nuevo error (62306) se introdujo exactamente en ese punto, lo que provocó que la cláusula completa que envolvía la generación KEYCODE_DEL se omitiera si el usuario había tecleado retroceso tantas veces como había escrito otros caracteres. Esto llevó a una falla al generar KEYCODE_DEL bajo esas circunstancias, incluso con TYPE_NULL, e incluso con targetSdkVersion <= 15. Esto causó que las aplicaciones que previamente habían podido obtener el comportamiento KEYCODE_DEL correcto a través del código de compatibilidad (targetSdkVersion <= 15) experimentaran esto de repente. problema cuando los usuarios actualizaban sus copias de Google Keyboard (o realizaban una OTA que contenía una nueva versión de Google Keyboard). Vea este archivo AOSP git en la línea 2146 (la cláusula que incluye "NOT_A_CODE"):

https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/android-4.4_r0.9/java/src/com/android/inputmethod/latin/LatinIME.java

Este problema persiste en las versiones publicadas de Google Keyboard hasta el momento actual (1/7/2014). Se ha corregido en el repositorio, pero a partir de este momento no se ha publicado.

Esa confirmación inédita se puede encontrar aquí (la confirmación de git que contiene esto fusiona una confirmación titulada "Enviar retroceso como un evento cuando TYPE_NULL"), en la línea 2110 (se puede ver que la cláusula "NOT_A_CODE" impidió llegar a la cláusula genera KEYCODE_DEL ha sido eliminado):

https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/b41bea65502ce7339665859d3c2c81b4a29194e4/java/src/com/android/inputmethod/latin/LatinIME.java

Cuando se publique este arreglo, esa versión del teclado de Google ya no tendrá ninguno de estos dos problemas que afecten a TYPE_NULL. Sin embargo , todavía habrá versiones anteriores instaladas en dispositivos particulares para el futuro indefinido. Por lo tanto, el problema aún necesitará una solución. Eventualmente, a medida que más personas actualicen a un nivel más alto que el último que no incluye la solución, esta solución alternativa se necesitará cada vez menos. Pero ya tiene el alcance para eliminar por sí mismo (una vez que realice los cambios indicados para poner el límite final en el alcance, cuando la solución definitiva se haya liberado para que sepa qué es realmente).


(Esta respuesta se entiende como una adición a la respuesta aceptada publicada aquí por Carl).

Si bien apreciamos mucho la investigación y la comprensión de los dos errores, tuve algunos problemas con la solución provisional publicada aquí por Carl. El problema principal que tuve fue que, aunque el bloque de comentarios de Carl dice que la ruta KeyEvent.ACTION_MULTIPLE en onKey() solo se tomaría en "el primer evento recibido después de seleccionar el estante de cartas", para mí, cada evento clave tomó esa ruta . (Descubrí al BaseInputConnection.java código BaseInputConnection.java para API-level-18 que esto se debe a que todo el texto Editable se usa en sendCurrentText() cada vez. No estoy seguro de por qué funcionó para Carl pero no para mí).

Entonces, inspirado por la solución de Carl, lo adapté para no tener este problema. Mi solución al número 62306 (vinculada a la respuesta de Carl) trata de lograr el mismo efecto básico de "engañar" al IME para que piense que siempre hay más texto sobre el que se puede retroceder. Sin embargo, lo hace asegurándose de que el Editable tenga exactamente un personaje. Para hacerlo, debe extender la clase subyacente que implementa la interfaz Editable , SpannedStringBuilder , de una manera similar a la siguiente:

private class MyEditable extends SpannableStringBuilder { MyEditable(CharSequence source) { super(source); } @Override public SpannableStringBuilder replace(final int start, final int end, CharSequence tb, int tbstart, int tbend) { if (tbend > tbstart) { super.replace(0, length(), "", 0, 0); return super.replace(0, 0, tb, tbstart, tbend); } else if (end > start) { super.replace(0, length(), "", 0, 0); return super.replace(0, 0, DUMMY_CHAR, 0, 1); } return super.replace(start, end, tb, tbstart, tbend); } }

Básicamente, siempre que el IME intente agregar un carácter al Editable (llamando a replace() ), ese carácter reemplaza al carácter singleton que esté allí. Mientras tanto, si el IME intenta eliminar lo que hay allí, la replace() reemplaza lo que está allí con un carácter "ficticio" singleton (que debería ser algo que su aplicación ignorará) para mantener la longitud de 1.

Esto significa que las implementaciones de getEditable() y onKey() pueden ser un poco más simples que las que Carl publicó anteriormente. Por ejemplo, suponiendo que la clase MyEditable anterior se implementa como una clase interna, getEditable() pasa a ser algo así como:

@Override public Editable getEditable() { if (Build.VERSION.SDK_INT < 14) return super.getEditable(); if (mEditable == null) { mEditable = this.new MyEditable(DUMMY_CHAR); Selection.setSelection(mEditable, 1); } else if (m_editable.length() == 0) { mEditable.append(DUMMY_CHAR); Selection.setSelection(mEditable, 1); } return mEditable; }

Tenga en cuenta que con esta solución, no hay necesidad de mantener una cadena de 1024 caracteres de largo. Tampoco hay ningún peligro de "retroceder demasiado" (como se comenta en los comentarios de Carl sobre mantener presionada la tecla de retroceso).

Para completar, onKey() convierte en algo así como:

@Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() != KeyEvent.ACTION_DOWN) return false; if ((int)DUMMY_CHAR.charAt(0) == event.getUnicodeChar()) return true; // Handle event/keyCode here as normal... }

Finalmente, debería tener en cuenta que todo lo anterior se entiende como una solución para emitir 62306 únicamente . No tuve problemas con la solución al otro problema, 42904, publicado por Carl (anulando deleteSurroundingText() ) y recomendaría usarlo como lo publicó.


Debido a los pensamientos de @ Carl, llegué a una solución que funciona correctamente para cualquier tipo de entrada. A continuación doy una aplicación de muestra de trabajo completa que consta de 2 clases: MainActivity y CustomEditText :

package com.example.edittextbackspace; import android.app.Activity; import android.os.Bundle; import android.text.InputType; import android.view.ViewGroup.LayoutParams; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CustomEditText edittext = initEditText(); setContentView(edittext); } private CustomEditText initEditText() { CustomEditText editText = new CustomEditText(this) { @Override public void backSpaceProcessed() { super.backSpaceProcessed(); editTextBackSpaceProcessed(this); } }; editText.setInputType(InputType.TYPE_CLASS_NUMBER); editText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); editText.setText("1212"); return editText; } private void editTextBackSpaceProcessed(CustomEditText customEditText) { // Backspace event is called and properly processed } }

package com.example.edittextbackspace; import android.content.Context; import android.text.Editable; import android.text.Selection; import android.text.TextWatcher; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.EditText; import java.util.ArrayList; import java.util.List; public class CustomEditText extends EditText implements View.OnFocusChangeListener, TextWatcher { private String LOG = this.getClass().getName(); private int _inputType = 0; private int _imeOptions = 5 | EditorInfo.IME_FLAG_NO_EXTRACT_UI; private List<String> _lastComposingTextsList = new ArrayList<String>(); private BaseInputConnection _inputConnection = null; private String _lastComposingText = ""; private boolean _commitText = true; private int _lastCursorPosition = 0; private boolean _isComposing = false; private boolean _characterRemoved = false; private boolean _isTextComposable = false; public CustomEditText(Context context) { super(context); setOnFocusChangeListener(this); addTextChangedListener(this); } @Override public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { CustomEditText.this._inputConnection = new BaseInputConnection(this, false) { @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { handleEditTextDeleteEvent(); return super.deleteSurroundingText(beforeLength, afterLength); } @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { CustomEditText.this._isTextComposable = true; CustomEditText.this._lastCursorPosition = getSelectionEnd(); CustomEditText.this._isComposing = true; if (text.toString().equals(CustomEditText.this._lastComposingText)) return true; else CustomEditText.this._commitText = true; if (text.length() < CustomEditText.this._lastComposingText.length()) { CustomEditText.this._lastComposingText = text.toString(); try { if (text.length() > 0) { if (CustomEditText.this._lastComposingTextsList.size() > 0) { if (CustomEditText.this._lastComposingTextsList.size() > 0) { CustomEditText.this._lastComposingTextsList.remove(CustomEditText.this._lastComposingTextsList.size() - 1); } } else { CustomEditText.this._lastComposingTextsList.add(text.toString().substring(0, text.length() - 1)); } } int start = Math.max(getSelectionStart(), 0) - 1; int end = Math.max(getSelectionEnd(), 0); CustomEditText.this._characterRemoved = true; getText().replace(Math.min(start, end), Math.max(start, end), ""); } catch (Exception e) { Log.e(LOG, "Exception in setComposingText: " + e.toString()); } return true; } else { CustomEditText.this._characterRemoved = false; } if (text.length() > 0) { CustomEditText.this._lastComposingText = text.toString(); String textToInsert = Character.toString(text.charAt(text.length() - 1)); int start = Math.max(getSelectionStart(), 0); int end = Math.max(getSelectionEnd(), 0); CustomEditText.this._lastCursorPosition++; getText().replace(Math.min(start, end), Math.max(start, end), textToInsert); CustomEditText.this._lastComposingTextsList.add(text.toString()); } return super.setComposingText("", newCursorPosition); } @Override public boolean commitText(CharSequence text, int newCursorPosition) { CustomEditText.this._isComposing = false; CustomEditText.this._lastComposingText = ""; if (!CustomEditText.this._commitText) { CustomEditText.this._lastComposingTextsList.clear(); return true; } if (text.toString().length() > 0) { try { String stringToReplace = ""; int cursorPosition = Math.max(getSelectionStart(), 0); if (CustomEditText.this._lastComposingTextsList.size() > 1) { if (text.toString().trim().isEmpty()) { getText().replace(cursorPosition, cursorPosition, " "); } else { stringToReplace = CustomEditText.this._lastComposingTextsList.get(CustomEditText.this._lastComposingTextsList.size() - 2) + text.charAt(text.length() - 1); getText().replace(cursorPosition - stringToReplace.length(), cursorPosition, text); } CustomEditText.this._lastComposingTextsList.clear(); return true; } else if (CustomEditText.this._lastComposingTextsList.size() == 1) { getText().replace(cursorPosition - 1, cursorPosition, text); CustomEditText.this._lastComposingTextsList.clear(); return true; } } catch (Exception e) { Log.e(LOG, "Exception in commitText: " + e.toString()); } } else { if (!getText().toString().isEmpty()) { int cursorPosition = Math.max(getSelectionStart(), 0); CustomEditText.this._lastCursorPosition = cursorPosition - 1; getText().replace(cursorPosition - 1, cursorPosition, text); if (CustomEditText.this._lastComposingTextsList.size() > 0) { CustomEditText.this._lastComposingTextsList.remove(CustomEditText.this._lastComposingTextsList.size() - 1); } return true; } } return super.commitText(text, newCursorPosition); } @Override public boolean sendKeyEvent(KeyEvent event) { int keyCode = event.getKeyCode(); CustomEditText.this._lastComposingTextsList.clear(); if (keyCode > 60 && keyCode < 68 || !CustomEditText.this._isTextComposable || (CustomEditText.this._lastComposingTextsList != null && CustomEditText.this._lastComposingTextsList.size() == 0)) { return super.sendKeyEvent(event); } else return false; } @Override public boolean finishComposingText() { if (CustomEditText.this._lastComposingTextsList != null && CustomEditText.this._lastComposingTextsList.size() > 0) CustomEditText.this._lastComposingTextsList.clear(); CustomEditText.this._isComposing = true; CustomEditText.this._commitText = true; return super.finishComposingText(); } @Override public boolean commitCorrection(CorrectionInfo correctionInfo) { CustomEditText.this._commitText = false; return super.commitCorrection(correctionInfo); } }; outAttrs.actionLabel = null; outAttrs.inputType = this._inputType; outAttrs.imeOptions = this._imeOptions; return CustomEditText.this._inputConnection; } @Override public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { if (keyCode == KeyEvent.KEYCODE_DEL) { int cursorPosition = this.getSelectionEnd() - 1; if (cursorPosition < 0) { removeAll(); } } return super.onKeyDown(keyCode, keyEvent); } @Override public void setInputType(int type) { CustomEditText.this._isTextComposable = false; this._inputType = type; super.setInputType(type); } @Override public void setImeOptions(int imeOptions) { this._imeOptions = imeOptions | EditorInfo.IME_FLAG_NO_EXTRACT_UI; super.setImeOptions(this._imeOptions); } public void handleEditTextDeleteEvent() { int end = Math.max(getSelectionEnd(), 0); if (end - 1 >= 0) { removeChar(); backSpaceProcessed(); } else { removeAll(); } } private void removeAll() { int startSelection = this.getSelectionStart(); int endSelection = this.getSelectionEnd(); if (endSelection - startSelection > 0) this.setText(""); else nothingRemoved(); } private void removeChar() { KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); super.onKeyDown(event.getKeyCode(), event); } public void nothingRemoved() { // Backspace didn''t remove anything. It means, a cursor of the editText was in the first position. We can use this method, for example, to switch focus to a previous view } public void backSpaceProcessed() { // Backspace is properly processed } @Override protected void onSelectionChanged(int selStart, int selEnd) { if (CustomEditText.this._isComposing) { int startSelection = this.getSelectionStart(); int endSelection = this.getSelectionEnd(); if (((CustomEditText.this._lastCursorPosition != selEnd && !CustomEditText.this._characterRemoved) || (!CustomEditText.this._characterRemoved && CustomEditText.this._lastCursorPosition != selEnd)) || Math.abs(CustomEditText.this._lastCursorPosition - selEnd) > 1 || Math.abs(endSelection - startSelection) > 1) { // clean autoprediction words CustomEditText.this._lastComposingText = ""; CustomEditText.this._lastComposingTextsList.clear(); CustomEditText.super.setInputType(CustomEditText.this._inputType); } } } @Override public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { CustomEditText.this._lastComposingText = ""; CustomEditText.this._lastComposingTextsList.clear(); } } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { int startSelection = getSelectionStart(); int endSelection = getSelectionEnd(); if (Math.abs(endSelection - startSelection) > 0) { Selection.setSelection(getText(), endSelection); } } @Override public void afterTextChanged(Editable s) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { super.onTextChanged(s, start, before, count); } }

ACTUALIZACIÓN: Actualicé el código, porque no funcionaba correctamente cuando la Predicción de texto está habilitada en algunos dispositivos como Samsung Galaxy S6 (gracias a @Jonas que informó sobre este problema en el comentario a continuación) y el uso de InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS no ayuda en este caso. Probé esta solución en muchos dispositivos, pero todavía no estoy seguro de si funciona correctamente para todos. Espero que obtenga algunas críticas de usted en caso de cualquier comportamiento incorrecto de EditText.


Me he enfrentado a problemas similares donde KEYCODE_DEL no se recibía al tocar la tecla de retroceso. Depende del teclado de entrada suave, creo, porque mi problema solo estaba ocurriendo en el caso de algunos teclados de terceros (Swype, creo) y no con el teclado predeterminado de Google.



This is old post and giving my suggestions in case somebody is in need of super quick hack/implementation.

The simplest work around I came out with is to implement TextWatcher too along with on OnKeyListener and in onTextChanged compare with the previous existing string whether it is reduced by one character.

The benefit of this is it works on any type of keyboard with no long coding process easily.

For instance my editText holds only one character, so I compared characterSequence if it is empty string, then by that we can acknowledge that Delete key is pressed.

Below is the code explaining the same:

@Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { if(charSequence.toString().equals("")) //Compare here for any change in existing string by single character with previous string { //Carry out your tasks here it comes in here when Delete Key is pressed. } }

Note: In this case my edittext contains only single character so I''m comparing charSequesnce with empty string(since pressing delete will make it empty), for your needs you need to modify it and compare(Like after pressing key substring is part of the original string) it with existing string. Espero eso ayude.


INTRODUCCIÓN:

Después de probar las soluciones de @ Carl''s y @ Turix, noté que:

  1. La solución de Carl no funciona bien con caracteres Unicode o secuencias de caracteres, ya que parece que se entregan con el evento ACTION_MULTIPLE, lo que hace que sea difícil distinguir entre los caracteres "ficticios" y el personaje real.

  2. No pude obtener deleteSurroundingText funcionando en la última versión de Android en mi Nexus 5 (4.4.2). Probé la orientación de varias versiones de SDK diferentes, pero ninguna de ellas funcionó. Tal vez Google decidió volver a cambiar la lógica detrás de la tecla DEL ...

Por lo tanto, he encontrado la siguiente solución combinada, usando las respuestas de Carl y Turix. Mi solución funciona combinando la idea de Carl de un prefijo largo de carácter ficticio para hacer que funcione el DEL, pero usando la solución de Turix para un Editable personalizado para generar los eventos clave apropiados.

RESULTADOS:

He probado esta solución en varios dispositivos con diferentes versiones de Android y diferentes teclados. Todos los casos de prueba a continuación funcionan para mí. No he encontrado un caso donde esta solución no funciona.

  • Nexus 5 (4.4.2) con teclado estándar de Google
  • Nexus 5 (4.4.2) con SwiftKey
  • HTC One (4.2.2) con teclado estándar HTC
  • Nexus One (2.3.6) con teclado estándar de Google
  • Samsung Galaxy S3 (4.1.2) con teclado Samsung estándar

También probé la orientación de diferentes versiones de SDK:

  • Objetivo 16
  • Objetivo 19

Si esta solución también funciona para usted, entonces

LA VISTA:

public class MyInputView extends EditText implements View.OnKeyListener { private String DUMMY; ... public MyInputView(Context context) { super(context); init(context); } private void init(Context context) { this.context = context; this.setOnKeyListener(this); // Generate a dummy buffer string // Make longer or shorter as desired. DUMMY = ""; for (int i = 0; i < 1000; i++) DUMMY += "/0"; } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { MyInputConnection ic = new MyInputConnection(this, false); outAttrs.inputType = InputType.TYPE_NULL; return ic; } @Override public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { int action = keyEvent.getAction(); // Catch unicode characters (even character sequeneces) // But make sure we aren''t catching the dummy buffer. if (action == KeyEvent.ACTION_MULTIPLE) { String s = keyEvent.getCharacters(); if (!s.equals(DUMMY)) { listener.onSend(s); } } // Catch key presses... if (action == KeyEvent.ACTION_DOWN) { switch (keyCode) { case KeyEvent.KEYCODE_DEL: ... break; case KeyEvent.KEYCODE_ENTER: ... break; case KeyEvent.KEYCODE_TAB: ... break; default: char ch = (char)keyEvent.getUnicodeChar(); if (ch != ''/0'') { ... } break; } } return false; } }

LA CONEXIÓN DE ENTRADA:

public class MyInputConnection extends BaseInputConnection { private MyEditable mEditable; public MyInputConnection(View targetView, boolean fullEditor) { super(targetView, fullEditor); } private class MyEditable extends SpannableStringBuilder { MyEditable(CharSequence source) { super(source); } @Override public SpannableStringBuilder replace(final int start, final int end, CharSequence tb, int tbstart, int tbend) { if (tbend > tbstart) { super.replace(0, length(), "", 0, 0); return super.replace(0, 0, tb, tbstart, tbend); } else if (end > start) { super.replace(0, length(), "", 0, 0); return super.replace(0, 0, DUMMY, 0, DUMMY.length()); } return super.replace(start, end, tb, tbstart, tbend); } } @Override public Editable getEditable() { if (Build.VERSION.SDK_INT < 14) return super.getEditable(); if (mEditable == null) { mEditable = this.new MyEditable(DUMMY); Selection.setSelection(mEditable, DUMMY.length()); } else if (mEditable.length() == 0) { mEditable.append(DUMMY); Selection.setSelection(mEditable, DUMMY.length()); } return mEditable; } @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { // Not called in latest Android version... return super.deleteSurroundingText(beforeLength, afterLength); } }



I think you may find that you can intercept the key if you override the dispatchKeyEvent method of the appropriate view/activity (in my case, the main activity was fine).

For example I''m developing an app for a device which has hardware scroll keys, and I was surprised to discover the onKeyUp / onKeyDown methods never get called for them. Instead, by default the key press goes through a bunch of dispatchKeyEvent s until it invokes a scroll method somewhere (in my case, bizarrely enough, one key press invokes a scroll methods on each of two separate scrollable views--how annoying).


InputFilter called for backspace and if edittext is empty.

editText.setFilters(new InputFilter[]{new InputFilter() { @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { if(source.equals("")) { //a backspace was entered } return source; } }});