Android – 无法捕捉退格/删除软按。 键盘

我重写视图的onKeyDown方法(openGL表面视图)以捕获所有按键。 问题是在几个设备上KEYCODE_DEL没有被捕获。 我已经尝试添加一个onKeyListener到视图,并捕获除了退格键之外的所有东西。

必须有一种方法来听取这个关键的新闻事件,但如何?

11/12/2014更新:修改的范围不限于<API级别19,因为在第三方键盘仍然有超过19的错误。

2014年1月9日更新:我已经devise了一种方法,使用代码来解决所有Google键盘(LatinIME)KEYCODE_DEL问题,特别是问题42904和62306。

图里克斯答案的提高已被许可纳入我自己的代码中 。 Turix的改进删除需要注入垃圾字符到可编辑缓冲区,而不是寻找一种增量的方式,以确保一个字符总是在该缓冲区。

我已经在部署的应用程序中使用(类似)代码,欢迎您testing:
https://play.google.com/store/apps/details?id=com.goalstate.WordGames.FullBoard.trialsuite%5D

介绍:

下面介绍的解决方法旨在适用于所有版本的Google键盘,无论是过去的还是未来的,就这两个错误而言。 此解决方法不要求应用程序继续停留在API级别15或更低级别,这些应用程序为了利用兼容性问题而受到限制,这些代码绕过了问题42904。

这些问题仅作为实现了onCreateInputConnection()重写的视图的错误呈现,并且返回TYPE_NULL到调用的IME(在由IME传递给该方法的EditorInfo参数的inputType成员中)。 只有通过这样做,视图才能合理预期键盘事件(包括KEYCODE_DEL)将从软键盘返回。 因此,这里介绍的解决方法需要TYPE_NULL InputType。

对于不使用TYPE_NULL的应用程序,来自onCreateInputConnection()覆盖的由视图返回的BaseInputConnection派生的对象中存在各种覆盖,当用户执行编辑时,由IME调用, 而不是 IME生成密钥事件。 这种(非TYPE_NULL)方法通常比较好,因为软键盘的function现在已经远远超过了按键的敲击,还有声音input,完成等等。关键事件是一种较老的方法,那些在Google实施LatinIME的人说过他们希望看到使用TYPE_NULL(和关键事件)消失。

如果停止使用TYPE_NULL是一个选项,那么我会敦促你继续使用InputConnection重写方法而不是关键事件(或者更简单地说,通过使用从EditText派生的类来完成这个操作)。

尽pipe如此,TYPE_NULL行为还没有正式停止,因此在某些情况下,LatinIME不能生成KEYCODE_DEL事件确实是一个错误。 我提供了以下解决方法来解决此问题。

概述:

应用程序在从LatinIME接收KEYCODE_DEL时遇到的问题是由于两个已知的错误,如下所示:

https://code.google.com/p/android/issues/detail?id=42904 (列为WorkingAsIntended,但问题是,我维护,一个错误,因为它导致无法支持KEYCODE_DEL事件生成的应用程序定位API等级16及以上版本已经明确列出了TYPE_NULL的InputType,这个问题已经在最新版本的LatinIME中得到修复,但是过去的版本仍然存在这个bug,所以应用程序使用TYPE_NULL和目标API Level 16或者上面仍然需要一个可以在应用程序内执行的解决方法。

这里

http://code.google.com/p/android/issues/detail?id=62306 (目前列为固定但尚未发布 – FutureRelease – 但即使一旦发布,我们仍然需要一个可以执行的解决方法从应用程序内部处理过去的版本,将持续“在野外”)。

与本论文(即KEYCODE_DEL事件遇到的问题是由于LatinIME中的错误)相一致,我发现使用外部硬件键盘时以及使用第三方SwiftKey软键盘时,不会发生这些问题, 确实发生在特定版本的LatinIME中。

这些问题中的一个或另一个(但不是同时出现)在一些LatinIME版本中出现。 因此,开发人员在testing过程中很难知道是否已经解决了所有KEYCODE_DEL问题,有时当执行Android(或Google键盘)更新时,testing中将不再有问题。 尽pipe如此,导致此问题的LatinIME版本将出现在许多正在使用的设备上。 这迫使我深入研究AOSP LatinIME git repo,以确定两个问题(即,具体的LatinIME和Android,这两个问题可能存在的每个版本)的确切范围。 下面的解决方法代码已被限制在那些特定的版本。

下面介绍的解决方法代码包含了大量的注释,可以帮助您理解它正在尝试完成的内容。 在演示代码之后,我将提供一些额外的讨论,其中将包括特定的Android开源项目(AOSP)提交,在这两个提交中引入了这两个错误中的每一个,并在其中消失,包括受影响的Google键盘版本。

