LoginSignup
61

More than 3 years have passed since last update.

Androidカスタムキーボードの作り方

Last updated at Posted at 2017-12-26

導入

AndroidのオリジナルInput Method Editorを開発するぞ!!
・・・
ん!?
文献なさすぎて作れねぇ!ナンジャコリャーとなっている方多いと思います。
ここで一回ちゃんとまとめてみましょうよ...ということで現状得ている知見をまとめます。
少しでも役に立てば嬉しいです。

※本記事はAndroid StudioをIDEとして話を進めていきます。Xamarinでやりたいという人はこちらの記事を参考にしてください。

Android IMEの基本

AndroidのIMEはInputMethodServiceというServiceと、設定用のActivityによって構成されます。
Serviceの実装方法は通常のActivityのそれとは少し異なりますので注意が必要です。
しかし、通常のAndroidアプリが作れるスキルがあったほうが開発もスムーズですので、まずは通常のアプリを作れるようになることをお勧めします。(ちなみに私は初めてのAndroid開発がIMEでした汗)
参考:Androidアプリ開発を独学で学ぶ人への効果的な勉強法

シンプルなキーボード作成!

プロジェクトの立ち上げ

  1. Application nameを「CustomKeyboard」に、Company domainを「sample.com」にしてCreate Android Projectからプロジェクトを作成してください。
  2. APIのレベルは自分の必要な環境にしてください。(筆者はAPI 23: Android 6.0 Marshmallowで成功しています)
  3. Empty Activityを選択してください。
  4. Activity Nameを「NewKeyboard」に、Layout Nameを「keyboard_view」にしてください。

※ここでもしも「Gradle 'CustomKeyboard' project refresh failed」とエラーが出たら、Install missing platform(s) and sync projectを押してエラーを解決してください。

スクリーンショット 2017-12-27 2.33.36.png
↑こんな感じになっていればOKです。

必要なファイルの用意

  1. まず、resの配下にxmlという名前のDirectoryを追加してください。
  2. 次に、xmlの配下にmethod.xmlという名前のファイルを追加してください。(このとき、New -> XML resource fileとせずに、New -> Fileとするようにすること) また、同じくxmlの配下にkeyboard.xmlという名前のファイルを追加してください。
  3. 続いて、res/layoutの配下にkeyboard_view.xmlという名前のLayout resource fileを追加してください。(この時、Root elementはKeyboardにすること)

下準備は以上です。

AndroidManifestの実装

app/manifests/AndroidManifest.xmlを開いてください。
内容を以下のようにします。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sample.customkeyboard">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <service
            android:name=".NewKeyboard"
            android:label="@string/keyboard_name"
            android:permission="android.permission.BIND_INPUT_METHOD">
            <intent-filter>
                <action android:name="android.view.InputMethod" />
            </intent-filter>

            <meta-data
                android:name="android.view.im"
                android:resource="@xml/method" />
        </service>

    </application>

</manifest>

res/values/strings.xmlの編集

res/values/strings.xmlを開き、以下のように編集します。

strings.xml
<resources>
    <string name="app_name">CustomKeyboard</string>
    <string name="keyboard_name">NewKeyboard</string>
</resources>

res/xml/method.xmlの実装

res/xml/method.xmlを開き、以下のように編集します。

method.xml
<?xml version="1.0" encoding="utf-8"?>
<input-method xmlns:android="http://schemas.android.com/apk/res/android">
    <subtype
        android:label="@string/keyboard_name"
        android:imeSubtypeLocale="en_US"
        android:imeSubtypeMode="keyboard" />
</input-method>

res/layout/keyboard_view.xmlの実装

res/layout/keyboard_view.xmlを開き、以下のように編集します。

keyboard_view.xml
<?xml version="1.0" encoding="utf-8"?>
<android.inputmethodservice.KeyboardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/keyboard"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true" >
</android.inputmethodservice.KeyboardView>

res/xml/keyboard.xmlの実装

