The question is ancient, but for anyone looking for a more recent solution, I just published a barebones non-keyboard IME for Android to Github: https://github.com/jskubick/MiNKI (Minimal Non-Keyboard Ime). This is basically the answer I wish I'd had available when I first stumbled across this question 2 days ago :-)
Details:
Since StackOverflow frowns upon link-only answers, here's how to re-create it:
Create a new Android Studio project.
- Name it "MiNKI"
- Use the template for "Empty Project", and name the main class' package "example.ime.minki" (the two .java files I listed below go there).
You can always rename the package and project later, but following these two steps will increase the likelihood of everything working on the first try.
Add the following to AndroidManifest.xml:
Don't forget the meta-data for android.view.im... if you omit it, Android won't recognize it as a valid IME.
<service android:name="example.ime.minki.MinkiService"
android:label="@string/app_name"
android:permission="android.permission.BIND_INPUT_METHOD"
android:exported="true">
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
<meta-data android:name="android.view.im"
android:resource="@xml/method" />
</service>
res/xml/method.xml:
Note that I specified imeSubtypeMode, but not locale or language. I'm ultimately writing an IME that's language-agnostic (or at least, language-that-uses-the-Roman-alphabet agnostic), and didn't want to risk encouraging others to copy a solution that might someday leave someone in Britain, Canada, or elsewhere swearing violently because a future paternalistic, dystopian version of Android pointlessly locked them out of a keyboard specified as "en_US"
<?xml version="1.0" encoding="utf-8"?>
<input-method xmlns:android="http://schemas.android.com/apk/res/android">
<subtype android:imeSubtypeMode="keyboard" />
</input-method>
res/layout/minki.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:theme="@style/Theme.AppCompat.DayNight">
<example.ime.minki.MinkiView
android:id="@+id/minkiView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#aabbaa"
/>
</LinearLayout>
src/main/java/example/ime/minki/MinkiService.java:
You'll probably want to replace everything in onTouch()... I just put the most minimal stuff possible to detect the following single strokes, and omitted all the code that would normally draw their path below your finger because it adds massive amounts of code and makes it harder to understand the IME code itself.
There's nothing special about implementing the OnTouchListener() in MinkiService. I register it as a listener in onCreateInputView(). Notice how I handled normal letters, the enter key, and backspace. None of this is necessarily "best practice"... but it works.
Strokes it recognizes (should be at least 50-100 DP long)
- left to right: the letter 'x' (or X, if shift is active)
- right to left: backspace
- bottom to top: toggle shift
- top to bottom: the string "taco" or "cat", depending upon whether or not shift is active.
- approximately 45-degree falling slope ("\"): Enter key
package example.ime.minki;
import static android.view.KeyEvent.ACTION_UP;
import android.inputmethodservice.InputMethodService;
import android.os.SystemClock;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
public class MinkiService extends InputMethodService implements View.OnTouchListener {
private static final String TAG = "MinkiService";
private static int counter = 0;
private InputMethodManager inputMethodManager;
private int downAtX = 0;
private int downAtY = 0;
private boolean isShifted = false;
@Override
public void onCreate() {
super.onCreate();
inputMethodManager = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
}
@Override
public View onCreateInputView() {
Log.i(TAG, "about to inflate and return R.layout.minki");
View v = getLayoutInflater().inflate(R.layout.minki, null);
MinkiView mv = v.findViewById(R.id.minkiView);
v.setOnTouchListener(this);
return v;
}
@Override
public void onStartInput(EditorInfo info, boolean restarting) {
super.onStartInput(info, restarting);
Log.i(TAG, "onStartInput() called, counter=" + ++counter);
isShifted = false;
}
@Override
public void onFinishInput() {
super.onFinishInput();
Log.i(TAG, "onFinishInput() called, counter=" + counter);
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getActionMasked()==MotionEvent.ACTION_DOWN) {
downAtX = Math.round(motionEvent.getX());
downAtY = Math.round(motionEvent.getY());
Log.i(TAG, "ACTION_DOWN! (" + downAtX + "," + downAtY + ")");
}
else if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
Log.i(TAG, "ACTION_UP (" + motionEvent.getX() + "," + motionEvent.getY() + ")" );
int xDiff =Math.round(motionEvent.getX()) - downAtX ;
int yDiff = Math.round(motionEvent.getY()) - downAtY;
int slope = (xDiff == 0) ? Integer.MAX_VALUE : (int) ((yDiff / (float)xDiff) * 100);
Log.i(TAG, "xDiff=" + xDiff + ", yDiff=" + yDiff + ", slope=" + slope);
if (Math.abs(xDiff) < 50) {
if (yDiff < -100) {
isShifted = !isShifted;
Toast.makeText(this, isShifted ? "shifted" : "unshifted", Toast.LENGTH_SHORT).show();
return true;
}
else if (yDiff > 100) {
commitText(isShifted ? "CAT" : "taco");
return true;
}
}
// horizontal stroke
if ((Math.abs(slope) < 30) && (Math.abs(xDiff) > 100)){
if (xDiff < -100)
getCurrentInputConnection().deleteSurroundingText(1,0); // backspace
else if (xDiff > 100) {
getCurrentInputConnection().commitText( isShifted ? "X" : "x", 1);
isShifted = false;
}
}
else if ((slope > 50) && (slope < 300)) {
getCurrentInputConnection().sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
getCurrentInputConnection().sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
Log.i(TAG, "hit return");
}
downAtX = 0;
downAtY = 0;
}
return true;
}
private void commitText(CharSequence text) {
Log.i(TAG, "committing '" + text + "'");
Log.i(TAG, String.valueOf(getCurrentInputConnection().commitText(text, 1)));
}
}
src/main/java/example/ime/minki/MinkiView.java:
package example.ime.minki;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
public class MinkiView extends View {
public static final String TAG = "MinkiView";
public MinkiView (Context context, AttributeSet attrs) {
super(context, attrs);
}
}
Important!
This example shouldn't be taken as showing any "best practice". Its whole point is to get you past the first roadblock, and give you a little bit of easy gratification by getting an IME compiled by you to install and work.
I deliberately stripped it to the bone to make it clear which things MUST be part of an Android Studio project for a non-keyboard IME. Assuming I haven't overlooked anything, adding those 5 things to a newly-created Android Studio project SHOULD be all you need to do to get it to work.