如何在键盘被弃用后实现自定义视图?

27

我正在使用最新版的Android Studio和Kotlin为Android(API 100%用户)制作系统键盘。我试图遵循IME生命周期

有一个关于覆盖onCreateInputView()方法的指南。

override fun onCreateInputView(): View {
    return layoutInflater.inflate(R.layout.input, null).apply {
        if (this is MyKeyboardView) {
            setOnKeyboardActionListener(this@MyInputMethod)
            keyboard = latinKeyboard
        }
    }
}

其中MyKeyboardView是一个自定义的KeyboardView实现的实例,用于呈现键盘。

问题只是因为自API级别29起,android.inputmethodservice.KeyboardView已被弃用。文档中指出:

此类已被弃用,因为这只是一个方便的UI小部件类,应用程序开发人员可以在现有公共API之上重新实现。

我不想使用已弃用的功能,但该指南尚未针对此更改进行更新。制作我的自定义键盘的粗暴方法将只是在约束布局中制作大量按钮。这是正确的方法吗?作为一个完全的新手,一旦无法按照指南操作,我就迷失了方向。


1
说实话,既然它运行良好且重新实现没有意义,我会禁用已弃用的lint并继续使用它。 - Yannick
如果你在谷歌上搜索“Android,为什么键盘类被弃用?”,你不会得到一个好的答案。也许没有一个好的答案。 - user2782
1
这里是关于 Google 建议的内容以及我是如何让它起作用的。https://dev59.com/sFIH5IYBdhLWcg3wRrnK#63689954 - Misha Akopov
2个回答

3
此处的文档可以清楚地看到:
此类在API 29级中已被弃用。此类已被弃用,因为这只是一个方便的UI小部件类,应用程序开发人员可以在现有公共API之上重新实现它。如果您已经依赖于此类,请考虑将实现从AOSP复制到您的项目中或自己重新实现类似的小部件。
这意味着您必须创建具有所有按键的自定义视图,这也意味着必须自己处理所有的点击事件,例如输入、删除和切换到符号等键盘
实际上有许多方法可以实现它。但是我将尝试为您提供一个简单的想法,您将遵循使用弃用的KeyboardView时使用的大部分步骤: 首先创建您自己的自定义键盘布局,您可以使用任何方便您的布局,如LinearLayoutRelativeLayoutButton等用于按键的元素。我使用了一个带有ButtonGridLayout 然后像往常一样创建InputMethodService子类:
public class MyIMService extends InputMethodService implements View.OnClickListener {

    private static String TAG = "MyIMService";

    @Override
    public View onCreateInputView() {
        View myKeyboardView = getLayoutInflater().inflate(R.layout.key_layout, null);
        Button btnA = myKeyboardView.findViewById(R.id.btnA);
        btnA.setOnClickListener(this);
        //ADD ALL THE OTHER LISTENERS HERE FOR ALL THE KEYS
        return myKeyboardView;
    }

    @Override
    public void onClick(View v) {
        //handle all the keyboard key clicks here

        InputConnection ic = getCurrentInputConnection();
        if (v instanceof Button) {
            String clickedKeyText = ((Button) v).getText().toString();
            ic.commitText(clickedKeyText, 1);
        }
    }
}

如我之前所说,你可以尝试另一种处理所有点击事件的方法。但这应该能给你基本的想法。

就是这样。你必须像往常一样将此服务添加到清单文件中,并执行其他通常的步骤。现在应该可以工作了。

更新 Kotlin 版本:

class MyIMService : InputMethodService(), View.OnClickListener {
    override fun onCreateInputView(): View {
        val myKeyboardView: View = layoutInflater.inflate(R.layout.key_layout, null)
        val btnA: Button = myKeyboardView.findViewById(R.id.btnA)
        btnA.setOnClickListener(this)
        //ADD ALL THE OTHER LISTENERS HERE FOR ALL THE KEYS
        return myKeyboardView
    }

    override fun onClick(v: View) {
        //handle all the keyboard key clicks here
        val ic = currentInputConnection
        if (v is Button) {
            val clickedKeyText: String = (v as Button).getText().toString()
            ic.commitText(clickedKeyText, 1)
        }
    }

    companion object {
        private const val TAG = "MyIMService"
    }
}

1
我正在考虑使用你们的解决方案,但是我有疑虑,因为我找不到任何官方来源来处理 API 29 以后的事情;这是唯一的方法吗? - Nayk0
1
@Nayk0 由于他们已经弃用了KeyboardView和其他相关类,所以将来会将其删除。因此,考虑从AOSP中复制实现到您的项目中或自己重新实现类似的小部件 - Nongthonbam Tonthoi
1
@Nayk0 他们不会立即删除它,但一旦删除,您将无法使用sdk(其中已删除该类)编译代码。因此,如果您不关心未来的支持,那么就继续吧。 - Nongthonbam Tonthoi
能否请求在布局和MainActivity方面提供更多的解释/示例代码?我正在尝试采用此处的答案https://dev59.com/WGkw5IYBdhLWcg3w4emS#45005691,结合您的提示,但遇到了太多错误。我想我已经过时了,因为我在Java时代构建Android应用程序。 我特别困扰于以下行,我想我应该将其添加到MainActivity中 val keyboard: MyIMService = findViewById<View>(R.id.example_view) as MyIMService非常感谢。 - Uğur Dinç
@Nongthonbam Tonthoi,请问您能指导我如何现在制作keypreview吗? - Zeeshan Ali
显示剩余3条评论

