Android: ListView内のEditTextの実装

ListViewの中にEditTextを配置して,動的に編集可能なEditTextの追加・削除方法を記す。

導入

EditTextの内容を変更する場合,TextWatcherの実装インスタンスでテキストの変更を検知して処理を行う。編集可能なEditTextの実装には,「Android: TextWatcherによる編集可能なEditText」で解説した通り,EdtiTextTextWatcherを設定する。

EditTextごとにTextWatcherインスタンスが必要なのは分かるが,ListViewで動的に生成したEditTextにどうやってTextWatcherインスタンスを設定すればいいかわからなかった。

そこで,実現方法を記す。以下の情報を参考にした。

参考

ListViewsetAdapterメソッドで設定するAdapterを自前で実装する。その際に,getViewメソッドがデータごとに呼び出されるので,getViewメソッドでEditTextTextWatcherを設定すればいいようだ。

実装例

実装例を示す。以下のようにListView内にEditTextを配置した。

1番目のブロックはAdapterArrayAdapterを設定したListViewだ。ListView内にEditTextだけを配置したシンプルな例となっている。

2番目のブロックはAdapterを実装しなかった場合をNGの例として掲載している。2番目のブロックはTextWatcherを設定できていないので,EditTextに入力した内容が維持されない

3番目のブロックはAdapterSimpleAddapterを設定したListViewだ。ArrayAdapterと異なりListViewの中に複数のウィジェットが含まれる複雑なケースの例となっている。

起動イメージ
ソースコード

ソースコードは以下となる。

MainActivity.java
package jp.senooken.android.edittextinlistview;

import androidx.appcompat.app.AppCompatActivity;

import androidx.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // OK case
        ListView listView1 = findViewById(R.id.listView1);
        listView1.setAdapter(new MyArrayAdapter(this, new String[]{"1", "2"}));

        // NG case
        List<Map<String, String>> list = new ArrayList<>();
        HashMap<String, String> map1 = new HashMap<>();
        map1.put("key", "3");
        list.add(map1);
        HashMap<String, String> map2 = new HashMap<>();
        map2.put("key", "4");
        list.add(map2);

        SimpleAdapter adapter = new SimpleAdapter(
                this, list, R.layout.list_item, new String[]{"key"}, new int[]{R.id.edit_text});
        ListView listView2 = findViewById(R.id.listView2);
        listView2.setAdapter(adapter);

        // OK case
        List<Map<String, String>> list3 = new ArrayList<>();
        HashMap<String, String> map3 = new HashMap<>();
        map3.put("bullet", "5");
        map3.put("edit_text", "55");
        list3.add(map3);
        HashMap<String, String> map4 = new HashMap<>();
        map4.put("bullet", "6");
        map4.put("edit_text", "66");
        list3.add(map4);

        ListView listView3 = findViewById(R.id.listView3);
        listView3.setAdapter(new MySimpleAdapter(this, list3));
    }

    private class MyArrayAdapter extends ArrayAdapter<String> {
        final String[] values_;

        MyArrayAdapter(Context context, String[] values) {
            super(context, R.layout.list_item, values);
            values_ = values;
        }

        @Override
        public int getViewTypeCount() {
            return getCount();
        }
        @Override
        public int getItemViewType(int position) {
            return position;
        }

        @NonNull
        @Override
        public View getView(final int position, View convertView, @NonNull ViewGroup parent) {
            if (convertView == null) {
                convertView = getLayoutInflater().inflate(R.layout.list_item, parent, false);
            }
            EditText editText = convertView.findViewById(R.id.edit_text);
            editText.setText(values_[position]);
            editText.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                }

                @Override
                public void afterTextChanged(Editable s) {
                    values_[position] = s.toString();
                }
            });
            return convertView;
        }
    }

    private class MySimpleAdapter extends SimpleAdapter {
        private final List<? extends Map<String, String>> data_;
        // MySimpleAdapter(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to) {
        MySimpleAdapter(Context context, List<? extends Map<String, String>> data) {
            super(context, data, R.layout.list_item2
                    , new String[]{"bullet", "edit_text"}, new int[]{R.id.bullet, R.id.edit_text2});
            data_ = data;
        }

        @Override
        public int getViewTypeCount() {
            return getCount();
        }
        @Override
        public int getItemViewType(int position) {
            return position;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = getLayoutInflater().inflate(R.layout.list_item2, parent, false);
            }
            TextView textView = convertView.findViewById(R.id.bullet);
            textView.setText(data_.get(position).get("bullet"));
            EditText editText = convertView.findViewById(R.id.edit_text2);
            editText.setText(data_.get(position).get("edit_text"));
            editText.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                }

                @Override
                public void afterTextChanged(Editable s) {
                    data_.get(position).put("edit_text", s.toString());
                }
            });
            return convertView;
        }
    }
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>

