
【Android】Webでよくみる入力Boxを手作り
2022.04.18
Webでよくみる入力Boxを手作り
(株)ライトコードでモバイルアプリケーション開発をしている笹川(ささがわ)です!
今日は様々なWebページで見かける入力フォームにある入力Boxを2つのパターンで作っていきたいと思います!

シンプルな入力Boxを作りたい場合はTextInputLayoutを使おう!
TextInputLayoutとはMaterial DesignのText Fieldsを実現できるレイアウトです
導入、カスタマイズのの仕方は調べればすぐにわかると思うのでここでは割愛しますね
自分で作りたい場合はCustomViewを作ろう!
ここからがメインです
フォームは様々な例がありますが、今回は「枠線」「文字数制限」「エラー表示」を対応したものを作ります

まずはレイアウトを先に組み上げます
今回はLinearLayoutとAppCompatEditTextとTextViewの3つを使って作ります
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <?xml version="1.0" encoding="utf-8"?> <layout> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/frame" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="65" android:background="@drawable/input_text_border" android:orientation="vertical" android:padding="1dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.appcompat.widget.AppCompatEditText android:id="@+id/edit_text" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="12dp" android:layout_marginBottom="12dp" android:layout_weight="1" android:background="@color/white" android:gravity="start|center_vertical" android:paddingStart="16dp" android:paddingEnd="16dp" android:textSize="14sp" tools:hint="メールアドレス" tools:text="" /> <TextView android:id="@+id/error_text_title" android:layout_width="match_parent" android:layout_height="20dp" android:background="@color/highlight_red" android:gravity="center" android:padding="2dp" android:text="100文字以内で入力" android:textColor="@color/error" android:textSize="11sp" android:textStyle="bold" android:visibility="gone" tools:visibility="visible" /> </LinearLayout> </layout> |
ボーダーに関しては下記をLinearLayoutの背景に指定しています
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="true" android:state_hovered="false" android:state_selected="false"> <shape android:shape="rectangle"> <solid android:color="@android:color/white" /> <stroke android:width="1dp" android:color="@color/high_light" /> </shape> </item> <item android:state_enabled="false" android:state_hovered="false" android:state_selected="false"> <shape android:shape="rectangle"> <solid android:color="@android:color/white" /> <stroke android:width="1dp" android:color="@color/white" /> </shape> </item> <item android:state_selected="true"> <shape android:shape="rectangle"> <solid android:color="@android:color/white" /> <stroke android:width="1dp" android:color="@color/error" /> </shape> </item> <item android:state_hovered="true"> <shape android:shape="rectangle"> <solid android:color="@android:color/white" /> <stroke android:width="1dp" android:color="@color/error" /> </shape> </item> </selector> |
フォーカスのON/OFFとエラー状態によってボーダーの色を変えるようにしています
stateの変更はKotlin/Java側で対応が必要です(先ほどLinearLayoutでIDを指定したのはこのためです
CustomViewクラスはこんな感じになります
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | package com.example.exampleinputtext import android.annotation.SuppressLint import android.content.Context import android.text.Editable import android.text.Spannable import android.text.style.ForegroundColorSpan import android.util.AttributeSet import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.widget.doAfterTextChanged import com.example.exampleinputtext.databinding.InputTextViewBinding class InputTextView : ConstraintLayout { lateinit var binding: InputTextViewBinding /** 入力された文字 */ var inputText: String set(value) { binding.editText.setText(value) updateBorderColor(false, errorLength, inputText, binding.frame, binding.errorText) resizeHeight() } get() = binding.editText.text?.toString() ?: "" /** 最大文字数 */ var errorLength: Int = 100 /** 現在の行数 最低でも1になる */ private val lineCount: Int get() = if (binding.editText.lineCount > 0) binding.editText.lineCount else 1 private val margin: Int get() { val errorHeight = if (binding.errorText.visibility == View.VISIBLE) { getDimensionPixelSize(R.dimen.input_text_error_height) } else { 0 } return getDimensionPixelSize(R.dimen.space_28dp) + errorHeight } private val lineHeight: Int get() = (lineCount * getDimensionPixelSize(R.dimen.input_text_line_height)) + margin constructor(context: Context) : super(context) { init(null, 0) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(attrs, 0) } constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { init(attrs, defStyle) } override fun onFinishInflate() { super.onFinishInflate() setListener() } override fun dispatchKeyEventPreIme(event: KeyEvent?): Boolean { // フォーム外タップはキーボード閉じる if (event?.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { closedKeyboard() } return super.dispatchKeyEventPreIme(event) } @SuppressLint("CustomViewStyleable") fun init(attributeSet: AttributeSet?, defStyle: Int) { binding = InputTextViewBinding.inflate(LayoutInflater.from(context), this, true) // エラー文字数や各種初期定義はstyleで受け取ってここで定義の方がスマート } private fun setListener() { binding.editText.setOnFocusChangeListener { _, hasFocus -> updateBorderColor( hasFocus = hasFocus, errorLength = errorLength, inputText = inputText, frame = binding.frame, errorText = binding.errorText ) updateTextColor(binding.editText.text, errorLength) resizeHeight() } binding.editText.doAfterTextChanged { if (!binding.editText.isEnabled) return@doAfterTextChanged updateBorderColor( errorLength = errorLength, inputText = inputText, frame = binding.frame, errorText = binding.errorText ) updateTextColor(it, errorLength) resizeHeight() } } private fun updateTextColor(inputText: Editable?, errorLength: Int) { if (inputText == null || inputText.isEmpty()) return val start = if (inputText.length > errorLength) errorLength else 0 val color = if (inputText.length > errorLength) R.color.error else R.color.black if (start >= inputText.length) return inputText.setSpan( ForegroundColorSpan(ContextCompat.getColor(context, color)), start, inputText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } private fun closedKeyboard() { updateBorderColor(false, errorLength, inputText, binding.frame, binding.errorText) binding.editText.clearFocus() } private fun updateBorderColor(hasFocus: Boolean = true, errorLength: Int, inputText: String, frame: ViewGroup, errorText: TextView) { val isLengthError = inputText.length > errorLength frame.isSelected = isLengthError frame.isEnabled = hasFocus errorText.visibility = if (isLengthError && hasFocus) View.VISIBLE else View.GONE } private fun resizeHeight() { if (measuredHeight == 0) return // EditTextをwrapしているので、高さは自分でリサイズしていく必要がある layoutParams.height = lineHeight + (getDimensionPixelSize(R.dimen.line_1dp) * 2) requestLayout() } private fun getDimensionPixelSize(res: Int): Int = context.resources.getDimensionPixelSize(res) } |
今回のようなEditTextをwrapするような形で作ると改行等による高さ調整が自動でできなくなります
そうなると使い勝手が悪いので文字入力やフォーカス変更のタイミングで高さ調整がされるように実装しています
また文字数制限を実装していますが、入力制限ではなくエラー表示と文字色の変更、ボーダーカラーの変更でエラーを表現しています
Androidでは文字数制限を簡単に実装できますが、今回のようなフォームではコピー&ペーストで入力されることも考慮して制限をいれません
使いたい画面に設定
先ほど作成したクラスを使いたい画面のlayout.xmlに定義します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.example.exampleinputtext.InputTextView android:id="@+id/input_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="32dp" android:paddingStart="16dp" android:paddingEnd="16dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> |
サンプル
こちらのリポジトリにサンプルを置いておきますので参考にどうぞ
これでアプリを実行しますとこのような感じになります!

書いた人はこんな人

- 新潟生まれ新潟育ち本業はモバイルアプリエンジニア。
日々、猫(犬)エンジニアとして活躍中!
IT技術9月 20, 2023開発効率を少しだけ上げるGithubActionsの便利な使い方
IT技術4月 7, 2023【ISUCON部】ライトコードISUCON部 始動!
IT技術4月 18, 2022【Android】Webでよくみる入力Boxを手作り
IT技術1月 19, 2022【Android】SeekbarでスイッチなUIを作る