我会警告任何人想使用这种方法来执行自己的testing,以validation它适用于他们的特定应用程序。 我认为它一般可以工作,并且已经在很多设备和LatinIME版本上进行了testing,但是推理很复杂,所以请谨慎行事。 如果您发现任何问题,请在下面发表评论。

CODE

在这里,我的解决方法是解决这两个问题,并在代码的注释中join一个解释:

首先,在您的应用程序中包含以下类(经过编辑尝试),在其自己的源文件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 (eg, 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 (ie, to versions at and above 4.0) // I am using the original behavior here for non-affected versions. return super.deleteSurroundingText(beforeLength, afterLength); } } } 

接下来,需要从LatinIME软键盘接收每个需要接收按键事件的View派生类,并按如下方式进行编辑:

首先,在要接收关键事件的视图中创build一个覆盖到onCreateInputConnection(),如下所示:

  @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; } 

其次,对视图的onKey()处理程序进行以下更改:

  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. } }; 

最后,我们需要为我们的可编辑定义一个类,以确保我们的可编辑缓冲区中至less有一个字符:

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

这完成了我所发现的源代码更改似乎处理这两个问题。

附加说明

Issue 42904中描述的问题是在API级别为16的LatinIME版本中引入的。在此之前,无论是否使用TYPE_NULL,都会生成KEYCODE_DEL事件。 在Jelly Bean发布的LatinIME中,这一代已经停产了,但TYPE_NULL并没有例外,因此针对API级别16以上的应用程序有效地禁用了TYPE_NULL行为。但是,添加了兼容性代码,允许具有targetSdkVersion <16即使没有TYPE_NULL也会继续接收KEYCODE_DEL事件。 请参阅1493行的AOSP提交:

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

因此,您可以通过在您的应用中将targetSdkVersion设置为15或更低来解决此问题。

从提交4.4_r0.9开始(在4.4发行版之前),通过为守护KEYCODE_DEL生成的条件添加对isTypeNull()的testing来解决此问题。 不幸的是,在这一点上引入了一个新的错误(62306),导致整个子句封装KEYCODE_DEL生成被跳过,如果用户键入了退格键的次数,她已经键入其他字符。 这导致在这种情况下无法生成KEYCODE_DEL,即使使用TYPE_NULL,甚至使用targetSdkVersion <= 15。这导致以前能够通过兼容性代码(targetSdkVersion <= 15)获得正确的KEYCODE_DEL行为的应用程序突然遇到此问题用户升级了Google键盘的拷贝(或者执行了包含新版Google键盘的OTA)时出现问题。 在2146行(包括“NOT_A_CODE”的子句)看到这个AOSP git文件:

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

这个问题在Google键盘的发布版本中一直持续到现在(1/7/2014)。 它已经在回购中得到了修复,但截至本文撰写尚未公布。

这个未发布的提交可以在这里find(包含它的git提交合并了一个标题为“发送退格作为TYPE_NULL时的事件”的提交),在2110行(你可以看到用来阻止我们到达子句的“NOT_A_CODE”生成KEYCODE_DEL已被删除):

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

当此修复程序发布时,该版本的Google键盘将不再有影响TYPE_NULL的这两个问题之一。 但是 ,在特定的设备上仍然会安装较旧的版本。 因此,问题仍然需要解决方法。 最终,随着更多人升级到最后一个不包括修复程序的人,这个解决方法将会越来越less。 但是它已经被限定范围了(一旦你做了修改,把最后的限制放在范围上,当最后的修正被实际释放后,你就知道它是什么了)。

看起来像一个Android的错误:

问题42904 :KEYCODE_DEL事件未传递到SDK 16及更高版本中的EditText

发行42904 @ code.google.com

介绍:

在testing@ Carl's和@ Turix的解决scheme之后,我注意到:

  1. 卡尔的解决scheme不适用于Unicode字符或字符序列,因为这些似乎与ACTION_MULTIPLE事件交付,这使得难以区分“虚拟”字符和实际字符。

  2. 我无法在Nexus 5(4.4.2)的Android版本中使用最新版本的deleteSurroundingText 。 我testing了几个不同的sdk版本,但都没有工作。 也许Google决定再次改变DEL键背后的逻辑…

因此,我使用了Carl和Turix的答案,提出了以下综合解决scheme。 我的解决scheme通过结合Carl的长虚拟字符前缀的思想来使DEL工作,但使用Turix的解决scheme来定制Editable来生成适当的关键事件。

