TextInputLayout とは
TextInputLayout は Android Design Support Library に含まれている、テキスト入力機能を提供する View です。マテリアルデザインの Text Fields によく従っているので、マテリアルデザインを実現したいときに重宝します。
(Text fields - Components - Google design guidelines より)
しかしながら、最新の v23.1.1 においても、知らないとハマってしまう箇所がいくつかあります。
この記事では、TextInputLayout を使う際に気を付けた方が良い点について解説します。
Tips
1. 子供の EditText
にも id を振った方が良い
TextInputLayout
は以下のように、EditText
を入れ子にして使います。
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_message"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/some_hints"
android:inputType="text"
android:minHeight="@dimen/text_caption"
android:textSize="@dimen/text_caption" />
</android.support.design.widget.TextInputLayout>
このとき、 TextInputLayout#getEditText() で、子供の EditText
が取得できるので、わざわざ EditText
に id を振る必要はないように見えます。そして、実際、なくても動くように見えます。
しかし、画面を回転させてみましょう。入力途中のテキストが消えるはずです。
これは何故かというと、Android SDK が自動的に View の状態を保存/復元するときに、View が ID を持っているかどうかが判断の基準になるからです。Android Developers にも、以下のようにしっかり書かれています。
注: Android システムがアクティビティのビューの状態を復元できるようにするためには、各ビューは固有の ID を持っている必要があります。これは android:id によって提供されます。
(アクティビティを再作成する | Android Developers より)
ということで、使わなくても、内部の EditText
には android:id
を指定しておきましょう。
余談ですが、この ID の有無をチェックしているコードは https://github.com/android/platform_frameworks_base/blob/android-6.0.0_r7/core/java/android/view/View.java#L14740 にあります。筆者は、デバッグ実行して、ここに辿りついて、ようやく上記の仕様を思い出しました(Lint で Warning を出してほしい)。
2. ヒントをアニメーションさせたくない時は
TextInputLayout
は、忠実にマテリアルデザインの floating labels を実現するために、テキストボックスに入力を始めると、ヒントがキャプションにふわっと移動する、という Android にしては珍しく気の利いた機能が実装されています。
しかし、たまに邪魔です。
このアニメーションをオフにするメソッドは提供されていませんが、以下のようにプログラマティックに子供の EditText
にヒントを指定すれば実現できます。ちなみに、これに関しては AOSP に Issue も開かれています。
final TextInputLayout til = (TextInputLayout) findViewById(R.id.text2);
final CharSequence hint = til.getHint();
til.setHint(null);
til.getEditText().setHint(hint);
3. TextInputLayout
と baseline を合わせるには
注意! Support Library 24.2.0 から、 TextInputLayout
は LinearLayout
を継承しないため、ここで書いてある Tip は使えないようです(参照: https://code.google.com/p/android/issues/detail?id=223772)。
@litmon さん、情報ありがとうございました。
下記の内容は参考までに残しておきますが、Support Library 24.2.0 以降では動かないことにご注意ください
通常、EditText
や TextView
を水平方向に並べるときは RelativeLayout
の android:layout_alignBaseline 属性などを使うことで、テキストのベースラインが揃うようにします。
しかし、TextInputLayout
が絡むと、簡単にはいきません。なぜなら、親の TextInputLayout
に android:layout_alignBaseline
属性を指定しても、子供の EditText
のベースラインには効かないからです
そういうときは以下のように指定します。TextInputLayout
は LinearLayout
を継承しているので、この android:baselineAlignedChildIndex 属性が使えるのです。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="パイント" />
<android.support.design.widget.TextInputLayout
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAlignedChildIndex="0"
android:layout_alignBaseline="@id/label"
android:layout_toLeftOf="@id/label">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="飲んだビールの量" />
</android.support.design.widget.TextInputLayout>
4. カウンターやエラーメッセージの見た目を変更するには
カウンターやエラーメッセージのスタイルは以下のように変更することができます。
<android.support.design.widget.TextInputLayout
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:counterEnabled="true"
app:counterMaxLength="10"
app:counterTextAppearance="@style/counterText"
app:counterOverflowTextAppearance="@style/counterOverflowText"
app:errorTextAppearance="@style/errorText">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.TextInputLayout>
<style name="counterText">
<item name="android:textColor">@android:color/holo_green_dark</item>
</style>
<style name="counterOverflowText">
<item name="android:textStyle">bold</item>
</style>
<style name="errorText">
<item name="android:textColor">@android:color/holo_red_dark</item>
<item name="android:textStyle">bold</item>
</style>
なお、TextInputLayout の実装を読むと、エラーテキストとカウンターは以下のような LinearLayout
で表される mIndicatorArea
の子要素として動的に追加されるようです(**注:**生成される View がどういうものか表すための概念としての XML です。実際にこの XML が使われるわけではありません)。
<LinearLayout
android:id="@+id/indicatorArea"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/errorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/counterView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"/>
</LinearLayout>
互いの表示/非表示に関わらず、エラーメッセージが左寄せ、カウンターが右寄せになることが分かります。
5. エラーメッセージ非表示時に View の高さが戻らない問題
複数行にまたがるエラーメッセージを setError(CharSequence)
で表示したあとに setError(null)
を呼ぶと、エラーメッセージの View の visibility が View.INVISIBLE
になりますが、 View 自体は削除されないため、下部に無駄なスペースが残ってしまいます。
これを元に戻すには setErrorEnabled(false)
を呼びます。これによってエラーメッセージの View の visibility が View.GONE
になります。
final TextInputLayout til = (TextInputLayout) findViewById(R.id.text2);
til.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (s.toString().contains("error")) {
til.setError("WRYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY!");
} else {
til.setErrorEnabled(false);
}
}
おわりに
Design Support Library で提供される TextInputLayout を使う際に注意する点や tips を紹介しました。
ググると分かりますが、サポートライブラリのバージョンによって挙動が異なるようです。たとえば、Working with the EditText · codepath/android_guides Wiki ではカウンタの表示が左寄せですが、最新の v23.1.1 では右寄せになっています。使う際には、そのあたりにも注意が必要になるかもしれません。