前の Codelab で学んだように、マテリアルは Google が作成したデザイン システムであり、ユーザー インターフェース設計のベスト プラクティスをサポートするガイドライン、コンポーネント、ツールで構成されています。この Codelab では、チップ計算アプリ(前の Codelabで作成)を更新して、次の最終スクリーンショットに示されている洗練されたユーザー エクスペリエンスを実現します。また、ユーザー エクスペリエンスができるだけスムーズなものになるように、いくつかの追加のシナリオでアプリをテストします。

前提条件
- 一般的な UI ウィジェット(
TextView、ImageView、Button、EditText、RadioButton、RadioGroup、Switchなど)に精通している ConstraintLayoutと、制約を設定して子ビューを配置する処理に精通している- XML レイアウトの変更に慣れている
- ビットマップ画像とベクター型ドローアブルの違いを理解している
- テーマにテーマ属性を設定できる
- デバイスでダークテーマを有効にできる
- アプリの
build.gradleファイルでプロジェクトの依存関係を変更した経験がある
学習内容
- アプリでマテリアル デザイン コンポーネントを使用する方法
- Image Asset Studio からマテリアル アイコンをインポートしてアプリで使用する方法
- 新しいスタイルを作成し、適用する方法
- 色以外のテーマ属性を設定する方法
作成するアプリの概要
- 推奨される UI のベスト プラクティスに基づく洗練されたチップ計算アプリ
必要なもの
- Android Studio がインストールされているパソコン
- 前の Codelab で作成した Tip Time アプリのコード
前の Codelab では、Tip Time アプリを作成しました。このアプリは、チップをカスタマイズするオプションを含むチップ計算アプリです。現在、アプリの UI は以下のスクリーンショットのように表示されます。機能は問題ありませんが、プロトタイプのように見えます。フィールドが見やすく配置されていません。スタイルと間隔の一貫性、およびマテリアル デザイン コンポーネントの使用という観点から見ると、明らかに改善の余地があります。

マテリアル コンポーネントは、アプリにマテリアル スタイル設定を簡単に実装できる一般的な UI ウィジェットです。このドキュメントでは、マテリアル デザイン コンポーネントの使用方法とカスタマイズ方法について説明します。コンポーネントにはそれぞれ一般的なマテリアル デザイン ガイドラインがあり、Android で利用可能なコンポーネントについては Android プラットフォーム固有のガイダンスがあります。選択しているプラットフォームにコンポーネントが存在しない場合は、ラベル付きの図を参照して、コンポーネントを再作成するための十分な情報を得ることができます。

マテリアル コンポーネントを使用すると、作成するアプリの動作とユーザーのデバイス上の他のアプリの動作の一貫性が向上します。つまり、あるアプリで習得した UI パターンを別のアプリでも使用できます。そのため、ユーザーは新しいアプリの使い方をより短時間で習得できます。可能な場合は、非マテリアル ウィジェットではなく、マテリアル コンポーネントを使用することをおすすめします。また、次のタスクで学習するように、マテリアル コンポーネントはより柔軟なカスタマイズが可能です。
マテリアル デザイン コンポーネント(MDC)ライブラリをプロジェクトの依存関係として含める必要があります。Android Studio 4.1 以降を使用している場合は、デフォルトで次の行がプロジェクトに存在するはずです。アプリの build.gradle ファイルで、この依存関係が最新バージョンのライブラリに含まれていることを確認します。詳しくは、マテリアルのサイトのスタートガイド ページをご覧ください。
app/build.gradle
dependencies {
...
implementation 'com.google.android.material:material:<version>'
}
テキスト フィールド
現在、チップ計算アプリでは、レイアウトの上部に、サービス料金を表す EditText フィールドが表示されます。この EditText フィールドは機能に問題はありませんが、テキスト フィールドの外観と動作に関する最近のマテリアル デザイン ガイドラインに従っていません。
新しいコンポーネントを使用する際は、まずマテリアルのサイトでそのコンポーネントについて確認してください。テキスト フィールドに関するガイドによると、テキスト フィールドには次の 2 つのタイプがあります。
塗りつぶしテキスト フィールド

枠線付きテキスト フィールド

上記のテキスト フィールドを作成するには、MDC ライブラリの TextInputEditText を包含する TextInputLayout を使用します。マテリアル テキスト フィールドは、次のように簡単にカスタマイズできます。
- 入力テキストまたは常時表示されるラベルを表示する
- テキスト フィールド内にアイコンを表示する
- ヘルパーまたはエラー メッセージを表示する
この Codelab の最初のタスクでは、サービス料金の EditText を、TextInputLayout と TextInputEditText で構成されるマテリアル テキスト フィールドに置き換えます。
- Android Studio で Tip Time アプリを開いたまま、
activity_main.xmlレイアウト ファイルに移動します。このファイルにはチップ計算レイアウトのConstraintLayoutが含まれている必要があります。 - マテリアル テキスト フィールドに対応する XML のサンプルについては、Android のガイダンスに戻ってテキスト フィールドをご覧ください。次のようなスニペットを確認できます。
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
- このサンプルを確認した後、マテリアル テキスト フィールドを
ConstraintLayoutの最初の子として(EditTextフィールドの前に)挿入します。EditTextフィールドは、この後のステップで削除します。
Android Studio でこれを入力する場合は、オートコンプリートを使用して簡単に入力できます。または、ドキュメントのページからサンプル XML をコピーして、次のようにレイアウトに貼り付けることもできます。TextInputLayout に子ビュー TextInputEditText があることに注目してください。実際に変更された XML の行に注意を向けられるように、省略記号(...)を使用してスニペットの表示を簡潔にしています。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/cost_of_service" ... />
...
TextInputLayout 要素に誤りがあることにお気づきでしょうか。このビューは、まだ親 ConstraintLayout 内で適切に制限されていません。また、文字列リソースが認識されていません。これらの誤りはこの後のステップで修正します。