<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">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.3" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.6" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/MyArrayAdapter"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/listView1" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/SimpleAdapter"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/listView1" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/MySimpleAdapter"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/listView2" />

    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/guideline1"
        app:layout_constraintTop_toTopOf="parent"
        tools:context=".MainActivity" />

    <ListView
        android:id="@+id/listView2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/guideline2"
        app:layout_constraintTop_toTopOf="@+id/guideline1"
        tools:context=".MainActivity" >

    </ListView>

    <ListView
        android:id="@+id/listView3"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline2"
        tools:context=".MainActivity" />

</androidx.constraintlayout.widget.ConstraintLayout>
list_item.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"
    xmlns:tools="http://schemas.android.com/tools">

    <EditText
        android:id="@+id/edit_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:ems="10"
        android:inputType="text"
        android:hint="@string/hint"
        android:autofillHints=""
        tools:targetApi="o"
        />
</LinearLayout>
list_item2.xmlc
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/bullet"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        />
    <EditText
        android:id="@+id/edit_text2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:ems="10"
        android:hint="@string/hint"
        android:inputType="text"
        android:autofillHints=""
        tools:targetApi="o" />
</LinearLayout>
strings.xml
<resources>
    <string name="app_name">EditTextInListView</string>
    <string name="hint">Fill in some texts.</string>
    <string name="MyArrayAdapter">OK: MyArrayAdapter. Can save edited text.</string>
    <string name="SimpleAdapter">NG: SimpleAdapter. Cannot save edited text!</string>
    <string name="MySimpleAdapter">OK: MySimpleAdapter. Can save edited text.</string>
</resources>

いくつかポイントがあるので説明していく。

getView

getViewのシグネチャーは以下となっている。

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {

第1引数のpositionには表示するViewの位置が入っている。ListViewに詰め込むデータの添字に対応しているため,後でこの添字を使って行ごとのデータを参照する。

第2引数のconvertViewには,Viewの再利用用に既存のViewが存在すれば入っている。

第3引数のparentには親のviewが入っている。

positioninnerクラスからアクセスするためにfinalを指定している。finalを指定しないと以下のエラーが出る。

error: local variable position is accessed from within inner class; needs to be declared final
Viewの再利用

getViewの冒頭では以下のコードにより,convertViewが存在すればそれを使い回すようにしている。

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) { convertView = getLayoutInflater().inflate(R.layout.list_item2, parent, false); }

getLayoutInflater()によりInflaterを取得してそこからViewを生成している。Viewの生成は処理不可が高いそうで,このようなViewの使いまわし処理を入れないと,Inspect時に以下の警告を受ける。

Warning:(134, 27) Unconditional layout inflation from view adapter: Should use View Holder pattern (use recycled view passed into this method as the second parameter) for smoother scrolling
getViewTypeCountgetItemViewTypeの実装

初期状態でListViewにウィジェットを配置した実装で,convertViewを再利用する場合,以下のコードが必要だった (参考: Android Listview Edittext With TextView Get/Set Text Values Of EditText)。

    @Override
    public int getViewTypeCount() {
        return getCount();
    }
    @Override
    public int getItemViewType(int position) {
        return position;
    }

これらの2個のメソッドはgetViewで使われるらしい。それで,1行に複数のViewがある場合,これらの実装が必要らしい。

View: The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view. Heterogeneous lists can specify their number of view types, so that this View is always of the right type (see getViewTypeCount() and getItemViewType(int)).
Adapter | Android Developers

これらの実装がない場合,1個目と2個目のEditTextへの入力内容が同じになってしまった。

なお,初期状態でListViewにウィジェットを配置せず,後から配置する場合はこれらの実装がなくても問題なかった。タイミングの問題があるのかもしれない。

結論

ListViewへのEditTextの配置方法を解説した。

EditTextは入力UIとして重宝するが,TextWatcherの設定が必要だったり,getViewの実装が必要だったり,何かと面倒くさい。

定型処理として覚えてしまいたい。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です