Android viewでPinCodeレイアウトを実装
はじめに
ピンコードの入力欄を実装する必要があったので得た知見をまとめていきます。
まだまだAndroid開発を始めたばかりなので「もっとこうしたほうがいいよ」などあれば教えていただけると幸いです。
ここではピンコードは4文字として説明しますが、この実装方法はどの文字数でも可能です。
実装
まずは結論から、以下が実際の動作とコード全文です。
Android Studioのlogcatから動画をとり、ffmpegでgifに変換しています。
https://qiita.com/wMETAw/items/fdb754022aec1da88e6e
記事を参考にさせていただきました。
<!-- 省略 -->
<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>
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"
にしています
visibility
はgone
やinvisible
にしていません。
実際にはvisible
でheight="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]
が空文字ではなかったらという処理にしないのは、そもそもEditText
に1
文字しか入力されていない場合、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