- 親
ConstraintLayout内にテキスト フィールドを適切に配置するには、テキスト フィールドに縦方向と横方向の制約を追加します。まだEditTextを削除していないため、次の属性をEditTextから切り取って貼り付け、TextInputLayoutに配置します: 制約、リソース ID(cost_of_service)、レイアウトの幅(160dp)、レイアウトの高さ(wrap_content)、チップのテキスト(@string/cost_of_service)。
...
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:hint="@string/cost_of_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
...
cost_of_service ID が EditText のリソース ID と同じであるというエラーが表示される場合がありますが、このエラーは今のところ無視してかまいません(EditText はこの後のステップで削除します)。
- 次に、
TextInputEditText要素に適切な属性がすべて設定されていることを確認します。EditTextからの入力タイプを切り取ってTextInputEditText.に貼り付け、TextInputEditText要素のリソース ID をcost_of_service_edit_text.に変更します。
<com.google.android.material.textfield.TextInputLayout ... >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/cost_of_service_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>
match_parent の幅と wrap_content の高さはそのままでかまいません。match_parent の幅を設定すると、TextInputEditText の幅はその親である TextInputLayout と同じ(160dp)になります。
- 以上で
EditTextからすべての関連情報をコピーしたので、次はレイアウトからEditTextを削除します。 - レイアウトのデザインビューに、次のようなプレビューが表示されます。サービス料金フィールドがマテリアル テキスト フィールドらしく表示されています。

MainActivity.ktファイルのcalculateTip()メソッドに誤りがあるため、まだアプリを実行することはできません。以前の Codelab で、プロジェクトでビュー バインディングが有効になっていると、リソース ID 名に基づいてバインディング オブジェクト内にプロパティが作成されたことを思い出してください。サービス料金の取得元フィールドが XML レイアウトで変更されたため、それに応じて Kotlin コードを更新する必要があります。
リソース ID が cost_of_service_edit_text の TextInputEditText 要素から、ユーザー入力を取得します。MainActivity 内で、binding.costOfServiceEditText を使用して、その中に格納されているテキスト文字列にアクセスします。calculateTip() メソッド残りの部分は同じままでかまいません。
private fun calculateTip() {
// Get the decimal value from the cost of service text field
val stringInTextField = binding.costOfServiceEditText.text.toString()
val cost = stringInTextField.toDoubleOrNull()
...
}
- おつかれさまでした。アプリを実行して、前と同様に動作することを確認してください。入力すると、[Cost of Servive] ラベルが入力の上に表示されます。以前と同様、想定どおりにチップが計算されるはずです。

スイッチ
マテリアル デザイン ガイドラインには、スイッチに関するガイダンスもあります。スイッチとは、設定のオン / オフを切り替えられるウィジェットです。
- マテリアル スイッチについては、Android のガイダンスをご確認ください。スイッチ用のマテリアル スタイル設定を提供する
SwitchMaterialウィジェット(MDC ライブラリにあります)について学習できます。引き続きガイドをスクロールすると、サンプル XML が表示されます。 SwitchMaterialを使用するには、レイアウト内でSwitchMaterialを明示的に指定し、完全修飾パス名を使用する必要があります。
activity_main.xml レイアウト内で、XML タグを Switch から com.google.android.material.switchmaterial.SwitchMaterial. に変更します。
...
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
android:layout_width="0dp"
android:layout_height="wrap_content" ... />
...
- アプリを実行して、前と同様に動作することを確認します。アプリに目に見える変化はありません。ただし、Android プラットフォームの
Switchの代わりに MDC ライブラリのSwitchMaterialを使用するメリットは、SwitchMaterial用のライブラリの実装が更新されたとき(例: マテリアル デザイン ガイドラインが変更されたとき)に、アプリを一切変更せずに、更新されたウィジェットを無料で利用できることです。これは、アプリを将来も有効に使い続けるために役立ちます。
ここまでは、すぐに使えるマテリアル デザイン コンポーネントの使用が UI にもたらすメリットと、アプリをマテリアル ガイドラインに近づける方法に関する 2 つの例を見てきました。Android で提供されるその他のマテリアル デザイン コンポーネントについては、こちらのサイトでいつでも確認できます。
アイコンとは、目的の機能を視覚的に伝達することで、ユーザーがユーザー インターフェースを理解することを助けるシンボルです。多くの場合、アイコンはユーザーがよく知っていると思われる現実の物体から着想を得ています。ほとんどのアイコンのデザインは、ユーザーが即座に理解できる最小限のレベルまで簡略化されています。たとえば、現実の鉛筆は字を書くために使用されるので、通常、鉛筆のアイコンはアイテムの作成、追加、編集を表します。
|
|
フロッピー ディスク アイコンのように、かつて実際にあった物体に関連するアイコンもあります。このアイコンは、ファイルやデータベース レコードの保存を表すためによく使われます。しかし、フロッピー ディスクは 1970 年代は一般的でしたが、2000 年以降はほとんど使われなくなりました。それにもかかわらずこのアイコンが今でも使われているということは、強力な視覚的デザインが物質的形態としての寿命を越えていかに長く生き続けるかを物語っています。
|
|
アプリ内のアイコンの表現
アプリ内のアイコンでは、画面密度ごとに異なるバージョンのビットマップ画像を使用する代わりに、ベクター型ドローアブルを使用することをおすすめします。ベクター型ドローアブルは、画像を構成する実際のピクセルを保存するのではなく、画像の作成方法を指示する XML ファイルとして表現されます。ベクター型ドローアブルは、画質の低下とファイルサイズの増大を招くことなく、拡大または縮小できます。
提供されるアイコン
マテリアル デザインでは、多くのニーズに対応した多数のアイコンが一般的なカテゴリ別に用意されています。アイコンの一覧をご覧ください。