res/xml/keyboard.xmlを開き、以下のように編集します。

keyboard_view.xml
<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keyWidth="10%p"
    android:horizontalGap="0px"
    android:verticalGap="0px"
    android:keyHeight="40dp">

    <Row>
        <Key android:codes="8"  android:keyLabel="1" android:keyEdgeFlags="left" />
        <Key android:codes="9"  android:keyLabel="2" />
        <Key android:codes="10" android:keyLabel="3" />
        <Key android:codes="11" android:keyLabel="4" />
        <Key android:codes="12" android:keyLabel="5" />
        <Key android:codes="13" android:keyLabel="6" />
        <Key android:codes="14" android:keyLabel="7" />
        <Key android:codes="15" android:keyLabel="8" />
        <Key android:codes="16" android:keyLabel="9" />
        <Key android:codes="7"  android:keyLabel="0" android:keyEdgeFlags="right" />
    </Row>

    <Row>
        <Key android:codes="-5" android:keyLabel="Delete" android:isRepeatable="true" />
        <Key android:codes="66" android:keyLabel="Return" android:isRepeatable="true" />
    </Row>

</Keyboard>

KeyCodeの参照先1
KeyCodeの参照先2
※KeyEventのものとKeyboardのものとあるので注意!

InputMethodServiceの実装

キーボードのメインのコードに当たるNewKeyboard.java(InputMethodService)の実装を行います。最低限の実装ですので、エラー対策などが甘くクラッシュしやすいです。

NewKeyboard.java
package com.sample.customkeyboard;

import android.os.Bundle;
import android.util.Log;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;

public class NewKeyboard extends InputMethodService implements KeyboardView.OnKeyboardActionListener {

    private KeyboardView keyboardView;
    private Keyboard keyboard;

    //初回だけ呼ばれる
    @Override
    public void onCreate() {
        super.onCreate();
    }

    //初回だけ呼ばれる
    @Override
    public View onCreateInputView() {
        super.onCreateInputView();

        keyboardView = (KeyboardView) getLayoutInflater().inflate(R.layout.keyboard_view, null);
        keyboard = new Keyboard(this, R.xml.keyboard);
        keyboardView.setKeyboard(keyboard);
        keyboardView.setOnKeyboardActionListener(this);
        keyboardView.setPreviewEnabled(false);
        return keyboardView;
    }

    //キーボードが表示されるたびに呼ばれるメソッド
    @Override
    public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
        //なんらかの処理
    }

    //キーボードが閉じる時に呼ばれるメソッド
    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    //キーを押した時
    @Override
    public void onKey(int primaryCode, int[] keyCodes) {
        InputConnection ic = getCurrentInputConnection();
        switch (primaryCode) {
            case KeyEvent.KEYCODE_1:
                ic.commitText("1", 1);
                break;
            case KeyEvent.KEYCODE_2:
                ic.commitText("2", 1);
                break;
            case KeyEvent.KEYCODE_3:
                ic.commitText("3", 1);
                break;
            case KeyEvent.KEYCODE_4:
                ic.commitText("4", 1);
                break;
            case KeyEvent.KEYCODE_5:
                ic.commitText("5", 1);
                break;
            case KeyEvent.KEYCODE_6:
                ic.commitText("6", 1);
                break;
            case KeyEvent.KEYCODE_7:
                ic.commitText("7", 1);
                break;
            case KeyEvent.KEYCODE_8:
                ic.commitText("8", 1);
                break;
            case KeyEvent.KEYCODE_9:
                ic.commitText("9", 1);
                break;
            case KeyEvent.KEYCODE_0:
                ic.commitText("0", 1);
                break;
            case Keyboard.KEYCODE_DELETE:
                ic.deleteSurroundingText(1, 0);
                break;
            case KeyEvent.KEYCODE_ENTER:
                ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
                break;
            default:
                break;
        }
    }

    @Override
    public void onPress(int primaryCode) {
    }

    @Override
    public void onRelease(int primaryCode) {
    }

    @Override
    public void onText(CharSequence text) {
    }

    @Override
    public void swipeLeft() {
    }

    @Override
    public void swipeRight() {
    }

    @Override
    public void swipeDown() {
    }

    @Override
    public void swipeUp() {
    }

}

