27

I am using latest Android Studio and Kotlin to make a system keyboard for Android (API 100% users). I am trying to follow the IME life cycle.

There is this guideline on overriding onCreateInputView()

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

where MyKeyboardView is an instance of a custom implementation of KeyboardView that renders a Keyboard.

The problem only rose because android.inputmethodservice.KeyboardView has been deprecated since API level 29. The document says

This class is deprecated because this is just a convenient UI widget class that application developers can re-implement on top of existing public APIs.

I do not want to use a deprecated feature but the guide has not been updated accounting for this change. The brutal way of making my own would be just making a ton of buttons in constraint layout. Is this the correct way? As a complete beginner, I am lost as soon as I cannot follow the guide.

James Z
  • 12,209
  • 10
  • 24
  • 44
Seung
  • 763
  • 1
  • 7
  • 19
  • 1
    Honestly, since it works great and makes no sense to reimplement i would disable deprecated lint and use it anyway. – Yannick Oct 27 '19 at 09:31
  • If you Google on "Android, why was keyboard class deprecated?", you do not get a good answer. Perhaps there is no good answer. – user2782 Jan 13 '20 at 23:41
  • 1
    Here, what google suggests and how I made it work https://stackoverflow.com/a/63689954/3101777 – Misha Akopov Sep 03 '20 at 11:39

2 Answers2

3

It is clear, from the docs here :

This class was deprecated in API level 29. This class is deprecated because this is just a convenient UI widget class that application developers can re-implement on top of existing public APIs. If you have already depended on this class, consider copying the implementation from AOSP into your project or re-implementing a similar widget by yourselves

This means that you have to create your own view with all the keys, which also means handling all the click events like enter, delete and switching keyboards eg. to symbols etc. by yourself.

Actually there are many ways to do it. But I will try to give you a simple idea, you will follow most of the steps that you used while using the deprecated KeyboardView:

First create your custom keyboard layout, you can use any layout depending what is convenient for you like LinearLayout, RelativeLayout and Buttons for the keys etc. I used a GridLayout with Buttons.

Then create the subclass of InputMethodService as usual:

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

As I said earlier you can try a different way of handling all the click events. But this should give you the basic idea.

That's it. You have to add this service in your manifest file as usual and also the other steps as usual. This should work now.

UPDATE Kotlin version:

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"
    }
}
Nongthonbam Tonthoi
  • 12,667
  • 7
  • 37
  • 64
  • 1
    I'm considering using your solution but I have my doubts since I can't find any official source on how to handle things since api 29; Is it the only way to do it ? – Nayk0 Jun 29 '20 at 10:44
  • 1
    @Nayk0 Since they have deprecated the `KeyboardView` and the other related classes, they will remove it in future. So `consider copying the implementation from AOSP into your project or re-implementing a similar widget by yourselves` – Nongthonbam Tonthoi Jun 29 '20 at 14:31
  • Alright thank you. Now, if I only need to use the deprecated version for api < 29, will it be a problem in the future ? I don't have experience using deprecated features but I guess if I set a check like "if (sdk < 29)" it would be ok to use it ? – Nayk0 Jun 30 '20 at 12:49
  • 1
    @Nayk0 They will not remove it immediately, but once they do, you would not be able to compile your code (because the class does not exists) with the sdk (in which it was removed). So if you don't care about future support, well go ahead. – Nongthonbam Tonthoi Jun 30 '20 at 15:21
  • How do I bind service with the view? – Ashutosh Soni Aug 27 '20 at 11:21
  • Thanks you much. It was really helpful for me. – Zeeshan Ali Mar 16 '22 at 18:04
  • Would it be too much to ask for a bit more explanation/sample code on layout side and MainActivity side? I am trying to adopt the answer here https://stackoverflow.com/a/45005691/3681880 with your tip but running into too many errors. I guess I am deprecated myself as I built android apps at java times. I am specifically struggling with the following line I guess I should add to MainActivity val keyboard: MyIMService = findViewById(R.id.example_view) as MyIMService Many thanks in advance. – Uğur Dinç May 10 '22 at 21:46
  • @Nongthonbam Tonthoi. can you please guide that how can i make keypreview now ? – Zeeshan Ali Jun 09 '22 at 06:50
1

I will give a detailed guide on creating one without using any deprecated APIs.

First, let's start with the InputMethodService implementation, you can use the onCreateInputView() method to display the main keyboard layout. It needs you to return a View that will be displayed on the screen when the service starts. So you can create a Keyboard layout in 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;
     }

     .....


To create the layout, you can use whatever you want, but I will show you how to do it with LinearLayouts. You can use one LinearLayout as the root element with orientation:vertical, and then nest more LinearLayouts with orientation:horizontal to create rows, with Buttons. For storing the keycodes, you can use the android:tag attribute in the Buttons. For styles, you can apply theme to the root element or style to each Button.

<?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>
    

Reference: 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>

As you can see, onKeyClick() method is bound to each Button via the NormalKeyStyles, it will handle the click events. Let's see how to handle them.

We can use the View.getTag() method to extract the keycode we assigned to the keys in xml file. And then, we can use InputConnection.sendKeyEvent() to send the event to the client.

 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) {
                    //
                }
                }
            }
    }

And now you are good to go, but I will like to add some notes that might be helpful to you:

  1. You can change the layout after initialization using setInputView(), so you may add a key with tag "CHANGE_LAYOUT" and add a case for it in the onKeyClick() method to switch between different keyboards.

  2. If you want to add extra symbols in the layout, which has no direct KeyEvent keycode field, such as $ sign, or emojis or other unicode characters, you can use SHIFT mask there if available (like SHIFT + 4 produces $), or you can use commitText() for inputting that.

  3. For adding key preview popups, you can use a PopupWindow with a TextView and display it on top of the key when touch starts, and dismiss it when touch ends, with setText() to change the PopupWindow text.

  4. For a more sophisticated implementation, you can see my Control Board source on GitHub.

Bikram Kumar
  • 393
  • 1
  • 4
  • 15
  • I've followed this approach for a test keyboard, it ran but I am facing issue in some apps in landscape mode. I've posted a question https://stackoverflow.com/questions/76279935/custom-system-wide-android-soft-keyboard-behaves-oddly-in-landscape-orientation regarding this issue. Any idea what might cause this issue? I also ran your `Control Board` project from github. same result. – AL-zami May 20 '23 at 14:03