また、アイコンは 5 つのテーマ(塗りつぶし、枠線付き、角丸、ツートン、鋭角)のいずれかを使用して描画できます。色を使用して色合いを調整することもできます。
塗りつぶし | 枠線付き | 角丸 | ツートン | 鋭角 |
|
|
|
|
|
アイコンを追加する
このタスクでは、次の 3 つのベクター型ドローアブル アイコンをアプリに追加します。
- サービス料金テキスト フィールドの横のアイコン
- サービスに関する質問の横のアイコン
- チップの端数切り上げプロンプトの横のアイコン
アプリの最終バージョンのスクリーンショットは以下のとおりです。アイコンを追加した後、アイコンの配置に合わせてレイアウトを調整します。フィールドと計算ボタンが右にずらされ、アイコンが追加されていることに注目してください。

ベクター型ドローアブル アセットを追加する
これらのアイコンは、Android Studio の Asset Studio から直接、ベクター型ドローアブルとして作成できます。
- アプリ ウィンドウの左側にある [Resource Manager] タブを開きます。
- プラス(+)アイコンをクリックして [Vector Asset] を選択します。

- [Asset Type] で [Clip Art] ラジオボタンがオンになっていることを確認します。

- [Clip Art] の横のボタンをクリックして、別のクリップアート画像を選択します。表示されたプロンプトで、ウィンドウに「call made」と入力します。この矢印アイコンは、チップの端数切り上げオプションに使用します。このアイコンを選択して [OK] をクリックします。

- アイコンの名前を
ic_round_upに変更します(アイコン ファイルの名前には、接頭辞 ic_ を使用することをおすすめします)。[**Size*] は 24 dp x 24 dp、[**Color**] は黒(000000)のままでかまいません。 - [Next] をクリックします。
- デフォルトのディレクトリの場所を受け入れて [Finish] をクリックします。

- 他の 2 つのアイコンについて、ステップ 2~7 を繰り返します。
- サービスに関する質問のアイコン:「room service」アイコンを検索し、
ic_serviceとして保存します。 - サービス料金のアイコン:「store」アイコンを検索し、
ic_storeとして保存します。
- 完了すると、Resource Manager が以下のスクリーンショットのように表示されます。また、これら 3 つのベクター型ドローアブル(
ic_round_up、ic_service、ic_store)がres/drawableフォルダ内に表示されます。

古い Android バージョンをサポートする
たった今ベクター型ドローアブルをアプリに追加しましたが、Android プラットフォームでのベクター型ドローアブルのサポートは、Android 5.0(API レベル 21)未満では追加されていないことに注意する必要があります。
プロジェクトを設定した方法に基づき、Tip Time アプリの最小 SDK バージョンは API 19 です。つまり、このアプリを実行できるのは、Android プラットフォーム バージョン 19 以上を搭載した Android デバイスです。
それより古いバージョンの Android でアプリを動作させる(いわゆる下位互換性を維持する)には、アプリの build.gradle ファイルに vectorDrawables 要素を追加します。そうすれば、API 21 未満のバージョンのプラットフォームでベクター型ドローアブルを使用できます。プロジェクトのビルド時に PNG に変換する必要はありません。詳しくは、こちらをご覧ください。
app/build.gradle
android {
defaultConfig {
...
vectorDrawables.useSupportLibrary = true
}
...
}
プロジェクトが適切に構成されていれば、これでアイコンをレイアウトに追加できるようになります。
アイコンと位置要素を挿入する
アプリ内にアイコンを表示するには、ImageViews を使用します。最終的な UI は次のように表示されます。

activity_main.xmlレイアウトを開きます。- まず、サービス料金テキスト フィールドの横にストアのアイコンを配置します。
ConstraintLayoutの最初の子として、TextInputLayoutの前に新しいImageViewを挿入します。
<androidx.constraintlayout.widget.ConstraintLayout
...>
<ImageView
android:layout_width=""
android:layout_height=""
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
...
ic_storeアイコンを保持するImageViewに適切な属性を設定します。ID をicon_cost_of_serviceに設定します。app:srcCompat属性をドローアブル リソース@drawable/ic_storeに設定します。これにより、XML の該当行の横にアイコンのプレビューが表示されます。この画像は装飾目的でのみ使用するので、android:importantForAccessibility="no"も設定します。
<ImageView
android:id="@+id/icon_cost_of_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_store" />
ビューがまだ制限されていないため、ImageView にエラーがあるはずです。これは後で修正します。
- 2 つのステップで
icon_cost_of_serviceの位置を調整します。最初に(このステップで)ImageViewに制約を追加し、次に(ステップ 5 で)その横にあるTextInputLayoutの制約を更新します。次の図は、制約の設定方法を示しています。

