🦖

Android viewでPinCodeレイアウトを実装

2023/08/26に公開

はじめに

ピンコードの入力欄を実装する必要があったので得た知見をまとめていきます。
まだまだAndroid開発を始めたばかりなので「もっとこうしたほうがいいよ」などあれば教えていただけると幸いです。

ここではピンコードは4文字として説明しますが、この実装方法はどの文字数でも可能です。

実装

まずは結論から、以下が実際の動作とコード全文です。

Android Studioのlogcatから動画をとり、ffmpegでgifに変換しています。
https://qiita.com/wMETAw/items/fdb754022aec1da88e6e
記事を参考にさせていただきました。

activity_main.xml
<!-- 省略 -->
<EditText
    android:id="@+id/pinCodeEditText"
    android:layout_width="match_parent"
    android:layout_height="1dp"
    android:autofillHints=""
    android:inputType="number"
    android:maxLength="4"
    android:visibility="visible" />

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="30dp"
    android:gravity="center"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/pinCodeTextView1"
        style="@style/PinCodeEditText" />

    <TextView
        android:id="@+id/pinCodeTextView2"
        style="@style/PinCodeEditText" />

    <TextView
        android:id="@+id/pinCodeTextView3"
        style="@style/PinCodeEditText" />

    <TextView
        android:id="@+id/pinCodeTextView4"
        style="@style/PinCodeEditText" />
</LinearLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // TextViewをタップしたらEditTextにフォーカスが移るようにする
        listOf(
            binding.pinCodeTextView1,
            binding.pinCodeTextView2,
            binding.pinCodeTextView3,
            binding.pinCodeTextView4
        ).also {
            it.forEachIndexed { index, textView ->
                textView.setOnClickListener {
                    binding.pinCodeEditText.requestFocus()
                    val inputMethodManager =
                        getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                    inputMethodManager.showSoftInput(
                        binding.pinCodeEditText,
                        InputMethodManager.SHOW_IMPLICIT
                    )
                }
            }
        }

        binding.pinCodeEditText.addTextChangedListener(onTextWatcher(binding))
    }

    // テキスト変更されるたびに、TextViewにテキストをセットする
    private fun onCustomTextChange(binding: ActivityMainBinding) = object : TextWatcher {
        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
        override fun afterTextChanged(p0: Editable?) = Unit

        override fun onTextChanged(s: CharSequence?, p1: Int, p2: Int, p3: Int) {
            s?.let { text ->
                binding.pinCodeTextView1.text = text.getString(0)
                binding.pinCodeTextView2.text = text.getString(1)
                binding.pinCodeTextView3.text = text.getString(2)
                binding.pinCodeTextView4.text = text.getString(3)
            }
        }
    }

    private fun CharSequence.getString(index: Int) = if (this.length > index) {
        this[index].toString()
    } else {
        " "
    }
}

実装の説明

簡単に説明すると

  • ユーザには見えないEditTextを用意
  • ピンコードの文字数分のTextViewを用意
  • EditTextに入力された文字をTextViewに表示する

以上の方法でピンコード入力欄を実現します。

では少し詳しく説明していきます。

EditTextの作成

<EditText
    android:id="@+id/pinCodeEditText"
    android:layout_width="match_parent"
    android:layout_height="1dp"
    android:autofillHints=""
    android:inputType="number"
    android:maxLength="4"
    android:visibility="visible" />

今回の実装ではピンコードは4文字としていますのでmaxLength="4"にしています

visibilitygoneinvisibleにしていません。
実際にはvisibleheight="1dp"等にして見えないようにしています。

TextViewの作成

こちらはピンコードの文字数分のTextViewを用意するのみです。

TextViewをタップしたら、EditTextにフォーカスが移るようにする

listOf(
    binding.pinCodeTextView1,
    binding.pinCodeTextView2,
    binding.pinCodeTextView3,
    binding.pinCodeTextView4
).also {
    it.forEachIndexed { index, textView ->
        textView.setOnClickListener {
            binding.pinCodeEditText.requestFocus()
            val inputMethodManager =
                getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            inputMethodManager.showSoftInput(
                binding.pinCodeEditText,
                InputMethodManager.SHOW_IMPLICIT
            )
        }
    }
}

4つのTextViewに対して、タップした際にEditTextにフォーカスが移るようにします。
フォーカスが移るようにしただけではキーボードが表示されないので、InputMethodManagerでキーボードが表示されるようにします。

テキストが変更されるたびにTextViewにテキストをセットする

private fun onCustomTextChange(binding: ActivityMainBinding) = object : TextWatcher {
    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
    override fun afterTextChanged(p0: Editable?) = Unit

    override fun onTextChanged(s: CharSequence?, p1: Int, p2: Int, p3: Int) {
        s?.let { text ->
            binding.pinCodeTextView1.text = text.getString(0)
            binding.pinCodeTextView2.text = text.getString(1)
            binding.pinCodeTextView3.text = text.getString(2)
            binding.pinCodeTextView4.text = text.getString(3)
        }
    }
}

private fun CharSequence.getString(index: Int) = if (this.length > index) {
    this[index].toString()
} else {
    " "
}

拡張関数を用いて少し分かりにくいかもしれませんが、やっていることは以下のとおりです。

s?.let { text ->
    // テキストが1文字以上のときはテキストの0番目(1文字目)の文字を格納する
    if (text.toString().length > 0) {
        binding.pinCodeTextView1.text = text[0].toString()
    }

    // テキストが2文字以上のときはテキストの1番目(2文字目)の文字を格納する
    if (text.toString().length > 1) {
        binding.pinCodeTextView2.text = text[1].toString()
    }
    // 省略
}

ここで、下記のようにtext[1]が空文字ではなかったらという処理にしないのは、そもそもEditText1文字しか入力されていない場合、text[1]を参照しようとした時点でStringIndexOutOfBoundsExceptionが発生して、アプリが落ちるからです。

s?.let { text ->
    // テキストが1文字以上のときはテキストの0番目(1文字目)の文字を格納する
    binding.pinCodeTextView1.text = if (text[0].toString.isNotEmpty) text[0].toString else ""

    // テキストが2文字以上のときはテキストの1番目(2文字目)の文字を格納する
    binding.pinCodeTextView2.text = if (text[1].toString.isNotEmpty) text[1].toString else ""
    // 省略
}

まとめ

ここまで書きましたが、ピンコード入力欄の実装は一つのEditTextやるのが一番簡単です。
そのほうが実装も簡単ですし、バグも発生しないです。

ただ要件によっては入力欄を一文字ずつに見せる必要があるときなどは、今回のやり方を参考にしてみてください。

また、こういったPINコードの実装に関しては「OTP」という単語でしらべると沢山でてきます。

ここまでお読みいただき、ありがとうございました。

Discussion