ListView
の中にEditText
を配置して,動的に編集可能なEditText
の追加・削除方法を記す。
目次
導入
実装例
ソースコード
getView
Viewの再利用
getViewTypeCountとgetItemViewTypeの実装
結論
導入
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.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が入っている。
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 (see getViewTypeCount()
and getItemViewType( int)
).
Adapter | Android Developers
これらの実装がない場合,1個目と2個目のEditText
への入力内容が同じになってしまった。
なお,初期状態でListView
にウィジェットを配置せず,後から配置する場合はこれらの実装がなくても問題なかった。タイミングの問題があるのかもしれない。
結論ListView
へのEditText
の配置方法を解説した。
EditText
は入力UIとして重宝するが,TextWatcher
の設定が必要だったり,getView
の実装が必要だったり,何かと面倒くさい。
定型処理として覚えてしまいたい。
関連