AndroidでテンキーのようなViewを作る

アプリ内の入力システムを自分で作る必要があったのでメモ兼ねて記事を書きます。
説明が足りていない点や改善点などあればコメントで遠慮なくご指摘ください。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270890/27945284-aa35-3dea-3a81-d371110c16be.png">

# 仕様
- テンキーのように動作する
- キーを押している間は色が変わる
- 再利用可能(カスタムViewとして使い回せる)
- xmlから入力対象となるEditText(TextView)をidで指定できる

# 実装
アプリ全体のソースコードは[こちら](https://github.com/yt8492/NumericKeyboard)
今回はTableLayoutを継承したViewとして作っていきます。

```xml:values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="NumericKeyboard">
        <attr name="targetField" format="reference" />
    </declare-styleable>
</resources>
```

```xml:drawable/keyboard_divider.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/darker_gray"/>
    <size
            android:width="1dp"
            android:height="1dp"/>
</shape>
```

```xml:drawable/keyboard_button_background.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <solid  android:color="#dddddd" />
        </shape>
    </item>

    <item android:state_pressed="false">
        <shape>
            <solid android:color="@android:color/transparent" />
        </shape>
    </item>
</selector>
```

```xml:values/styles.xml
<resources>

    ...

    <style name="KeyboardRow">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:gravity">center</item>
        <item name="android:divider">@drawable/keyboard_divider</item>
        <item name="android:showDividers">middle</item>
    </style>

    <style name="KeyboardButton">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">match_parent</item>
        <item name="android:layout_weight">1</item>
        <item name="android:paddingTop">12dp</item>
        <item name="android:paddingBottom">12dp</item>
        <item name="android:background">@drawable/keyboard_button_background</item>
        <item name="android:clickable">true</item>
        <item name="android:focusable">true</item>
        <item name="android:gravity">center</item>
        <item name="android:scaleType">fitCenter</item>
        <item name="android:textColor">@android:color/black</item>
        <item name="android:textSize">20sp</item>
        <item name="android:textStyle">bold</item>
    </style>

</resources>

```

```xml:layout/numeric_keyboard.xml
<?xml version="1.0" encoding="utf-8"?>
<merge
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:android="http://schemas.android.com/apk/res/android">
    <TableRow style="@style/KeyboardRow">
        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey7"
                android:text="7" />

        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey8"
                android:text="8" />

        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey9"
                android:text="9" />
    </TableRow>

    <TableRow style="@style/KeyboardRow">
        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey4"
                android:text="4" />

        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey5"
                android:text="5" />

        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey6"
                android:text="6" />
    </TableRow>

    <TableRow style="@style/KeyboardRow">
        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey1"
                android:text="1" />

        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey2"
                android:text="2" />

        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey3"
                android:text="3" />
    </TableRow>

    <TableRow style="@style/KeyboardRow">
        <TextView
                style="@style/KeyboardButton"
                android:clickable="false" />

        <TextView
                style="@style/KeyboardButton"
                android:id="@+id/numericKey0"
                android:text="0" />

        <ImageView
                style="@style/KeyboardButton"
                android:id="@+id/numeric_key_backspace"
                android:tint="@android:color/black"
                app:srcCompat="@android:drawable/ic_input_delete" />
    </TableRow>
</merge>
```

```kotlin:NumericKeyboard.kt
class NumericKeyboard @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : TableLayout(context, attrs), View.OnClickListener {
    private val keyValues = mapOf(
        R.id.numericKey0 to "0",
        R.id.numericKey1 to "1",
        R.id.numericKey2 to "2",
        R.id.numericKey3 to "3",
        R.id.numericKey4 to "4",
        R.id.numericKey5 to "5",
        R.id.numericKey6 to "6",
        R.id.numericKey7 to "7",
        R.id.numericKey8 to "8",
        R.id.numericKey9 to "9"
    )
    private val targetFieldId: Int
    private lateinit var inputConnection: InputConnection
    init {
        context.obtainStyledAttributes(attrs, R.styleable.NumericKeyboard).also { typedArray ->
            targetFieldId = typedArray.getResourceIdOrThrow(R.styleable.NumericKeyboard_targetField)
        }.recycle()
        View.inflate(context, R.layout.numeric_keyboard, this).apply {
            keyValues.keys.forEach { id ->
                findViewById<View>(id).setOnClickListener(this@NumericKeyboard)
            }
            findViewById<View>(R.id.numeric_key_backspace).setOnClickListener(this@NumericKeyboard)
        }
        dividerDrawable = context.getDrawable(R.drawable.keyboard_divider)
        showDividers = LinearLayout.SHOW_DIVIDER_BEGINNING or LinearLayout.SHOW_DIVIDER_MIDDLE
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        val targetField: TextView = (parent as View).findViewById(targetFieldId)
        targetField.isFocusable = false
        inputConnection = targetField.onCreateInputConnection(EditorInfo())
    }

    override fun onClick(v: View?) {
        val view = v ?: return
        if (view.id == R.id.numeric_key_backspace) {
            inputConnection.deleteSurroundingText(1, 0)
        } else if (view.id in keyValues.keys) {
            val value = keyValues[view.id] ?: return
            inputConnection.commitText(value, 1)
        }
    }
}
```

# 解説
今回の仕様に「xmlから入力対象となるEditText(TextView)をidで指定できる」があるので、xml側で指定できるよう追加のattrを `attrs.xml` で設定しています。利用する側はこんな感じです。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        tools:context=".MainActivity">

    <EditText
            android:id="@+id/target"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

    <com.yt8492.numerickeyboard.NumericKeyboard
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:targetField="@id/target"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

`NumericKeyboard` の `app:targetField="@id/target"` というようにEditTextのidを指定しています。
ここで取得したidをもとにKotlin(Java)側で `InputConnection` を生成し、押されたキーに対応する値を `inputConnection.commitText(value, 1)` で渡す(=EditTextに入力される)という実装にしています。

「キーを押している間は色が変わる」はキーを押しているときとそうでないときに色を変える `keyboard_button_background` を各キーの `background` に指定することで実現しています。