実装は簡単で、押されたキーのKEYCODEによってSwitch文で分岐して処理を割り当てています。

キーボードのテスト

現状ですとActivityがないので、そのままではRunできません。
Edit Configurations...からLaunchをNothingにしましょう。
Android エミュレータが起動したら、環境設定から、言語と入力を選び、NewKeyboardをONにしてください。(NewKeyboardが表示されない場合はAppsを確認してインストールされているか確かめてください。表示されないことはよくあり、原因を探るのは困難です。)

Screenshot_1514314020.png
こんな感じで起動されればOKです。

設定アクティビティの作成

AndroidManifestの実装

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sample.customkeyboard">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <service
            android:name=".NewKeyboard"
            android:label="@string/keyboard_name"
            android:permission="android.permission.BIND_INPUT_METHOD">
            <intent-filter>
                <action android:name="android.view.InputMethod" />
            </intent-filter>

            <meta-data
                android:name="android.view.im"
                android:resource="@xml/method" />
        </service>

        <activity
            android:name=".Settings"
            android:label="@string/app_name"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

Activityを追加します。ここでLAUNCHER属性をつけるのを忘れずに。

res/layout/settings_view.xmlの追加・実装

res/layout配下に「settings_view.xml」というLayout resource fileを追加してください。(レイアウトはLinearLayoutにすると楽です。)
doubleKeyというidのついたボタンを一つ配置しましょう。

settings_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Switch
        android:id="@+id/doubleKey"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="double key" />

</LinearLayout>

Activityの追加・実装

NewKeyboard.javaと同じ階層に「Settings.java」というJava Classを追加してください。

Settings.java
package com.sample.customkeyboard;

import android.app.Activity;
import android.os.Bundle;

/**
 * Created by Kyome on 2017/12/27.
 */

public class Settings extends Activity {

    @Override
    protected  void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings_view);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

}

共有データの保存・取得

SharedPreferencesを利用します。IME側と設定アクティビティ側で共有するためにモードはMODE_MULTI_PROCESSにします。(非推奨だが仕方がない)

設定アクティビティ側

Settings.java
package com.sample.customkeyboard;

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.Switch;

public class Settings extends Activity {

    private SharedPreferences prefs;
    private SharedPreferences.Editor editor;
    private Switch doubleKeySwitch;

    @Override
    protected  void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings_view);

        prefs = getSharedPreferences("NewKeyboardData", MODE_MULTI_PROCESS);
        editor = prefs.edit();

        doubleKeySwitch = (Switch) findViewById(R.id.doubleKey);
        doubleKeySwitch.setChecked(prefs.getBoolean("doubleKey", false));
        doubleKeySwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                editor.putBoolean("doubleKey", b).apply();
            }
        });
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

}

キーボード側

NewKeyboard.java
//・・・
import android.content.SharedPreferences;

public class NewKeyboard extends InputMethodService implements KeyboardView.OnKeyboardActionListener {

    private KeyboardView keyboardView;
    private Keyboard keyboard;
    private SharedPreferences prefs;

    //・・・

