Android Kotlin基礎講座 10.3:利用者に向けたデザイン

目次

タスク:リージョンをフィルタリングするためにチップを使う

チップとは属性、テキスト、エンティティ、アクションなどを表すコンパクトな要素です。これによって、ユーザーは情報の入力、選択肢の選択、コンテンツのフィルタリング、何かしらのアクションを起こしたりできます。

Chipウィジェットは全てのレイアウトおよび描写ロジックを含むChipDrawableを囲むビューラッパーです。タッチ、マウス、キーボード、アクセシビリティナビゲーションをサポートするための別のロジックも存在しています。主要なチップと閉じるアイコンはロジック用のサブビューに分けられるべきというのが一般的で、それらには画面遷移の挙動や各々のビューの状態などが含まれています。

チップはドローアブルを使用します。Androidドローアブルを使うことで、画像、シェイプ、アニメーション等を画面に描写することができます。またドローアブルには固定サイズも動的に変わるサイズもあります。GDGアプリ内の画像のような画像はドローアブルとして使うことができます。また、ヴェクター画像を使うことで、想像できるものはすべて描写することができます。
この講座では解説しませんが、 9-patch drawableというリサイズ可能なドローアブルもあります。drawable/ic_gdg.xml内のGDGロゴも別のドローアブルです。

ドローアブルはビューではないので、ConstraintLayout内に直接ドローアブルを置くことはできません。ImageViewの中に置く必要があります。またドローアブルを使ってテキストビューやボタン用の背景を表現することもできます。

ステップ1:GDGsのリストにチップを追加する

下の画像のチェックが入ったチップは三つのドローアブルを使用しています。背景とチェックマークはそれぞれドローアブルです。チップをタッチすることで、エフェクトが表示されますが、それもRippleDrawableというドローアブルを使用しています。

このタスクでは、チップをGDGsのリストに追加し、選択されたときに状態が変わるようにします。今回の場合は、検索画面のトップにchipsというボタンの列を追加します。それぞれのボタンはGDGリストをフィルタリングし、ユーザーが選択した地域からのみ結果を取得できるようにします。ボタンが選択されると、ボタンの背景が変わり、チェックマークが付きます。

  1. fragment_gdg_list.xmlを開いてください。
  2. HorizontalScrollView内にcom.google.android.material.chip.ChipGroupを作成してください。
    そのsingleLineプロパティをtureにしl、全てのチップが水平にスクロールできるライン上に並ぶようにします。
    singleSelectionプロパティをtureにし、グループ内のチップから一つのみ選択できるようにします。以下がコードになります。
<com.google.android.material.chip.ChipGroup
    android:id="@+id/region_list"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:singleSelection="true"
    android:padding="@dimen/spacing_normal"/>
  1. layoutフォルダー内にregion.xmlという新規レイアウトリソースファイルを作成してください。これはチップ用のレイアウトを定義するためのものです。
  2. region.xml内の全てのコードを以下のコードに置き換えてください。Chipがマテリアルコンポーネントであることを確認してください。また、app:checkedIconVisibleプロパティを設定することによってチェックマークを取得していることも確認してください。
    selected_highlightカラーがないことによるエラーが表示されます。
<?xml version="1.0" encoding="utf-8"?>

<com.google.android.material.chip.Chip
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        style="@style/Widget.MaterialComponents.Chip.Choice"
        app:chipBackgroundColor="@color/selected_highlight"
        app:checkedIconVisible="true"
        tools:checked="true"/>
  1. slected_highlightカラーを作成するために、selected_highligtの上にカーソルをおいて、インテンションメニューを開いてください。create color resource for selected highlightを選択してください。設定は全てデフォルトのままで大丈夫ですので、OKをクリックしてください。ファイルがres/colorフォルダに作成されます。
  2. res/color/selected_highlight.xmlを開いてください。<selector>としてエンコードされているカラー状態リスト内では、異なる状態に応じて別のカラーを提供することができます。それぞれの状態とそれに紐づいたカラーは<item>としてエンコードされます。
  3. <selector>内にデフォルトカラーであるcolorOnSurfaceを追加してください。この状態リスト内で、常に全ての状態に対応することが重要です。その方法の一つとして、デフォルトカラーを設定しておくことが挙げられます。
