相変わらず日本語圏でのIME作成に関する記事が少なめなので、技術者ではなくなったものの発信は続けていきます。
今回は公式ドキュメントにも紹介されているため今までお世話になってきた方も多いであろう、KeyboardおよびKeyboardViewの代替を試みるものになっています。
なにがあったか
API Level 29からKeyboardに関するクラスが一挙に非推奨となりました。
実際これらのクラスのオンラインドキュメントを読むと別手法を取るよう勧告されています。
どうするか
新しい仕組みが用意されているわけでもないので、いちから自作します。
Viewまわり
まずは res/layout/keyboard.xml にViewレイアウトを作成。Google日本語入力の電話配列を意識して今回はGridLayoutを採用。
<GridLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="5">
<Button
android:id="@+id/key_1"
android:text="1"
style="@style/Key"/>
<Button
android:id="@+id/key_2"
android:text="2"
style="@style/Key"/>
<Button
android:id="@+id/key_3"
android:text="3"
style="@style/Key"/>
<Button
android:id="@+id/key_4"
android:text="4"
style="@style/Key"/>
<Button
android:id="@+id/key_5"
android:text="5"
style="@style/Key"/>
<Button
android:id="@+id/key_6"
android:text="6"
style="@style/Key"/>
<Button
android:id="@+id/key_7"
android:text="7"
style="@style/Key"/>
<Button
android:id="@+id/key_8"
android:text="8"
style="@style/Key"/>
<Button
android:id="@+id/key_9"
android:text="9"
style="@style/Key"/>
<Button
android:id="@+id/key_10"
android:text="10"
style="@style/Key"/>
<Button
android:id="@+id/key_11"
android:text="11"
style="@style/Key"/>
<Button
android:id="@+id/key_12"
android:text="12"
style="@style/Key"/>
<Button
android:id="@+id/key_13"
android:text="13"
style="@style/Key"/>
<Button
android:id="@+id/key_14"
android:text="14"
style="@style/Key"/>
<Button
android:id="@+id/key_15"
android:text="15"
style="@style/Key"/>
<Button
android:id="@+id/key_16"
android:text="16"
style="@style/Key"/>
<Button
android:id="@+id/key_17"
android:text="17"
style="@style/Key"/>
<Button
android:id="@+id/key_18"
android:text="18"
style="@style/Key"/>
<Button
android:id="@+id/key_19"
android:text="19"
style="@style/Key"/>
<Button
android:id="@+id/key_20"
android:text="20"
style="@style/Key"/>
</GridLayout>
キーの代替となるButtonの共通属性は res/values/styles.xml にて定義。
ミソはandroid:minWidth
とandroid:layout_columnWeight
。こいつらを指定することで見覚えあるアレに近づきます。
<resources>
<style name="Key">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">#000</item>
<item name="android:textAlignment">center</item>
<item name="android:minWidth">48dip</item>
<item name="android:layout_columnWeight">1</item>
</style>
</resources>
キーマップ
つづいてキーマップの作成。
キー情報は以前使っていたものを流用。修飾キーを使わない人はこんなことしなくてOK。
sealed class KeyInfo {
object Null : KeyInfo()
abstract class AsciiKeyInfo : KeyInfo() {
abstract val char: Char
abstract val code: Int
}
object Num0 : AsciiKeyInfo() {
override val char = '0'
override val code = KeyEvent.KEYCODE_0
}
object Num1 : AsciiKeyInfo() {
override val char = '1'
override val code = KeyEvent.KEYCODE_1
}
// 以下略
}
これらキーマップの構成要素をキーボードの各Buttonに対応させます。内部にandroid:id
をキーとしたハッシュマップを用意しておき、ここから随時取り出す形に。
interface KeymapHolder {
val keys: Set<Int>
fun getKeyInfo(indexId: Int): KeyInfo
}
class Keymap : KeymapHolder {
private val list: Map<Int, KeyInfo> = mapOf(
R.id.key_1 to KeyInfo.Num1,
R.id.key_2 to KeyInfo.Num2,
R.id.key_3 to KeyInfo.Num3,
R.id.key_4 to KeyInfo.Num4,
// 中略
R.id.key_19 to KeyInfo.Num9,
R.id.key_20 to KeyInfo.Num0
)
override val keys = list.keys
override fun getKeyInfo(indexId: Int): KeyInfo {
return list.getOrDefault(indexId, KeyInfo.Null)
}
}
その他
これらを使ったIMEが以下の通り。
class CustomIME : InputMethodService(), View.OnTouchListener {
private val keymap: KeymapHolder = Keymap()
private var currentKeyId = 0
@SuppressLint("InflateParams")
override fun onCreateInputView(): View {
return layoutInflater.inflate(R.layout.keyboard, null).also { view ->
keymap.keys.map { id ->
view.findViewById<Button>(id).also { it.setOnTouchListener(this) }
}
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return if (v is Button && event is MotionEvent) {
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// 押下したボタンのandroid:idを保持
currentKeyId = v.id
false
}
MotionEvent.ACTION_UP -> {
// 押下ボタンに基づいた文字を入力
sendKeyEvent(currentKeyId)
currentKeyId = 0
false
}
// タッチ位置が変わったり、複数指でタッチしても
// 見た目の変化が発生しないようにする
else -> true
}
} else {
true
}
}
// キー入力。本来はもっとごちゃごちゃするので簡略化。
private fun sendKeyEvent(id: Int) {
if (id !in keymap.keys) {
return
}
val keyInfo = keymap.getKeyInfo(id)
if (keyInfo is KeyInfo.AsciiKeyInfo) {
sendKeyChar(keyInfo.char)
}
}
}
AndroidManifest.xml はこのようにしておきます。
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="something.domain.of.keyboard">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!--
InputMethodServiceのサブクラスをnameに指定
-->
<service
android:name=".CustomIME"
android:label="@string/app_name"
android:permission="android.permission.BIND_INPUT_METHOD">
<meta-data
android:name="android.view.im"
android:resource="@xml/method"/>
<intent-filter>
<action android:name="android.view.InputMethod"/>
</intent-filter>
</service>
</application>
</manifest>
付随する res/xml/method.xml はとりあえず以下のように。
<input-method xmlns:android="http://schemas.android.com/apk/res/android">
<!--
android:imeSubtypeMode="keyboard"となっていればOK
上2つは表示用
-->
<subtype
android:label="@string/app_name"
android:imeSubtypeLocale="en_US"
android:imeSubtypeMode="keyboard"/>
</input-method>
動作確認
あとはメニューバーの「Run」→「Edit Configurations ...」からLaunch OptionsのDefault Activity
をNothing
にすれば準備完了。デバッグしてみれば期待通りの動作をするはずです。
嬉しい副作用
さて、KeyboardViewを自作することで以前できなかったことがいとも簡単にできるようになりました。
たとえばユーザー設定に基づいて色設定をコード上で行いたい時。
以下、上記CustomIME.onCreateInputView
より抜粋。
return layoutInflater.inflate(R.layout.keyboard, null).also { view ->
keymap.keys.map { id ->
view.findViewById<Button>(id).also { it.setOnTouchListener(this) }
}
}
この部分をこうしてあげればOK。
return layoutInflater.inflate(R.layout.keyboard, null).also { view ->
keymap.keys.map { id ->
view.findViewById<Button>(id).also {
it.setOnTouchListener(this)
// foreground
it.setTextColor(Color.WHITE)
// background
val onPress = Color.GRAY
val onRelease = Color.BLACK
it.background = StateListDrawable().apply {
// on Press
addState(
intArrayOf(+android.R.attr.state_pressed),
ColorDrawable(onPress)
)
// on Release
addState(
intArrayOf(-android.R.attr.state_pressed),
ColorDrawable(onRelease)
)
}
}
}
}
数年間にできなかったことがこんなにも簡単にできてしまうとは。勉強不足だったなぁ……。
所感
やってみての感想としては、「案外簡単にできるんだな」という印象。
多分Androidについて基礎からきちんと勉強すればなんてことはないものでしょう。基礎学習って大事。