    //キーボードが表示されるたびに呼ばれるメソッド
    @Override
    public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
        prefs = getSharedPreferences("NewKeyboardData", MODE_MULTI_PROCESS);
    }

    //・・・

    //キーを押した時
    @Override
    public void onKey(int primaryCode, int[] keyCodes) {

        boolean doubleKey = prefs.getBoolean("doubleKey", false);

        InputConnection ic = getCurrentInputConnection();
        switch (primaryCode) {
            case KeyEvent.KEYCODE_1:
                ic.commitText(!doubleKey ? "1" : "11", 1);
                break;
            case KeyEvent.KEYCODE_2:
                ic.commitText(!doubleKey ? "2" : "22", 1);
                break;
            case KeyEvent.KEYCODE_3:
                ic.commitText(!doubleKey ? "3" : "33", 1);
                break;
            case KeyEvent.KEYCODE_4:
                ic.commitText(!doubleKey ? "4" : "44", 1);
                break;
            case KeyEvent.KEYCODE_5:
                ic.commitText(!doubleKey ? "5" : "55", 1);
                break;
            case KeyEvent.KEYCODE_6:
                ic.commitText(!doubleKey ? "6" : "66", 1);
                break;
            case KeyEvent.KEYCODE_7:
                ic.commitText(!doubleKey ? "7" : "77", 1);
                break;
            case KeyEvent.KEYCODE_8:
                ic.commitText(!doubleKey ? "8" : "88", 1);
                break;
            case KeyEvent.KEYCODE_9:
                ic.commitText(!doubleKey ? "9" : "99", 1);
                break;
            case KeyEvent.KEYCODE_0:
                ic.commitText(!doubleKey ? "0" : "00", 1);
                break;
            case Keyboard.KEYCODE_DELETE:
                ic.deleteSurroundingText(1, 0);
                break;
            case KeyEvent.KEYCODE_ENTER:
                ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
                break;
            default:
                break;
        }
    }
    //・・・
}

コラム

キャレットの位置を移動する

KEYCODE_DPAD_LEFTとかでキャレットを左右上下に動かせるらしいです.

複雑なキーレイアウトを行う

Screenshot_1514316639.png
こんな感じのキーレイアウトを行う方法が意外と厄介だったので紹介。
horizontalGapを指定してあげることで解決。

keyboard.xml
<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keyWidth="50%p"
    android:horizontalGap="0px"
    android:verticalGap="0px"
    android:keyHeight="40dp">

    <Row>
        <Key android:codes="8"  android:keyLabel="1" android:keyHeight="80dp" android:keyEdgeFlags="left" />
        <Key android:codes="-5" android:keyLabel="Delete" android:keyWidth="50%p" android:isRepeatable="true" android:keyEdgeFlags="right" />
    </Row>

    <Row>
        <Key android:codes="66" android:keyLabel="Return" android:keyWidth="50%p" android:horizontalGap="50%" android:isRepeatable="true" />
    </Row>

</Keyboard>

ちゃんと動くReturnキーの実装

テキストフィールドが一行の時(検索ボックスなど)と複数行の時(メモ帳など)、用途が分かれる時などReturnキーの処理は場合分けが必要です。
自分なりによく反応する処理を考えてみましたので参考まで。

InputConnection ic = getCurrentInputConnection();
EditorInfo info = getCurrentInputEditorInfo();
if (ic != null) {
    int type = info.imeOptions & EditorInfo.IME_MASK_ACTION;
    switch (type) {
        case EditorInfo.IME_ACTION_UNSPECIFIED:
        case EditorInfo.IME_ACTION_NONE:
            ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
            break;
        default:
            if ((info.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0) {
                ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
            } else {
                ic.performEditorAction(type);
            }
            break;
    }
}

落ちないDeleteキーの実装

InputConnection ic = getCurrentInputConnection();
EditorInfo info = getCurrentInputEditorInfo();
if (ic != null) {
    CharSequence currentText = ic.getExtractedText(new ExtractedTextRequest(), 0).text;
    int length = ic.getTextBeforeCursor(currentText.length(), 0).length();
    if (length > 0) {
        ic.deleteSurroundingText(1, 0);
    }
}

カーソルより前の文字数をカウントして消しています。

参考文献

日本語で書かれたAndroid IMEの参考文献めっちゃ少ない!!
ソフトウェアキーボードの作り方(Android)
AndroidのIME機能を使って寿司ネタキーボードを作る(役に立つかは微my...ゴホン)
InputMethodService(キーボード)開発の勘所となりそうな項目(とてもありがたいサイト)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61