<item android:alpha="0.18" android:color="?attr/colorOnSurface"/>
  1. デフォルトカラーの上に、colorPrimaryVariantという色用のitemを追加し、trueという状態の時にこれが使われるように設定します。
    状態リストは条件分岐文のように、上から下に読み取られていきます。もしどの状態にもマッチしなかった場合に、デフォルトカラーが適用される仕組みです。
<item android:color="?attr/colorPrimaryVariant"
         android:state_selected="true" />

ステップ2:チップの列を表示する

GDG FinderはGDGのある地域を表示するチップのリストを作成します。チップが選択されると、アプリはその地域用のGDGのみを表示するようにフィルタリングします。

Note: どのようにリストが生成されるか、結果がフィルタリングされるかはこの記事の範囲外です。もしGDGがどのように取得されているか、フィルタリングが実装されているかを知りたい場合は、search、networkパッケージ内のファイルを調べてみてください。特にGdgListViewModelを調べてみるといいです。

  1. searchパッケージ内、GdgListFragment.ktを開いてください。
  2. onCreateView()内、return文の直前にviewModel.regionListにオブザーバーを追加し、onChanged()をオーバーライドします。ビューモデルによって提供されている地域のリストが変更されるとき、チップは再生成される必要があります。dataがnullの場合は即座にreturnするための文を追加してください。
viewModel.regionList.observe(viewLifecycleOwner, object: Observer<List<String>> {
        override fun onChanged(data: List<String>?) {
             data ?: return
        }
})
  1. onChanged()内、nullチェックの下でbinding.regionListをchipGroupという新規変数に代入し、regionListをキャッシュしてください。
val chipGroup = binding.regionList
  1. さらにその下に、chipGroup.contextからのチップをインフレートするための新規変数layoutInflatorを作成してください。
val inflator = LayoutInflater.from(chipGroup.context)
  1. クリーン、リビルドしてデータバインディングエラーを取り除いてください。

これでインフレーターの下に実際のチップを作成することができます。

  1. 全てのチップを保持するための変数childrenを作成してください。それを渡されたdataのマッピング関数に代入し、それぞれのチップを作成して返します。
val children = data.map {} 
  1. mapのラムダ式の中で、それぞれのregionName用にチップを作成、インフレートします。完成コードは以下になります。
    Inside the map lambda, for each regionName, create and inflate a chip. The completed code is below.
   val children = data.map { regionName ->
       val chip = inflator.inflate(R.layout.region, chipGroup, false) as Chip
       chip.text = regionName
       chip.tag = regionName
       // TODO: Click listener goes here.
       chip
   }
  1. ラムダ式の中のchipを返している直前にクリックリスナーを追加します。chipがクリックされたら、状態をcheckedに設定します。viewModel内のonFilterChanged()を呼び出し、このフィルター用の結果を取得する一連のイベントを起動します。
chip.setOnCheckedChangeListener { button, isChecked ->
   viewModel.onFilterChanged(button.tag as String, isChecked)
}
  1. ラムダ式の最後で、chipGroupからの現在のビューすべてを削除し、childrenからの全てのビューをchipGroupに追加します。(このチップは更新することができないので、削除してから再生成する必要があります)
chipGroup.removeAllViews()

for (chip in children) {
   chipGroup.addView(chip)
}

最終的なオブザーバーは以下のようになります。

   override fun onChanged(data: List<String>?) {
       data ?: return

       val chipGroup = binding.regionList
       val inflator = LayoutInflater.from(chipGroup.context)

       val children = data.map { regionName ->
           val chip = inflator.inflate(R.layout.region, chipGroup, false) as Chip
           chip.text = regionName
           chip.tag = regionName
           chip.setOnCheckedChangeListener { button, isChecked ->
               viewModel.onFilterChanged(button.tag as String, isChecked)
           }
           chip
       }
       chipGroup.removeAllViews()

       for (chip in children) {
           chipGroup.addView(chip)
       }
   }
})
  1. アプリを起動して、検索画面を開くためにGDGを検索してください。チップを使ってみてください。チップをクリックすると、フィルタリングされた結果が表示されます。