我覆盖 View (openGL表面 View )的onKeyDown方法以捕获
所有按键。问题在于,在多个设备上,KEYCODE_DEL不是
被抓获。我尝试将onKeyListener添加到 View 中,并且捕获了除Backspace键之外的所有内容。

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

最佳答案



经过许可,Turix的答案已合并到我自己的代码中。 Turix的改进消除了将垃圾字符注入(inject)Editable缓冲区的麻烦,而改为寻找一种增量方式来确保该缓冲区中始终只有一个字符。

我在一个已部署的应用程序中使用了与此类似的代码,我们欢迎您对其进行测试:
https://play.google.com/store/apps/details?id=com.goalstate.WordGames.FullBoard.trialsuite]

简介:

就这两个错误而言,以下介绍的变通办法旨在适用于过去和将来的所有版本的Google Keyboard。此解决方法不需要使应用程序始终停留在API级别15或更低的水平,某些应用程序已将其限制在自己的位置,以利用绕过问题42904的兼容性代码。

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

对于不使用TYPE_NULL的应用, View 从其onCreateInputConnection()覆盖返回的BaseInputConnection派生对象中有各种覆盖,这些覆盖将由IME在用户执行编辑时调用,而不是由IME生成键事件。这种(非TYPE_NULL)方法通常比较优越,因为软键盘的功能现在已经远远超出了仅敲击按键的范围,扩展到了语音输入,完成等功能。按键事件是一种较旧的方法,谷歌实现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
(列为WorkingAsIntended,但我认为问题是,该错误导致它无法针对目标为API级别16或更高版本的应用(具体列出了InputType为TYPE_NULL的应用)支持KEYCODE_DEL事件生成。此问题已在最新版本中修复。发行了LatinIME,但仍存在一些过时的错误,因此使用TYPE_NULL且目标API级别为16或更高的应用仍需要可在应用内执行的解决方法。

和此处:

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

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

在某些LatinIME版本中存在一个或另一个(但不是一次)的问题。因此,开发人员很难在测试期间知道他们是否已经解决了所有KEYCODE_DEL问题,并且有时在执行Android(或Google键盘)更新时,测试中的问题将不再重现。尽管如此,引起问题的LatinIME版本仍将出现在使用中的许多设备上。这迫使我深入研究AOSP LatinIME git repo,以确定两个问题中每个问题的确切范围(即可能存在两个问题的特定LatinIME和Android版本)。下面的解决方法代码已限于这些特定版本。

下面显示的解决方法代码包括大量注释,这些注释应有助于您了解其要完成的工作。在介绍了代码之后,我将提供一些附加的讨论,其中包括特定的Android开放源代码项目(AOSP)提交,在该提交中引入了两个错误,在此错误中又消失了,以及可能包括受影响的Google键盘版本。

我会警告任何考虑使用此方法来执行自己的测试以验证它适用于其特定应用程序的人。我认为它可以正常运行,并且已经在许多设备和LatinIME版本上对其进行了测试,但是推理起来很复杂,因此请谨慎行事。如果发现任何问题,请在下面发表评论。

CODE :

然后,这是我针对这两个问题的解决方法,并在代码的注释中包含了解释:

首先,在您的应用程序中,在其自己的源文件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派生类,并按如下所示进行编辑:

首先,在要接收的 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;
}

其次,对 View 的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);
   }
 }

这样就完成了我发现可以解决这两个问题的源代码更改。

其他说明:

在API级别16随附的LatinIME版本中引入了问题42904描述的问题。在此之前,无论是否使用TYPE_NULL都将生成KEYCODE_DEL事件。在与Jelly Bean一起发布的LatinIME中,这一代产品已经停产,但TYPE_NULL并未异常(exception),因此对于API级别高于16的应用程序,TYPE_NULL行为已被有效禁用。但是,添加了兼容性代码,允许具有以下功能的应用程序使用即使没有TYPE_NULL,targetSdkVersion
https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/android-4.1.1_r1/java/src/com/android/inputmethod/latin/LatinIME.java

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

从commit 4.4_r0.9开始(仅在4.4版本之前),已通过在保护KEYCODE_DEL生成的条件中添加isTypeNull()测试来解决此问题。不幸的是,正是在这一点上引入了一个新的错误(62306),如果用户键入退格次数与键入其他字符的次数相同,则会导致包装KEYCODE_DEL代的整个子句被跳过。在这种情况下,即使使用TYPE_NULL,甚至使用targetSdkVersion
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日)。它已在存储库中修复,但截至本文撰写时尚未发布。

可以在此处找到未发布的提交(包含该提交的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的这两个问题。但是,在不确定的将来,仍将在特定设备上安装较旧的版本。因此,该问题仍将需要一种解决方法。最终,随着更多人升级到更高的级别而不是上一个不包含此修复程序的人,将越来越需要这种解决方法。但是它已经可以逐步淘汰(一旦您进行了指示性的更改以将最终限制放到范围上,而最终修订实际上已经发布了,那么您就知道它是什么了)。

10-01 02:32