安卓-无法捕获软键盘中的删除/退格按键。

38

我正在重写视图(OpenGL Surface View)的onKeyDown方法来捕获所有按键。问题是在几个设备上,KEYCODE_DEL键没有被捕获。我尝试将onKeyListener添加到视图中,它可以捕获除退格键之外的所有内容。

必须有一种方法来监听此按键事件,但如何实现呢?

11个回答

58

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的应用程序,视图从其onCreateInputConnection()重写返回的BaseInputConnection派生对象中有各种覆盖,当用户执行编辑时,IME将调用这些覆盖,而不是IME生成按键事件。这种(非TYPE_NULL)方法通常更好,因为软键盘的功能现在远不止简单的按键输入,还包括语音输入、完成等等。按键事件是一种较旧的方法,Google实施LatinIME的人员表示,他们希望看到TYPE_NULL(和按键事件)的使用消失。

如果停止使用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版本)。下面的解决方法代码已限制为这些特定版本。
下面呈现的解决代码包括广泛的注释,这些注释应该有助于您理解它试图完成的任务。在介绍代码后,我将提供一些附加讨论,其中将包括两个漏洞引入的特定Android开源项目(AOSP)提交以及它消失的时间,以及可能包含受影响的Google键盘版本的Android版本。
我要警告任何考虑使用这种方法来执行自己的测试以验证它是否适用于他们特定应用程序的人。我认为它通常会起作用,并在许多设备和LatinIME版本上进行了测试,但推理很复杂,所以请谨慎操作。如果您发现任何问题,请在下面发表评论。 代 码:
因此,以下是我针对这两个问题的解决方法,其中包括对代码的说明:
首先,在您的应用程序中的自己的源文件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);
    }
  }
}

下一步,对于需要接收LatinIME软键盘按键事件的每个View派生类进行编辑,具体操作如下:
首先,在要接收按键事件的视图中创建一个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.   } };

最后,我们需要为可编辑内容定义一个类,以确保我们的可编辑缓冲区中始终至少有一个字符:
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行:

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()测试,解决了此问题。不幸的是,正是在这一点上引入了一个新的错误(62306),如果用户键入的退格次数与其键入其他字符的次数相同,则导致跳过包含KEYCODE_DEL生成的整个子句。即使使用TYPE_NULL,甚至使用目标SDK版本<=15,也会导致无法生成KEYCODE_DEL。这导致以前能够通过兼容性代码(targetSdkVersion <= 15)获取正确的KEYCODE_DEL行为的应用,在用户升级其Google键盘副本(或执行包含新版本Google键盘的OTA)时突然遇到此问题。请参见AOSP git文件中的第2146行(包括“NOT_A_CODE”的子句):

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

这个问题一直存在于Google键盘的发布版本中,直到现在(2014年1月7日)。它已经在代码库中修复,但截至本文撰写时尚未发布。
您可以在此处找到未发布的提交(包含标题为“当TYPE_NULL时发送退格作为事件”的提交合并的git提交),位于第2110行(可以看到不再有阻止我们进入生成KEYCODE_DEL的“NOT_A_CODE”子句):

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

当这个修复程序发布时,Google键盘的那个版本将不再受到影响TYPE_NULL的这两个问题。然而,在特定设备上仍将安装旧版本,因此该问题仍需要解决。随着更多的人升级到不包括修复程序的更高级别,这个解决方法将越来越少需要。但是,它已经被限定为逐渐消失(一旦您进行了指示的更改以将最终限制范围确定,并且实际发布了最终修复程序,以便您知道它实际上是什么)。

5
嗨,@Carl,我尝试了您上面的代码,删除检测效果很好,但如果我将输入类型更改为InputType.TYPE_CLASS_NUMBER(我有一个数字的editText),它就不起作用-输入会被禁用(在editText的xml中,我指定了android:inputType =“number”)。 - limlim
1
嗨@Carl,首先感谢您为以上所有工作付出的努力。我发现它非常有帮助。我只是想让您知道,我在下面发布了一个“附录”,这就是我最终在我的应用程序中使事情正常运行的方法(受到您上面解决方案的启发)。干杯! - Turix
任何计划使用这种方法的人都应该根据Turix在他自己回答这个问题时提出的建议进行修订。我已经测试了这些增强功能,它们比我自己解决方案中相应的代码更优秀。我打算很快将这些建议纳入到我的答案代码中。 - Carl
@Carl,我基于你的解决方案和Turix的解决方案添加了一个修改后的解决方案,试图解决我遇到的一些问题。 - pqvst
1
@Jared:class->classy :-) - Carl
显示剩余9条评论

21

看起来是 Android 的一个 bug:

问题 42904: 在 SDK 16 及以上版本中,KEYCODE_DEL 事件无法传递到 EditText

问题 42904 @ code.google.com


5

介绍:

在测试了@Carl和@Turix的两种解决方案后,我注意到以下问题:

  1. @Carl的解决方案不适用于unicode字符或字符序列,因为它们似乎是通过ACTION_MULTIPLE事件传递的,这使得区分“虚拟”字符和实际字符变得困难。

  2. 我无法在我的Nexus 5(4.4.2)上使用deleteSurroundingText功能,即使我尝试针对几个不同的sdk版本进行测试,但都没有起作用。也许Google又一次改变了DEL键背后的逻辑...

因此,我提出了以下结合方案,使用了Carl的长虚拟字符前缀的想法来使DEL键正常工作,但使用了Turix的自定义Editable解决方案来生成正确的按键事件。

结果:

我已经在多个设备上测试了这个解决方案,其中包括不同版本的Android和不同的键盘。所有以下测试用例对我都有效。我没有发现这个解决方案不起作用的情况。

  • Nexus 5 (4.4.2) 使用标准Google键盘
  • Nexus 5 (4.4.2) 使用SwiftKey
  • HTC One (4.2.2) 使用标准HTC键盘
  • Nexus One (2.3.6) 使用标准Google键盘
  • Samsung Galaxy S3 (4.1.2) 使用标准Samsung键盘

我还测试了不同的sdk版本:

  • 目标16
  • 目标19

如果这个解决方案对您也有效,请

视图:

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

5

(这个答案是对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); 
        }
    }

基本上,每当输入法尝试通过调用replace()将字符添加到可编辑区域时,该字符将替换掉已有的单一字符。同时,如果输入法尝试删除内容,则replace()覆盖会将现有内容替换为一个单一的“虚拟”字符(应该是您的应用程序将忽略的东西),以保持长度为1。
这意味着getEditable()和onKey()的实现可以比卡尔上面发布的代码稍微简单一些。例如,假设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;
    } 

请注意,使用此解决方案无需维护一个长度为1024个字符的长字符串。同时,也不会出现“退格太多”的危险(正如卡尔在有关按住退格键的评论中所讨论的那样)。
为了完整起见,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的解决方案存在的问题(覆盖deleteSurroundingText()),并建议按照他发布的方式使用它。

我会尝试这个,因为它看起来比我现在的代码更简洁。我确实尝试过重写SpannableStringBuilder,但不确定IME会使用哪些方法,或者它们将如何被调用,所以似乎更确定的方法是在创建Editable时将大量垃圾推入其中,并让IME从那个起点进行操作。我的解决方案确实有效,但你是正确的,我的注释是错误的;正如你所说,每次都会收到ACTION_MULTIPLE,而处理这种事件的代码正在做所有的工作(它可以工作,但比你的方法不够简洁)。 - Carl
1
@Carl 至少在 API-level-18 上是有效的,因为所有对 SpannableStringBuilder 的各种调用都会通过我重写的 replace() 方法进行处理。(我通过查看源代码来确认这一点,尽管没有任何保证。) 但是,我不确定其他版本是否也是如此(或将来是否如此)。祝好运! - Turix
嗯,是的,这正是我的担忧;即IME可以任意操纵可编辑内容,正如我在上面的答案中所指出的那样,似乎没有办法知道在特定设备上运行的IME代码版本。更不用说所有第三方键盘了。我认为没有规定只能使用replace。另一方面,如果在Editable接口中真的没有办法在不经过replace()的情况下更改字符串的内容,那么我会更有信心,因为该接口已编译到应用程序中。 - Carl
1
实际上,Editable文档描述了几乎可以更改内容的所有事情,如“replace()的便利性...”,这确实带来了一些信心。 另一方面,您无法确定实际实现是否会调用replace。 但是显然对于SpannableStringBuilder,它确实会调用replace。 如果您想要强制执行,我猜您可以使用与SpannableStringBuilder中当前代码完全相同的代码覆盖每个此类方法。 或者,只需在编译到不同项目构建版本时注意该代码即可。 无论如何,谢谢,一旦测试,这将是一个巨大的改进。 - Carl
1
@Carl 如果你想把这个加入到你的原始答案中,我没问题。很高兴它对你有用! - Turix
显示剩余10条评论

4

出于@Carl的思考,我得出了一种适用于任何输入类型的解决方案。以下是由两个类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);
    }
}

更新: 我更新了代码,因为在某些设备上启用文本预测(例如三星Galaxy S6)时它无法正常工作(感谢@Jonas在下面的评论中提供了这个问题),使用InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS在这种情况下也无效。我在许多设备上测试了这个解决方案,但仍不确定它是否适用于所有设备。如果EditText有任何不当行为,请您给我一些反馈。


CustomInputConnection 没有任何作用,可以直接替换为 BaseInputConnection。除此之外,这个解决方案对我来说似乎是有效的。 - Jonas
@Jonas,感谢您指出这一点。我已经删除了不必要的类。 - Ayaz Alifov
由于某些原因,此功能在Galaxy S6上无法正常工作。无法在EditText中输入任何文本。因此,它是设备相关的。在Genymotion和Nexus 6上可以正常工作,但在三星手机上失败了。 - Jonas
@Jonas,问题是由于文本预测模式引起的。我已经修复了它,请您对此进行一些评论。感谢您让我知道这个问题。 - Ayaz Alifov

3

我曾经遇到过类似的问题,即按下退格键时无法接收到KEYCODE_DEL。我认为这取决于软输入键盘,因为我的问题只在某些第三方键盘(比如Swype)中出现,而默认的Google键盘则没有问题。


这取决于键盘!而Nexus 4附带的Google键盘不支持它。那么怎么解决呢? - Vladimir Gazbarov

2

如果您重写适当视图/活动的dispatchKeyEvent方法(在我的情况下,主要活动很好),则可以拦截按键。

例如,我正在为具有硬件滚动键的设备开发应用程序,并惊讶地发现onKeyUp/onKeyDown方法从未针对它们调用。相反,默认情况下,按键通过一堆dispatchKeyEvent直到调用某个滚动方法(在我的情况下,令人困惑的是,一个按键会在两个可滚动视图中各调用一个滚动方法-非常烦人)。


还有dispatchKeyEventPreIme,但我怀疑那不是它...不过尝试一下也无妨。 - Merk

0

如果您检查了退格字符的十进制数字会怎样呢?

我认为它是像'/r'(十进制数7)之类的东西,至少对于ASCII码来说是这样。

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


也许我没有表达清楚。当按下退格键时,绝对没有任何事件被捕获。什么都没有。 - Vladimir Gazbarov

-1

-1

当按下退格键且编辑框为空时,调用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;
        }
    }});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接