结果:

我已经在具有不同版本的Android和不同键盘的多个设备上testing过这个解决scheme。 以下所有的testing案例都适用于我。 我还没有find这种解决scheme无法正常工作的情况。

  • 带标准Google键盘的Nexus 5(4.4.2)
  • Nexus 5(4.4.2)与SwiftKey
  • HTC One(4.2.2)与标准的HTC键盘
  • 带标准Google键盘的Nexus One(2.3.6)
  • 三星Galaxy S3(4.1.2)与标准三星键盘

我也testing了针对不同的SDK版本:

  • 目标16
  • 目标19

如果这个解决scheme也适用于你,那么ple

风景:

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

input连接:

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

我遇到过类似的问题,KEYCODE_DEL没有被退格键所接收。 这取决于软input键盘我认为,因为我的问题只发生在一些第三方键盘(swype我认为)的情况下,而不是默认的谷歌键盘。

(这个答案是卡尔在这里公布的答案的附录。)

在对这两个bug的研究和理解非常赞赏的同时,我在Carl发表的这个解决方法中遇到了一些麻烦。 我的主要问题是,虽然卡尔的评论块说onKey()中的KeyEvent.ACTION_MULTIPLEpath只能在“select信箱后收到的第一个事件”上进行,对于我来说,每一个关键事件都采用了这个path。 (我通过查看API-level-18的BaseInputConnection.java代码发现,这是因为每次都在sendCurrentText()使用整个Editable文本,我不知道为什么它对Carl有效,但不是我)。

所以,在Carl的解决scheme的启发下,我调整了它没有这个问题。 我的问题62306的解决scheme(与卡尔的答案相关)试图达到“欺骗”input法的基本效果,认为总是有更多的文本可以被反过来。 但是,通过确保Editable中只有一个字符来实现这一点。 为此,您需要扩展实现Editable接口SpannedStringBuilder的基础类,类似于以下内容:

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

基本上,只要IME试图向Editable添加一个字符(通过调用replace() ),该字符就会replace任何单独的字符。 与此同时,如果IME试图删除那里的内容,那么replace()覆盖代替了单元素“虚拟”字符(应该是您的应用程序将忽略的),以保持1的长度。

这意味着getEditable()onKey()可能比Carl上面提到的稍微简单一些。 例如,假设上面的MyEditable类是作为一个内部类实现的, getEditable()就像这样:

  @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; } 

请注意,使用此解决scheme,不需要维护1024个字符的string。 也不存在“退格太多”的危险(正如Carl关于按下退格键的评论所述)。

为了完整性, onKey()变得像这样:

  @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... } 

最后,我应该注意到,上述所有内容仅仅是为了解决62306问题。 对于Carl发布的另一个问题(42904)的解决scheme(覆盖deleteSurroundingText() ),我没有任何问题,并build议在发布时使用它。

由于@卡尔的想法,我来到一个解决scheme,适用于任何inputtypes。 下面我给出一个完整的工作示例应用程序,由两个类组成: MainActivityCustomEditText

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

更新:我更新了代码,因为在Samsung Galaxy S6(谢谢@Jonas,他在下面的评论中告知这个问题),并且使用InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS在某些设备上启用文本预测function时无法正常工作这个案例。 我在很多设备上testing过这个解决scheme,但是仍然不确定它是否能正常工作。 如果EditText出现任何不正确的行为,我希望我会收到一些评论。

我认为你可能会发现,如果你重写适当的视图/活动的dispatchKeyEvent方法(在我的情况下,主要活动是罚款),你可以拦截的关键。

例如,我正在开发一个具有硬件滚动键的设备的应用程序,我惊讶地发现onKeyUp / onKeyDown方法永远不会被调用。 相反,默认情况下,按键会经历一堆dispatchKeyEvent直到在某处调用滚动方法(在我的例子中,足够奇怪的是,一个按键在两个单独的可滚动视图中的每一个上调用滚动方法 – 多么讨厌)。

如果你检查退格字符的十进制数字怎么办?

我认为它像'/ r'(十进制数字7)什么的,至less对于ASCII。

编辑:我猜Android使用UTF-8,所以这个十进制数将是8. http://www.fileformat.info/info/unicode/char/0008/index.htm

InputFilter调用backspace,如果edittext是空的。

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

Given the response by Umair you may consider applying a workaround here:

Capture a touch event that it's NOT a key event and happens around the lower-right part of the screen while the keyboard is shown.

How to get the Touch position in android?

有没有办法判断是否显示软键盘?

希望有所帮助

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. 希望它有帮助。