ImageView で、その始端を親ビューの始端に制限します(app:layout_constraintStart_toStartOf="parent")。
アイコンは横にあるテキスト フィールドに対して縦方向の中央に表示されるため、この ImageView の最上部(layout_constraintTop_toTopOf)をテキスト フィールドの最上部に制限します。この ImageView の最下部(layout_constraintBottom_toBottomOf)をテキスト フィールドの最下部に制限します。テキスト フィールドを参照するには、リソース ID @id/cost_of_service を使用します。デフォルトの動作では、2 つの制約が同じディメンション(最上部と最下部の制約など)でウィジェットに適用される場合、制約は均等に適用されます。その結果、アイコンはサービス料金フィールドに対して縦方向の中央に配置されます。
<ImageView
android:id="@+id/icon_cost_of_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_store"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/cost_of_service"
app:layout_constraintBottom_toBottomOf="@id/cost_of_service" />
デザインビューでは、アイコンとテキスト フィールドが依然として重なり合っています。これは次のステップで修正します。
- アイコンを追加する前、テキスト フィールドは親の開始位置に配置されていました。これを右にずらす必要があります。
icon_cost_of_serviceに関するcost_of_serviceテキスト フィールドの制約を更新します。

TextInputLayout の始端を ImageView(@id/icon_cost_of_service)の終端に制限する必要があります。2 つのビューの間隔を広げるには、TextInputLayout に開始余白 16dp を追加します。
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
...
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/icon_cost_of_service">
<com.google.android.material.textfield.TextInputEditText ... />
</com.google.android.material.textfield.TextInputLayout>
以上の変更が完了したら、アイコンをテキスト フィールドの真横に配置する必要があります。

- 次に、[How was the service?] の
TextViewの横に、サービスを表すベルのアイコンを挿入します。ImageViewはConstraintLayout内の任意の場所で宣言できますが、XML レイアウトで新しいImageViewをTextInputLayoutの後、service_questionTextViewの前に挿入すると、XML レイアウトが読みやすくなります。
新しい ImageView には、@+id/icon_service_question のリソース ID を割り当てます。ImageView とサービスに関する質問の TextView に適切な制約を設定します。

また、service_question TextView に 16dp の上余白を追加し、サービスに関する質問とその上にあるサービス料金テキスト フィールドの間にある縦の間隔を広げます。
...
<ImageView
android:id="@+id/icon_service_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/service_question"
app:layout_constraintBottom_toBottomOf="@id/service_question" />
<TextView
android:id="@+id/service_question"
...
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@id/cost_of_service"
app:layout_constraintTop_toBottomOf="@id/cost_of_service"/>
...
- この時点で、デザインビューは次のように表示されます。サービス料金フィールドとサービスに関する質問(およびそれぞれのアイコン)の配置は改善されましたが、今度はラジオボタンの位置がずれています。上にあるコンテンツと縦の位置が揃っていません。

- サービスに関する質問の下にあるラジオボタンを右にずらして、位置を調整します。つまり、
RadioGroup制約を更新します。RadioGroupの始端をservice_questionTextViewの始端に制限します。RadioGroupの残りの属性はすべて同じままでかまいません。

...
<RadioGroup
android:id="@+id/tip_options"
...
app:layout_constraintStart_toStartOf="@id/service_question">
...
- 次に、[Round up tip?] スイッチの横にあるレイアウトに
ic_round_upアイコンを追加します。これはご自分でトライしてください。行き詰ったときは、下記の XML が参考になります。新しいImageViewには、リソース IDicon_round_upを割り当てることができます。 - レイアウト XML で、
RadioGroupの後、SwitchMaterialウィジェットの前に、新しいImageViewを挿入します。 ImageViewにリソース IDicon_round_upを割り当てて、srcCompatをアイコン@drawable/ic_round_upのドローアブルに設定します。ImageViewの開始位置を親の開始位置に制限し、また、SwitchMaterialを基準としてアイコンを縦方向の中央に配置します。- アイコンの横に配置されるように
SwitchMaterialを更新し、16dpの開始余白を設定します。結果として生成される XML は、icon_round_upおよびround_up_switchのようになります。
...
<ImageView
android:id="@+id/icon_round_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_round_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/round_up_switch"
app:layout_constraintBottom_toBottomOf="@id/round_up_switch" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
...
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/icon_round_up" />
...
- デザイン ビューは次のようになります。3 つのアイコンがすべて正しく配置されています。

- これをアプリの最終スクリーンショットと比べると、計算ボタンも、サービス料金フィールド、サービスに関する質問、ラジオボタンのオプション、チップの端数切り上げプロンプトと縦に揃うようにずらされていることがわかります。そのような配置を実現するには、計算ボタンの開始位置を
round_up_switchの開始位置に制限します。また、計算ボタンとその上のスイッチの間に、8dpの縦方向の余白を追加します。

