Android: ListView内のEditTextの実装

ListViewの中にEditTextを配置して,動的に編集可能なEditTextの追加・削除方法を記す。
導入
EditTextの内容を変更する場合,TextWatcherの実装インスタンスでテキストの変更を検知して処理を行う。編集可能なEditTextの実装には,「Android: TextWatcherによる編集可能なEditText」で解説した通り,EdtiTextにTextWatcherを設定する。
EditTextごとにTextWatcherインスタンスが必要なのは分かるが,ListViewで動的に生成したEditTextにどうやってTextWatcherインスタンスを設定すればいいかわからなかった。
そこで,実現方法を記す。以下の情報を参考にした。
ListViewにsetAdapterメソッドで設定するAdapterを自前で実装する。その際に,getViewメソッドがデータごとに呼び出されるので,getViewメソッドでEditTextにTextWatcherを設定すればいいようだ。
実装例
実装例を示す。以下のようにListView内にEditTextを配置した。
1番目のブロックはAdapterにArrayAdapterを設定したListViewだ。ListView内にEditTextだけを配置したシンプルな例となっている。
2番目のブロックはAdapterを実装しなかった場合をNGの例として掲載している。2番目のブロックはTextWatcherを設定できていないので,EditTextに入力した内容が維持されない。
3番目のブロックはAdapterにSimpleAddapterを設定したListViewだ。ArrayAdapterと異なりListViewの中に複数のウィジェットが含まれる複雑なケースの例となっている。
ソースコード
ソースコードは以下となる。
MainActivity.javapackage 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が入っている。
positionはinnerクラスからアクセスするために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
getViewTypeCountとgetItemViewTypeの実装
初期状態で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 (seegetViewTypeCount()andgetItemViewType().int)
これらの実装がない場合,1個目と2個目のEditTextへの入力内容が同じになってしまった。
なお,初期状態でListViewにウィジェットを配置せず,後から配置する場合はこれらの実装がなくても問題なかった。タイミングの問題があるのかもしれない。
結論
ListViewへのEditTextの配置方法を解説した。
EditTextは入力UIとして重宝するが,TextWatcherの設定が必要だったり,getViewの実装が必要だったり,何かと面倒くさい。
定型処理として覚えてしまいたい。

