Suggestions in Android

One of the first things you may choose to add to your project is suggestions. There are probably many ways to do this. Here's how I did it.

State

I chose to store my state in some variables, alongside 'caps'.

private boolean caps = false;
private String composingText = "";
private String[] suggestions = {" ", " ", " "};


I'm not sure if this is the best way, but it's how I chose to proceed. Note that I chose the blank suggestions to consist of a single space, rather than an empty string, because if you set a Key's label to be an empty string, turning on caps/shift will cause the Android system to throw an error.

setComposingText

When we set up our project, we made use of Android Authority's tutorial. They use inputConnection's commitText, but inputConnection also has setComposingText. We're going to use both of these frequently, so let's create a useful method.


private void setComposingText(String txt, Boolean commit, InputConnection ic) {
    if (commit) {
        ic.commitText(txt, 1);
        composingText = "";
    } else {
        ic.setComposingText(txt, 1);
        composingText = txt;
    }
} 

CandidatesView

You're supposed to put the suggestions in a CandidateView, which is just a View set at creation with  to View onCreateCandidatesView or later with setCandidatesView(View) (you can also toggle the visibility with setCandidatesViewShown(boolean)).

Besides being able to toggle the visibility, it gives you a huge advantage over the Keyboard. When you create a Keyboard, you can only provide an xml, not a View, vastly limiting your control over what gets created (I can't even see a way to hide a key or generate them on the fly).

Having said that, I chose to skip CandidatesView completely and add three new Keys. Apparently Swype made a similar decision.

The keys themselves

I add the suggestions to the xml:
 
<Row>
    <Key android:codes="-7" android:keyLabel="sug" android:keyWidth="33%p" android:keyEdgeFlags="left"/>
    <Key android:codes="-8" android:keyLabel="sug" android:keyWidth="34%p"/>
    <Key android:codes="-9" android:keyLabel="sug" android:keyWidth="33%p" android:keyEdgeFlags="right"/>
</Row>
 
I then grab them with Keyboard.getKeys() like this

 
@Override
public View onCreateInputView() {
    //...
    allKeys = keyboard.getKeys();
    suggestKeys = new ArrayList<>();
    for (Key key : allKeys) {
        if(key.codes[0] <= -7 && key.codes[0] >= -9) {
            suggestKeys.add(key);
        }
    }
}


And update the keys like this
 
private void setComposingText(String txt, Boolean commit, InputConnection ic) {
    if (commit) {
        ic.commitText(txt, 1);
        composingText = "";
    } else {
        ic.setComposingText(txt, 1);
        composingText = txt;
    }


    // Update suggestions
    if (verbs != null) {
        String[] allSuggestions = getSuggestions(composingText);
        for (int i = 0; i < suggestKeys.size(); i++) {
            suggestions[i] = (i < allSuggestions.length) ? allSuggestions[i] : " ";
            suggestKeys.get(i).label = suggestions[i];
        }
    }
    keyboardView.invalidateAllKeys();
}

Note that we have modified some keys, so must do KeyboardView.invalidateAllKeys(). Then when the key is pressed, we'll insert this text in onKey's switch:
case -7:
case -8:
case -9:
    Integer suggestionIndex = -7 - primaryCode;
    if (!suggestions[suggestionIndex].equals(" ")) {
        selectedVerb = suggestions[suggestionIndex];
        setComposingText(
                selectedVerb,
                true,       
                inputConnection
        );
        keyboardView.setKeyboard(keyboardTenses);
    }
    break;

Resetting the state

You're going to have to reset the state in a number of places, but not when it's our keyboard inserting a letter. Let's keep track of that here:
 
private boolean isInsertingLetter;
 
We could probably figure this out, but it's just a lot easier this way. Let's reset when...
  • ...the keyboard starts in a new location:

public void onBindInput() {
    super.onBindInput();
    resetState();
}

  • When the keyboard ends:

@Override
public void onFinishInput() {
    super.onFinishInput();
    composingText = "";
    suggestions = new String[]{"", "", ""};
}
  • When the user moves the selection:

@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
        int candidatesStart, int candidatesEnd) {
    super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
            candidatesStart, candidatesEnd);

        if (isInsertingLetter) {
            isInsertingLetter = false;
        } else if (composingText.length() > 0) {
            resetState();

            InputConnection ic = getCurrentInputConnection();
            if (ic != null) {
                ic.finishComposingText();
            }
        }
}

When the user moves the selection:
 
private void resetState() {
    composingText = "";
    suggestions = new String[]{" ", " ", " "};

    if (keyboardView != null) {
        keyboardView.invalidateAllKeys();
    }
}

More work required

This should get the basic version working. Obviously more work needs to be done to create a smooth user experience, but I'll leave that up to you.

Comments

Popular posts from this blog

Available Resources for Android

Using React Native in an Android Custom Keyboard

Popup Keyboard for Special Characters in Android