...
<Button
android:id="@+id/calculate_button"
...
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@id/round_up_switch" />
...
- 最後に重要な作業として、
TextViewに8dpの上余白を追加し、tip_resultの位置を調整します。

...
<TextView
android:id="@+id/tip_result"
...
android:layout_marginTop="8dp" />
...
- たくさんのステップがありましたが、順を追って実施すれば大きな成果が得られます。レイアウト内に要素を正しく配置するには細部に十分な注意を払う必要がありますが、その結果として外観が大幅に改善されます。アプリを実行すると、以下のスクリーンショットのようになります。要素を縦に揃えて要素間の間隔を広げたことで、要素が密集していないすっきりしたレイアウトになりました。

作業はこれで終わりではありません。サービスに関する質問とチップ金額のフォントサイズと色が、ラジオボタンとスイッチのテキストと異なることにお気づきでしょうか。次のタスクでは、スタイルとテーマを使用して、この点についても一貫性を実現します。
スタイルとは、ウィジェットの単一のタイプに対応するビュー属性値のコレクションです。たとえば、TextView スタイルで、フォントの色、フォントサイズ、背景色などを指定できます。これらの属性をスタイルに抽出すると、レイアウト内の複数のビューにスタイルを適用し、一元的に管理することが容易になります。
このタスクでは、最初にテキストビュー、ラジオボタン、スイッチ ウィジェットのスタイルを作成します。
スタイルを作成する
- res > values ディレクトリに
styles.xmlという名前のファイルがない場合は、新たに作成します。作成するには、values ディレクトリを右クリックして、[New] > [Values Resource File] を選択します。styles.xmlという名前を付けます。新しいファイルには次の行が含まれています。
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
- 新しい
TextViewスタイルを作成して、アプリ全体でテキストの表示に一貫性を持たせます。styles.xmlで一度スタイルを定義すると、レイアウト内のすべてのTextViewsにそれを適用できます。スタイルは、ゼロから定義することも、MDC ライブラリの既存のTextViewスタイルから拡張することもできます。
通常、コンポーネントのスタイルを設定する際は、使用するウィジェット タイプの親スタイルから拡張する必要があります。これは 2 つの理由で重要です。第 1 に、すべての重要なデフォルト値が確実にコンポーネントに設定されます。第 2 に、親スタイルに対する今後のすべての変更が引き続きスタイルに継承されます。
スタイルには任意の名前を付けることができますが、推奨される命名規則があります。親マテリアル スタイルから継承する場合は、MaterialComponents をアプリの名前(TipTime)に置き換えて、類似したスタイル名を付けます。そうすると、変更が固有の名前空間に移動されるため、今後マテリアル コンポーネントに新しいスタイルが導入されたときに、競合する可能性を回避できます。例:
スタイル名: Widget.TipTime.TextView 親スタイルからの継承: Widget.MaterialComponents.TextView
styles.xml ファイルで、これを resources の開始タグと終了タグの間に追加します。
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
</style>
TextViewスタイルを設定して、属性android:minHeight,android:gravity,およびandroid:textAppearance.をオーバーライドします。
android:minHeight は、TextView の高さの最小値を 48 dp に設定します。すべての行の高さの最小値は、マテリアル デザイン ガイドラインに従って 48 dp にする必要があります。
android:gravity 属性を設定すると、TextView のテキストを縦方向の中央に配置できます(以下のスクリーンショットをご覧ください)。gravity は、ビュー内のコンテンツの位置を制御します。実際のテキスト コンテンツの高さは 48 dp を超えないので、値 center_vertical により TextView 内のテキストは縦方向の中央に配置されます(横方向の位置は変更されません)。gravity には、これ以外に center、center_horizontal、top、bottom などの値を指定できます。よろしければ、他の gravity の値を試して、テキストに及ぼす効果を確認してみてください。

テキスト表示属性の値を ?attr/textAppearanceBody1 に設定します。TextAppearance は、テキストサイズ、フォント、およびテキストのその他プロパティに関する事前作成済みのスタイルのセットです。マテリアルが提供するその他の可能なテキスト表示については、こちらの型スケールのリストをご覧ください。
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
activity_main.xmlの各TextViewにスタイル属性を追加することにより、Widget.TipTime.TextViewスタイルをservice_questionTextViewに適用します。
<TextView
android:id="@+id/service_question"
style="@style/Widget.TipTime.TextView"
... />
スタイルを追加する前、TextView は、小さいフォントサイズとグレーのフォントカラーで次のように表示されていました。

スタイルを追加した後、TextView は次のようになります。これで、この TextView とレイアウトの他の部分との一貫性が向上しました。

- 同じ
Widget.TipTime.TextViewスタイルをtip_resultTextViewに適用します。
<TextView
android:id="@+id/tip_result"
style="@style/Widget.TipTime.TextView"
... />

