我正在重写视图(OpenGL Surface View)的onKeyDown方法来捕获所有按键。问题是在几个设备上,KEYCODE_DEL键没有被捕获。我尝试将onKeyListener添加到视图中,它可以捕获除退格键之外的所有内容。
必须有一种方法来监听此按键事件,但如何实现呢?
2014年11月12日更新:更改修复范围,不仅限于API级别19以下,因为第三方键盘在19以上仍存在此bug。
2014年1月9日更新:我已经设计了一种方法,并附上代码,来解决所有Google键盘(LatinIME)KEYCODE_DEL问题,特别是42904和62306问题。
Turix的答案中的增强功能已经得到了我的允许,并已融入到我的代码中。 Turix的改进通过找到一种递增的方式来确保可编辑缓冲区中始终只有一个字符,从而消除了将垃圾字符注入可编辑缓冲区的需要。
我在一个部署的应用程序中使用了(类似的)代码,您可以测试一下:
https://play.google.com/store/apps/details?id=com.goalstate.WordGames.FullBoard.trialsuite]
介绍:
以下解决方法旨在解决所有版本的Google键盘中存在的这两个错误,无需将应用程序保留在API级别15或以下,因为一些应用程序已经限制自己以利用绕过问题42904的兼容性代码。这些问题仅作为错误出现在已实现onCreateInputConnection()覆盖并将TYPE_NULL返回到调用IME的视图上(通过IME传递给该方法的EditorInfo参数的inputType成员)。只有这样,视图才能合理地期望从软键盘返回按键事件(包括KEYCODE_DEL)。因此,此处提供的解决方法需要TYPE_NULL InputType。如果停止使用TYPE_NULL是一个选项,那么我建议您采用使用InputConnection重写方法而不是按键事件的推荐方法(或者更简单地使用从EditText派生的类,它会为您完成这些操作)。
尽管如此,TYPE_NULL行为并未被正式停止使用,因此LatinIME在某些情况下无法生成KEYCODE_DEL事件的故障确实是一个错误。我提供以下解决方法来解决这个问题。
概述:
应用程序无法从LatinIME接收KEYCODE_DEL的问题是由两个已知的错误引起的,如此处所报告的:
https://code.google.com/p/android/issues/detail?id=42904 (被列为“按预期工作”,但问题在于,它导致不支持针对API级别16及以上的应用程序生成KEYCODE_DEL事件,这是一个错误,需要修复,特别是对于已经指定了TYPE_NULL输入类型的应用程序。该问题已在最新版本的LatinIME中修复,但过去发布的版本仍然存在此错误,因此使用TYPE_NULL并针对API级别16或更高版本的应用程序仍需要一种可以在应用程序内执行的解决方法。
还有这里:
http://code.google.com/p/android/issues/detail?id=62306 (目前被列为已修复但尚未发布-将来发布-但即使它被发布,我们仍然需要一种可以在应用程序内执行的解决方法来处理仍然存在“野外”的过去发布版本。)
根据这个论点(即KEYCODE_DEL事件的问题是由于LatinIME中的错误导致的),我发现当使用外部硬件键盘以及第三方SwiftKey软键盘时,这些问题不会出现,而它们在特定版本的LatinIME中确实会出现。其中一个问题或另一个问题(但不会同时出现)存在于某些LatinIME版本中。因此,开发人员很难在测试期间知道他们是否已解决了所有的KEYCODE_DEL问题,并且有时当进行Android(或Google Keyboard)更新时,在测试中无法再次复现问题。尽管如此,引起问题的LatinIME版本将存在于许多设备中。这迫使我深入研究AOSP LatinIME git repo,以确定每个问题的确切范围(即可能存在每个问题的特定LatinIME和Android版本)。下面的解决方法代码已限制为这些特定版本。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);
}
}
}
@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. } };
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);
}
}
我找到了似乎能够解决这两个问题的源代码更改。
附加说明:
问题42904所描述的问题是在API级别16中提供的LatinIME版本中引入的。在此之前,无论是否使用TYPE_NULL,都会生成KEYCODE_DEL事件。在Jelly Bean发布的LatinIME中,停止了此生成,但没有为TYPE_NULL做出例外,因此针对API级别16以上的应用程序实际上禁用了TYPE_NULL行为。然而,添加了兼容性代码,允许目标SdkVersion<16的应用程序即使没有TYPE_NULL也继续接收KEYCODE_DEL事件。请参见此AOSP提交第1493行:
因此,您可以通过将应用中的targetSdkVersion设置为15或更低来解决此问题。看起来是 Android 的一个 bug:
问题 42904:
在 SDK 16 及以上版本中,KEYCODE_DEL 事件无法传递到 EditText
。
介绍:
在测试了@Carl和@Turix的两种解决方案后,我注意到以下问题:
@Carl的解决方案不适用于unicode字符或字符序列,因为它们似乎是通过ACTION_MULTIPLE事件传递的,这使得区分“虚拟”字符和实际字符变得困难。
我无法在我的Nexus 5(4.4.2)上使用deleteSurroundingText功能,即使我尝试针对几个不同的sdk版本进行测试,但都没有起作用。也许Google又一次改变了DEL键背后的逻辑...
因此,我提出了以下结合方案,使用了Carl的长虚拟字符前缀的想法来使DEL键正常工作,但使用了Turix的自定义Editable解决方案来生成正确的按键事件。
结果:
我已经在多个设备上测试了这个解决方案,其中包括不同版本的Android和不同的键盘。所有以下测试用例对我都有效。我没有发现这个解决方案不起作用的情况。
我还测试了不同的sdk版本:
如果这个解决方案对您也有效,请
视图:
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;
}
}
输入连接:
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);
}
}
(这个答案是对Carl发布的已接受答案的补充。)
虽然我非常感激对这两个漏洞的研究和理解,但我对Carl在此处发布的解决方法有些困扰。我主要遇到的问题是,尽管Carl的注释块说onKey()
中的KeyEvent.ACTION_MULTIPLE
路径只会在“选择字母架后接收到的第一个事件”上执行,但我每次都需要走这个路径。(通过查看API级别18的BaseInputConnection.java
代码,我发现这是因为每次都在sendCurrentText()
中使用整个Editable
文本。我不确定为什么它适用于Carl但是对我无效。)
因此,受到Carl的解决方案的启发,我改进了它以解决这个问题。我的解决方案针对问题62306(链接在Carl的答案中)试图实现与“欺骗”IME以使其认为始终有更多可以回退的文本相同的基本效果。但是,它通过确保可编辑内容中恰好有一个字符来实现这一点。为此,您需要以类似以下方式扩展实现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);
}
}
@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;
}
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...
}
SpannableStringBuilder
的各种调用都会通过我重写的 replace()
方法进行处理。(我通过查看源代码来确认这一点,尽管没有任何保证。) 但是,我不确定其他版本是否也是如此(或将来是否如此)。祝好运! - Turix出于@Carl的思考,我得出了一种适用于任何输入类型的解决方案。以下是由两个类MainActivity
和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);
}
}
更新: 我更新了代码,因为在某些设备上启用文本预测(例如三星Galaxy S6)时它无法正常工作(感谢@Jonas在下面的评论中提供了这个问题),使用InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS在这种情况下也无效。我在许多设备上测试了这个解决方案,但仍不确定它是否适用于所有设备。如果EditText有任何不当行为,请您给我一些反馈。
我曾经遇到过类似的问题,即按下退格键时无法接收到KEYCODE_DEL。我认为这取决于软输入键盘,因为我的问题只在某些第三方键盘(比如Swype)中出现,而默认的Google键盘则没有问题。
如果您重写适当视图/活动的dispatchKeyEvent
方法(在我的情况下,主要活动很好),则可以拦截按键。
例如,我正在为具有硬件滚动键的设备开发应用程序,并惊讶地发现onKeyUp
/onKeyDown
方法从未针对它们调用。相反,默认情况下,按键通过一堆dispatchKeyEvent
直到调用某个滚动方法(在我的情况下,令人困惑的是,一个按键会在两个可滚动视图中各调用一个滚动方法-非常烦人)。
dispatchKeyEventPreIme
,但我怀疑那不是它...不过尝试一下也无妨。 - Merk如果您检查了退格字符的十进制数字会怎样呢?
我认为它是像'/r'(十进制数7)之类的东西,至少对于ASCII码来说是这样。
编辑: 我猜Android使用UTF-8,所以这个十进制数应该是8。 http://www.fileformat.info/info/unicode/char/0008/index.htm
考虑到Umair的回复,您可以在此应用一个解决方法:
捕获一个触摸事件,它不是键盘事件,并且发生在屏幕右下角,同时键盘显示。
希望这能帮到您。
当按下退格键且编辑框为空时,调用InputFilter。
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;
}
}});