＊Android＊ListViewにカスタムレイアウトを使用してスクロールできることを確認・タップしてリストから値を取得する

# はじめに
こんにちは。今回は、カスタムレイアウトを使用してListViewを作成する際にハマった点を中心に、ListViewをスクロールできることを確認しつつ、ListViewのアイテムをタップして値を取得するところまで解説します。

カスタムレイアウトを使ったListViewの作成では、Androidでデフォルトで用意されているレイアウトを使う際には発生しない、スクロールできないといったエラーが発生することがあります。また、ListViewを画面全体に配置しない場合（画面の先頭ではなく途中から配置する場合）、ListViewを画面全体に配置する時には発生しないエラーが発生することがあります。これらのエラーにハマった際の解決方法を明確にしておきます。以下は解決方法として間違っています。

- ScrollViewでListViewを囲うことにより、スクロールできるようにする
- Java側のコードで何とかしようとする

# 前提
＊Android Studio 4.0.1
＊targetSdkVersion 29
＊Google Pixel 3a XL

# 完成イメージ
以下のような画面を作ります。リストビューのアイテムをタッチすると、ログにListViewのアイテムの値を出力するようにします。リストビューの各行は、3行のテキストビューからなります。リストビューはスクロールすることができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674933/ca679103-b1a8-86e9-8ccc-fa990a35d345.jpeg" width="40%">

# 画面レイアウトの作成
まずは、完成イメージの通りになるように画面を作成してみます。レイアウトは以下の通りです。ここで配置の仕方を間違えると、想定通りにViewが配置されない、スクロールできないといった予想外の挙動が発生します。ListViewをスクロールできない場合やListViewが1行しか表示されない、ListViewが指定している大きさと異なる大きさで配置されるといった問題がある場合は、画面レイアウトが間違っていることを疑うようにしてください。間違っても、ScrollViewを配置したり、Java側にレイアウトを修正するためのコードを記述しないようにしてください。

```xml:activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout 
    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_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/constraintLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:context=".ResultSongArtistActivity">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"
            app:layout_constraintEnd_toStartOf="@+id/button2"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintHorizontal_chainStyle="spread"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/button"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/constraintLayout2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/constraintLayout">

        <TextView
            android:id="@+id/textView"
            android:text="Sample"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="50dp"
        android:layout_marginEnd="50dp"
        android:layout_marginStart="50dp"
        android:layout_marginTop="250dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/constraintLayout2" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
ここで問題なのが、listviewの制約をconstraintLayout2に対してつけているにもかかわらず、layout_marginTopの値にはlistviewと画面上端の間隔である250dpを指定しているということです。これで見た目のレイアウトとしては問題ないですが、本来ならlayout_marginTopの値はlistviewとconstraintLayout2の間隔である50dpを設定するべきです。layout_marginTopの値を50dpにしつつ，見た目のレイアウトを完成イメージと同じにするためには、layout_widthに0dpを指定する必要があります。match_parentは親ViewいっぱいにViewを広げて配置するのに対し、0dpはレイアウトの中で埋まっていない領域いっぱいにViewを広げて配置します。そのため、match_parentの代わりに0dpを設定することで、完成イメージと同じレイアウトを実現できます。修正後のListViewは以下のようになります。

```xml:acctivity_main.xml
<ListView
    android:id="@+id/listView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginBottom="50dp"
    android:layout_marginEnd="50dp"
    android:layout_marginStart="50dp"
    android:layout_marginTop="50dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/constraintLayout2" />
```

# カスタムレイアウトの記述
ここまでで画面レイアウトを作り終えたので、ListViewの各行に表示するカスタムレイアウト、Java側の処理を記述していきます。まずはカスタムレイアウトを記述します。ListViewの各行にテキストを1行、2行配置できるレイアウトはカスタムする必要がなくデフォルトで存在するため、ここではテキストを3行配置するためのカスタムレイアウトを作成します。res/layoutフォルダを右クリックし，`[New]→[Layout resource file]`を選択します。list_custom.xmlを作成してください。

ListViewの各行に3行のテキストを表示するために、以下のようにTextViewを3つ配置してIDで区別します。

```xml:list_custom.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textList1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp"/>

    <TextView
        android:id="@+id/textList2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp"/>

    <TextView
        android:id="@+id/textList3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp"/>

</LinearLayout>
```

# Java側の処理
最後にJava側の処理を記述します。ListView、ListViewに表示するデータを格納するリストを作成し、値を格納します。ListViewの各行のレイアウトに2つ以上の値を表示する（例えば2行あるいは横に2つのテキストを表示する）場合、データを格納するリストは`List<Map<String, String>>`型で記述します。ListViewの1行に1つのMap<String, String>型のデータを適用します。最後に、ListViewの各行と、表示するデータを紐づけるためにadapterを使用します。コードは以下の通りです。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity {
    private ListView listView;
    private List<Map<String, String>> numList;
    private Map<String, String> numMap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.listView = findViewById(R.id.listView);
        this.numList = new ArrayList<>();
        this.numMap = new HashMap<>();
        this.numMap.put("word", "zero");
        this.numMap.put("num", "0");
        this.numMap.put("index", "index0");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "one");
        this.numMap.put("num", "1");
        this.numMap.put("index", "index1");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "two");
        this.numMap.put("num", "2");
        this.numMap.put("index", "index2");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "three");
        this.numMap.put("num", "3");
        this.numMap.put("index", "index3");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "four");
        this.numMap.put("num", "4");
        this.numMap.put("index", "index4");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "five");
        this.numMap.put("num", "5");
        this.numMap.put("index", "index5");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "six");
        this.numMap.put("num", "6");
        this.numMap.put("index", "index6");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "seven");
        this.numMap.put("num", "7");
        this.numMap.put("index", "index7");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "eight");
        this.numMap.put("num", "8");
        this.numMap.put("index", "index8");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "nine");
        this.numMap.put("num", "9");
        this.numMap.put("index", "index9");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "ten");
        this.numMap.put("num", "10");
        this.numMap.put("index", "index10");
        this.numList.add(this.numMap);
        this.numMap = new HashMap<>();
        this.numMap.put("word", "eleven");
        this.numMap.put("num", "11");
        this.numMap.put("index", "index11");
        this.numList.add(this.numMap);

        String[] from = {"word", "num", "index"};
        int[] to = {R.id.textList1, R.id.textList2, R.id.textList3};
        // 第2引数はリストデータそのもの
        // 第3引数はリストビューの各行のレイアウトを示すR値
        // fromとtoの組み合わせでMap内のどのデータをListView各行のどの部品に割り当てるかを指定できる
        SimpleAdapter adapter = new SimpleAdapter(this, numList, R.layout.customlist, from, to);
        listView.setAdapter(adapter);

        // parentはタップされたリスト全体を表す
        // viewはタップされた1行分の画面部品そのものを表す
        // positionはタップされた行番号を表す
        // positionと同じ値が渡される
        this.listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Map<String, String> item = (Map<String, String>) parent.getItemAtPosition(position);
                String itemWord = item.get("word");
                String itemNum = item.get("num");
                String itemIndex = item.get("index");
                Log.d("result", itemWord + ", " + itemNum + ", " + itemIndex);
            }
        });
    }
}
```

# 終わりに
今回は、カスタムレイアウトを使ったListViewについて解説しました。ListViewを画面の途中から始める場合のソースコードをすべて示しているので、様々なプログラムを作るときのテンプレとして使ってください。ListViewを表示する際にエラーが出たときは、真っ先にレイアウトファイルを疑ってみましょう。