- 同じテキスト スタイルをスイッチのテキストラベルに適用する必要があります。ただし、
TextViewスタイルをSwitchMaterialウィジェットに設定することはできません。TextViewスタイルはTextViewsにのみ適用できます。したがって、スイッチ用の新しいスタイルを作成します。属性は、minHeight、gravity、textAppearanceについては同じです。異なるのはスタイル名と親です。それらは MDC ライブラリのSwitchスタイルから継承するからです。この場合も、スタイルの名前に、親スタイルの名前を反映させる必要があります。
スタイル名: Widget.TipTime.CompoundButton.Switch 親スタイルからの継承: Widget.MaterialComponents.CompoundButton.Switch
<style name="Widget.TipTime.CompoundButton.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
このスタイルのスイッチに固有の追加属性を指定することもできますが、このアプリではその必要はありません。
- テキストの表示に一貫性を持たせる必要がある最後の箇所は、ラジオボタンのテキストです。
TextViewスタイルまたはSwitchスタイルをRadioButtonウィジェットに適用することはできません。代わりに、ラジオボタン用の新しいスタイルを作成する必要があります。これは、MDC ライブラリのRadioButtonスタイルから拡張できます。
このスタイルを作成する際に、ラジオボタン テキストと円形のシンボルの間にパディングを追加します。paddingStart は、まだ使用したことがない新しい属性です。パディングとは、ビューのコンテンツとビューの境界の間のスペースの量です。paddingStart 属性は、コンポーネントの開始位置にのみパディングを設定します。ラジオボタンの paddingStart を 0 dp にした場合と 8 dp にした場合の違いを次に示します。


<style name="Widget.TipTime.CompoundButton.RadioButton"
parent="Widget.MaterialComponents.CompoundButton.RadioButton">
<item name="android:paddingStart">8dp</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
- (省略可)
dimens.xmlファイルを作成すると、頻繁に使用する値の管理が容易になります。このファイルは、前述のstyles.xmlファイルと同じ方法で作成できます。values ディレクトリを選択し、右クリックして [New] > [Values Resource File] を選択します。
これまでに、この小さなアプリで高さの最小値の設定を 2 回繰り返しました。この程度であれば簡単に管理できますが、4 個、6 個、10 個、またはそれ以上のコンポーネントがこの値を共有するとなると、たちまち管理が困難になります。それらすべてを個別に変更する作業は退屈で、間違いが発生しやすくなります。res > values ディレクトリに dimens.xml という名前の便利なリソース ファイルを作成し、そのファイルで共通のディメンションを保持できます。共通の値を名前付きディメンションとして標準化することにより、アプリの管理が容易になります。ただし、TipTime は小さなアプリなので、この省略可能なステップ以外ではこのファイルを使用しません。デザインチームと協力して作業する本番環境で複雑なアプリを作成する場合は、dimens.xml を使用すると、共通のディメンションの値を頻繁に変更することが容易になります。
dimens.xml
<resources>
<dimen name="min_text_height">48dp</dimen>
</resources>
48dp を直接設定する代わりに、@dimen/min_text_height を使用するように styles.xml ファイルを更新します。
...
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">@dimen/min_text_height</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
...
スタイルをテーマに追加する
お気づきのとおり、新しい RadioButton および Switch スタイルをそれぞれのウィジェットにまだ適用していません。これは、テーマ属性を使用してアプリテーマに radioButtonStyle と switchStyle を設定するためです。テーマについてもう一度おさらいしましょう。
テーマとは、スタイルやレイアウトなどで後から参照できる名前付きリソース(テーマ属性と呼ばれます)のコレクションです。テーマは、個別の View. だけでなく、アプリ全体、アクティビティ、ビュー階層に対して指定できます。以前は colorPrimary や colorSecondary のようなテーマ属性を設定することにより、themes.xml でアプリテーマを変更していました。そして、テーマ属性はアプリとアプリのコンポーネント全体で使用されていました。
radioButtonStyle と switchStyle も設定可能なテーマ属性です。これらのテーマ属性に対して指定するスタイル リソースは、そのテーマが適用されるビュー階層内のすべてのラジオボタンとすべてのスイッチに適用されます。
指定されたスタイル リソースがアプリ内のすべてのテキスト入力フィールドに適用される、textInputStyle 用のテーマ属性もあります。TextInputLayout を(マテリアル デザイン ガイドラインで示されているとおり)枠線付きテキスト フィールドのように表示するには、MDC ライブラリで Widget.MaterialComponents.TextInputLayout.OutlinedBox として定義されている OutlinedBox スタイルを利用できます。ここでは、このスタイルを使用します。

- テーマが目的のスタイルを参照するように、
themes.xmlファイルを変更します。テーマ属性を設定する方法は、前の Codelab でcolorPrimaryおよびcolorSecondaryテーマ属性を宣言した方法と同じです。ただし、今回設定するテーマ属性は、textInputStyle、radioButtonStyle、switchStyleです。前にRadioButtonおよびSwitch用に作成したスタイルとともに、マテリアルのOutlinedBoxテキスト フィールド用のスタイルを使用します。
次の行をコピーして、res/values/themes.xml のアプリテーマのスタイルタグに貼り付けます。
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
res/values/themes.xmlファイルは次のようになります。お望みであれば、XML にコメントを追加できます(コメントは<!-と-->で示します)。
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Text input fields -->
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<!-- Radio buttons -->
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<!-- Switches -->
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
</style>
</resources>
- themes.xml(night)のダークテーマにも必ず同じ変更を加えてください。
res/values-night/themes.xmlファイルは次のようになります。
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Application theme for dark theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Text input fields -->
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<!-- For radio buttons -->
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<!-- For switches -->
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
</style>
</resources>
- アプリを実行して、変更を確認します。
OutlinedBoxスタイルによってテキスト フィールドの外観が改善され、すべてのテキストの表示に一貫性がもたらされました。