1
我将为您提供创建没有使用任何已弃用API的方法的详细指南。首先,让我们从InputMethodService实现开始,您可以使用onCreateInputView()方法显示主键盘布局。它需要您返回一个View,该视图在服务启动时显示在屏幕上。因此,您可以在res/layout/keyboard_view.xml中创建一个键盘布局:
public class ControlBoard extends InputMethodService {
 
     View mainKeyboardView;
     boolean shiftPressed = false; // shiftPressed and metaState will be used for tracking shift key
     int metaState = 0; 

     @Override
     public View onCreateInputView() {
         
         mainKeyboardView = getLayoutInflater().inflate(R.layout.keyboard_view, null);

         return mainKeyboardView;
     }

     .....



为了创建布局,您可以使用任何您想要的方式,但我将向您展示如何使用LinearLayout进行操作。您可以使用一个orientation:vertical的LinearLayout作为根元素,然后嵌套更多orientation:horizontal的LinearLayout来创建行,并使用Button。为了存储按键代码,您可以在按钮中使用android:tag属性。对于样式,您可以将theme应用于根元素或将style应用于每个按钮。
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/keyboard_view"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:background="@drawable/keyboard_bg"
    >

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        >
        
     <!-- 
     You can define the keycodes in "android:tag" using Unicode scheme or KeyEvent.KEYCODE_ scheme.
     I will use the latter because it is easier to implement and allows greater features,
     such as handling all special keys simultaneously and using meta keys like ctrl or alt. -->

        <Button style="@style/NormalKeyStyles" android:tag="SHIFT" android:text="SHFT" />
        <Button style="@style/NormalKeyStyles" android:tag="G" android:text="g" />
        <Button style="@style/NormalKeyStyles" android:tag="3" android:text="3" />
        <Button style="@style/NormalKeyStyles" android:tag="APOSTROPHE" android:text="\'" />
        <Button style="@style/NormalKeyStyles" android:tag="DEL" android:text="&lt;\-" />
        <Button style="@style/NormalKeyStyles" android:tag="ENTER" android:text="&lt;\-\|" />
        
    </LinearLayout>

    <!-- Add more rows with the same scheme. -->
</LinearLayout>
    

参考:KeyEvent.KEYCODE_

res/values/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="NormalKeyStyles" parent="TextAppearance.AppCompat">
        <item name="android:layout_width">0dp</item>
        <item name="android:layout_weight">1</item>
        <item name="android:layout_height">match_parent</item>

        <!-- key_bg_selector is a ColorStateList which specifies different background colors for different states of button. -->
        <item name="android:background">@color/key_bg_selector</item>
        <item name="android:textColor">@color/white</item>
        <item name="android:textSize">24sp</item>
        <item name="android:textAllCaps">false</item>
        <item name="android:onClick">onKeyClick</item>

    </style>
</resources>

正如你所看到的,onKeyClick() 方法通过 NormalKeyStyles 绑定在每个 Button 上,它将处理点击事件。现在让我们看看如何处理它们。

我们可以使用 View.getTag() 方法从 XML 文件中提取我们分配给按键的键码。然后,我们可以使用 InputConnection.sendKeyEvent() 将事件发送给客户端。

 public void onKeyClick(View pressedKey) {
        InputConnection inputConnection = getCurrentInputConnection();
        if (inputConnection == null) return;
        
        long now = System.currentTimeMillis();
        String keyType = (String) pressedKey.getTag();
        
        }
        
        switch(keyType) {
         
            case "SHIFT":
                shiftPressed = !shiftPressed;

            default:
                // check if shift is active and then set metaState accordingly for sending the KeyEvent
                if (shiftPressed) {
                    metaState = KeyCode.META_SHIFT_MASK;
                } else {
                    metaState = 0;
                }
                try {
                    // keycode is retrieved from the KeyEvent.KEYCODE_keyname variables using Reflection API.

                    int keycode = KeyEvent.class.getField("KEYCODE_" + keyType).getInt(null), 0, metaState);
                    
                    // you can also use commitText() if you want, but you will have to add extra cases for special keys like ENTER, DEL, etc.
                    // This approach handles all of them in one case
                    inputConnection.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keycode);

                } catch (IllegalAccessException | NoSuchFieldException e) {
                    //
                }
                }
            }
    }

现在你已经可以使用了,但我想添加一些可能对你有帮助的注释:
  1. 你可以使用 setInputView() 在初始化后更改布局,所以你可以添加一个标签为 "CHANGE_LAYOUT" 的按键,并在 onKeyClick() 方法中添加一个 case 语句来切换不同的键盘。

  2. 如果你想在布局中添加额外的符号,这些符号没有直接的 KeyEvent keycode 字段,比如 $ 符号、表情符号或其他 Unicode 字符,你可以在那里使用 SHIFT 掩码(如果有的话,比如 SHIFT + 4 会产生 $ 符号),或者你可以使用 commitText() 进行输入。

  3. 要添加键预览弹出窗口,你可以使用一个 PopupWindow,带有一个 TextView,并在触摸开始时在键上方显示它,在触摸结束时将其关闭,并使用 setText() 更改 PopupWindow 文本。

  4. 对于更复杂的实现,你可以查看我的 GitHub 上的 Control Board 源代码。


我已经按照这种方法测试了键盘,它可以运行,但在某些横屏模式的应用程序中遇到了问题。我已经在 https://stackoverflow.com/questions/76279935/custom-system-wide-android-soft-keyboard-behaves-oddly-in-landscape-orientation 上发布了一个问题,询问这个问题可能是由什么原因引起的?我还从github上运行了你的“控制板”项目,结果相同。 - AL-zami

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