アプリが完成に近づくにつれて、想定しているワークフローだけでなく、それ以外のユーザー シナリオでもアプリをテストする必要があります。わずかなコード変更で、ユーザー エクスペリエンスが大幅に改善されることがあります。
デバイスを回転させる
- デバイスを回転させて横表示にします。まず、[自動回転] 設定を有効にします(この設定は、デバイスの [クイック設定] か、[設定] > [ディスプレイ] > [詳細設定] > [画面の自動回転] オプションで行います)。

エミュレータでは、エミュレータ オプション(デバイスのすぐ隣の右上にあります)を使用して、画面を左右に回転させることができます。

- ご覧のとおり、[Calculate] ボタンなど、UI コンポーネントの一部の表示が切れています。これでは明らかにアプリを使用できません。

- この不具合を解決するには、
ConstraintLayoutを囲むScrollViewを追加します。XML は次のようになります。
<ScrollView
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:layout_height="match_parent"
android:layout_width="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
tools:context=".MainActivity">
...
</ConstraintLayout>
</ScrollView>
- もう一度アプリを実行してテストします。デバイスを回転させて横表示にした場合、UI をスクロールすると計算ボタンが使用可能になり、チップの計算結果が表示されます。この修正は、横表示だけでなく、サイズが異なる他の Android デバイスでも有用です。これで、デバイスの画面サイズにかかわらず、ユーザーはレイアウトをスクロールできるようになります。
Enter キーでキーボードを非表示にする
サービス料金を入力した後もキーボードが表示されたままになることにお気づきでしょうか。計算ボタンを使用しやすくするために、毎回キーボードを手動で非表示にするのはいささか面倒です。代わりに、Enter キーが押されたらキーボードが自動的に非表示になるようにしましょう。

テキスト フィールドでは、特定のキーがタップされたときにイベントに応答するキーリスナーを定義できます。キーボードのすべての可能な入力オプションには、Enter キーを含むキーコードが関連付けられています。画面キーボードは、物理キーボードに対してソフト キーボードと呼ばれることもあります。

このタスクでは、テキスト フィールドにキーリスナーを設定して、Enter キーが押されたときにリッスンするようにします。このイベントが検出されたら、キーボードを非表示にします。
- 下記のヘルパー メソッドをコピーして
MainActivityクラスに貼り付けます。MainActivityクラスの右中かっこの直前に挿入できます。handleKeyEvent()は、keyCode入力パラメータがKeyEvent.KEYCODE_ENTERと等しい場合に画面キーボードを非表示にする非公開のヘルパー関数です。InputMethodManager は、ソフト キーボードの表示と非表示を制御し、表示されるソフト キーボードをユーザーが選択できるようにします。このメソッドは、キーイベントが処理された場合は true を返し、処理されなかった場合は false を返します。
MainActivity.kt
private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// Hide the keyboard
val inputMethodManager =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
return true
}
return false
}
- 次に、キーリスナーを
TextInputEditTextウィジェットにアタッチします。バインディング オブジェクトを介して、binding.costOfServiceEditText.としてTextInputEditTextウィジェットにアクセスできることを思い出してください。
costOfServiceEditText で setOnKeyListener() メソッドを呼び出し、OnKeyListener を渡します。これは、binding.calculateButton.setOnClickListener { calculateTip() }. を使用して、アプリの計算ボタンにクリック リスナーを設定する方法と似ています。
ビューにキーリスナーを設定するコードはやや複雑ですが、キーの押下が発生したときにトリガーされる onKey() メソッドを OnKeyListener に設定するのが一般的な方法です。onKey() メソッドは 3 つの入力引数を受け取ります。それは、ビュー、押されたキーを表すコード、キーイベント(ここでは使用しないので、"_" を指定します)です。onKey() メソッドが呼び出されたら、handleKeyEvent() メソッドを呼び出して、ビューとキーコードの引数を渡す必要があります。これを記述する構文は、view, keyCode, _ -> handleKeyEvent(view, keyCode). です。これは実際にはラムダ式と呼ばれますが、ラムダの詳細については今後の学習ユニットで詳しく説明します。
アクティビティの onCreate() メソッド内のテキスト フィールドにキーリスナーを設定するコードを追加します。これは、レイアウトが作成された直後、ユーザーがアクティビティの操作を開始する前に、キーリスナーをアタッチする必要があるからです。
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
...
setContentView(binding.root)
binding.calculateButton.setOnClickListener { calculateTip() }
binding.costOfServiceEditText.setOnKeyListener { view, keyCode, _ -> handleKeyEvent(view, keyCode)
}
}
- 新たに加えた変更が機能するかどうかをテストします。アプリを実行して、サービス料金を入力します。キーボードの Enter キーを押すと、ソフト キーボードが非表示になるはずです。
TalkBack を有効にしてアプリをテストする
このコースで学習してきたとおり、できるだけ多くのユーザーが利用できるアプリを作成する必要があります。一部のユーザーは、TalkBack を使用してアプリを操作する場合があります。TalkBack は、Android デバイスに組み込まれている Google スクリーン リーダーです。TalkBack から音声フィードバックが出力されるので、画面を見ずにデバイスを使用できます。
TalkBack を有効にして、ユーザーがこのアプリでチップの計算を完了できるかどうかを確認しましょう。
- デバイスで TalkBack を有効にするには、こちらの手順を実施します。
- Tip Time アプリに戻ります。
- こちらの手順に沿って、TalkBack でアプリを探します。右にスワイプして画面要素間を順番に移動し、左にスワイプして逆に移動します。選択するには、任意の場所をダブルタップします。スワイプ操作でアプリのすべての要素にアクセスできることを確認します。
- TalkBack ユーザーが画面上の各アイテムにナビゲートできることを確認した後、サービス料金を入力し、チップ オプションを変更して、チップを計算します。そうすると、チップ金額が読み上げられるはずです。アイコンは
importantForAccessibility="no"としてマークしたため、アイコンの音声フィードバックは出力されないことにご注意ください。
アプリのユーザー補助機能を強化する方法について詳しくは、こちらの原則をご確認ください。
(省略可)ベクター型ドローアブルの色合いを調整する
この省略可能なタスクでは、テーマのメインカラーに基づいてアイコンの色合いを調整し、ライトテーマとダークテーマでアイコンの外観が変化するようにします(下記を参照)。この変更を行うと、アイコンとアプリテーマの調和が増し、UI がよりいっそう改善されます。

前述のように、ビットマップ画像と比較した場合の VectorDrawables の利点のひとつは、拡大縮小と色合い調整の機能です。ベルのアイコンを表す XML を下記に示します。特に注目すべきは、android:tint と android:fillColor の 2 つの色属性です。
ic_service.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2,17h20v2L2,19zM13.84,7.79c0.1,-0.24 0.16,-0.51 0.16,-0.79 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,0.28 0.06,0.55 0.16,0.79C6.25,8.6 3.27,11.93 3,16h18c-0.27,-4.07 -3.25,-7.4 -7.16,-8.21z"/>
</vector>

色合いが存在する場合、それによってドローアブルの fillColor ディレクティブがすべてオーバーライドされます。この場合は、白い色が colorControlNormal テーマ属性でオーバーライドされます。colorControlNormal は、ウィジェットの「標準」(選択されていない非アクティブの状態)の色です。現在、それはグレーです。
アプリの外観を改善する方法のひとつとして、アプリテーマのメインカラーに基づいてドローアブルの色合いを調整できます。アイコンは、ライトテーマでは @color/green として表示されますが、ダークテーマでは @color/green_light(これは ?attr/colorPrimary です)として表示されます。アプリテーマのメインカラーに基づいてドローアブルの色合いを調整すると、レイアウト内の要素の外観が統一され、調和が増します。また、ライトテーマとダークテーマ用に重複するアイコンのセットを保持せずに済みます。ベクター型ドローアブルのセットを 1 つだけ保持すれば、colorPrimary テーマ属性に基づいて色合いが変更されます。
ic_service.xmlのandroid:tint属性の値を変更します。
android:tint="?attr/colorPrimary"
Android Studio では、このアイコンに適切な色合いが設定されています。

colorPrimary テーマ属性が指す値は、ライトテーマかダークテーマかによって異なります。
- 他のベクター型ドローアブルで色合いを変更するには、同じ手順を繰り返します。
ic_store.xml
<vector ...
android:tint="?attr/colorPrimary">
...
</vector>
ic_round_up.xml
<vector ...
android:tint="?attr/colorPrimary">
...
</vector>
- アプリを実行し、ライトテーマとダークテーマでアイコンの外観が異なることを確認します。
- 最終クリーンアップとして、アプリ内のすべての XML ファイルと Kotlin コードファイルを再フォーマットする手順を忘れずに実施してください。
おつかれさまでした。これでチップ計算アプリは完成です。成し遂げた成果を誇りに思ってください。ここで学習したことが、もっと優れた外観と機能を持つアプリを作成するための足掛かりとなることを願っています。
この Codelab の解答コードは、下記の GitHub リポジトリにあります。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。
- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。
- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開かれるまで待ちます。
- 実行ボタン
をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
- [Project] ツール ウィンドウでプロジェクト ファイルを表示して、アプリがどのように実装されているかを確認します。
- マテリアル デザイン ガイドラインに沿って可能な限りマテリアル デザイン コンポーネントを使用し、カスタマイズを検討します。
- アイコンを追加することにより、アプリの各要素がどのように機能するかについてユーザーに視覚的な手掛かりを与えます。
ConstraintLayoutを使用してレイアウト内に要素を配置します。- エッジケース(例: デバイスを回転させて横向きの画面にアプリを表示する)でアプリをテストし、必要に応じて改善します。
- コードにコメントを挿入して、他の人がコードを読んだときに作成者の意図を簡単に理解できるようにします。
- コードを再フォーマットしてクリーンアップを行い、コードをできるだけ簡潔にします。
- 前の Codelab からの続きとして、ここで学習したベスト プラクティス(マテリアル デザイン コンポーネントの使用方法など)を使い、マテリアル ガイドラインにもっと近づけるように単位変換クッキング アプリを更新してください。
写真撮影: 
写真撮影: 




