質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.77%

【Android】EditTextを含むListViewをスクロールするとEditTextの内容が元に戻る

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 2,127

inumi

score 13

 前提・実現したいこと

ListViewの表示アイテムにEditTextを含めたいのですが、入力後にENTERキーで確定させないでListViewをスクロールさせるとEditTextへの入力内容が元に戻ってしまいます。

ListViewのアイテムをListItemをいうクラスにして、ListItem型のデータとListViewはAdapterで接続しています。

 発生している問題

ListItemのデータを書き換えができれば、表示内容は保持されることは分かりましたが、ENTERキーを押して確定するという動作が必要になってしまっています。文字を入力した段階でデータとして更新する方法が見つかりません。

 該当のソースコード

(ListItem.java)
public class ListItem {
    private int id;
    private String value;

    public int getId() { return this.id; }
    public void setId(int id) { this.id = id; }
    public String getValue() { return this.value; }
    public void setValue(String value) { this.value = value; }
}

(MainActivity.java)
public class MainActivity extends AppCompatActivity {

    MyListAdapter adapter;

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

        ArrayList<ListItem> data = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            ListItem item = new ListItem();
            item.setValue(Integer.toString(i));
            data.add(item);
        }
        this.adapter = new MyListAdapter(this, data, layout.list_item);
        ListView list = (ListView) this.findViewById(id.list);
        list.setAdapter(this.adapter);
    }
}

(MyListAdapter.java)
public class MyListAdapter  extends BaseAdapter  {
    private Context context;
    private ArrayList<ListItem> data;
    private int resource;
    private final LayoutInflater inflater;

    static class ViewHolder {
        EditText editText;
    }

    public int getCount() {
        return this.data.size();
    }

    public Object getItem(int position) {
        return this.data.get(position);
    }

    public long getItemId(int position) {
        return this.data.get(position).getId();
    }

    public MyListAdapter(Context context, ArrayList<ListItem> data, int resource)
    {
        this.context = context;
        this.data = data;
        this.resource = resource;
        inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public View getView(final int position, View convertView, final ViewGroup parent) {
        Activity activity = (Activity) this.context;
        final ListItem item = (ListItem) this.getItem(position);
        final MyListAdapter.ViewHolder holder;

        if (convertView == null) {
            convertView = activity.getLayoutInflater().inflate(this.resource, null);
            holder = new MyListAdapter.ViewHolder();
            holder.editText = (EditText) convertView.findViewById(id.editText);
            convertView.setTag(holder);
        }
        else{
            holder = (MyListAdapter.ViewHolder)convertView.getTag();
        }

        holder.editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void afterTextChanged(Editable editable) {
                /* 実験コード1 スクロール範囲外の行も一緒に変わってしまう */
                //item.setValue(editable.toString());
            }
        });

        holder.editText.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View view, int i, KeyEvent keyEvent) {
                //実験コード2 ENTERキーで確定すれば希望通りとなる */
                String str = holder.editText.getText().toString();
                item.setValue(str);
                return false;
            }
        });
        ((EditText) holder.editText).setText(item.getValue());

        return convertView;
    }
}
(list_item.xml)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal">

        <EditText
            android:id="@+id/editText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_alignParentTop="true"
            android:inputType="none"
            android:digits="0123456789abcdefABCDEF"
            android:maxLength="2"/>
    </RelativeLayout>
</LinearLayout>

(activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="jp.oops.listmyadapter.MainActivity">

    <ListView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/list" />

</RelativeLayout>

 試したこと

前出の実験コード1~2のいずれを有効にしてみました。
実験コード2の箇所で、EditTextへ入力したデータを取得できれば、望む結果になりそうです。

 補足情報(Android Studio 3.1.4 / windows10 Pro 64bit)

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

ENTERキーが押されなくても、EditText 内のテキストが変わったらそれを保持しておきたいとのことなので、実験コード1で使われている TextWatcher を使うのがよいと思います。実験コード2の方法では、ENTERなどのキーが押されないと、保持しているデータを更新できません。

 実験コード1の問題点

実験コード1での「スクロール範囲外の行も一緒に変わってしまう」問題は、ListViewが、画面外に消えた行のViewを、画面内に入ってくる別の行のために再利用していることに起因しています。

TextView.addTextChangedListener() は、"add" という名前が示す通り、新しいリスナー (TextWatcher) を追加登録するためのメソッドです。このメソッドを呼び出しても、EditTextにすでに登録されているリスナーは削除されません。つまり、新しい行のためにViewを再利用するとき、古い行のためのリスナーは EditText に登録されたままです。その結果、EditText のテキストが変更されると、新しい行のためのリスナーと古い行のためのリスナーの両方が呼ばれ、古い行も更新されてしまいます。

 実験コード2がうまく動いた理由

実験コード2で利用している View.setOnKeyListener() は、新しいリスナー (View.OnKeyListener) を上書き登録するためのメソッドです。このメソッドを呼び出すと、View に登録されている古いリスナーは削除されます。新しい行のためにViewを再利用するとき、古い行のための View.OnKeyListener は EditText から削除されるので、古い行の ListItem は更新されなくなり、期待通りの動作になります。

 TextWatcher を使いながらスクロール範囲外の行は変えないようにする方法

上記をふまえて、実験コード1を修正する方法ですが、大きく以下の2つの方針が考えられます。

  • 方針1: Viewを再利用するときに、古いリスナーを削除する
  • 方針2: Viewを再利用するときに、EditText に登録されている TextWatcher も再利用する

TextView に「登録済みの TextWatcher を全て削除する」というようなメソッドが用意されているなら、方針1が簡単なのですが、残念ながら用意されていないため、今回のケースでは方針2のほうがシンプルに実装できそうです。

方針2で TextWatcher を再利用するというのは、ある TextWatcher が更新対象とする ListItem を、再利用時に変える必要があるということです。実装方法はいくつか考えられますが、TextWatcher を ViewHolder の中に移動して、TextWatcher が更新対象とする ListItem を ViewHolder のフィールドとして管理するのがやりやすいと思います。

言葉で言われてもよくわからないと思うので、以下にコード例を示します。MyListAdapter.ViewHolder と MyListAdapter.getView() を次のように変更すれば、うまく動くと思います。

    static class ViewHolder {
        private final EditText editText;
        private ListItem item;

        private TextWatcher textWatcher = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void afterTextChanged(Editable editable) {
                if (item != null) {
                    item.setValue(editable.toString());
                }
            }
        };

        ViewHolder(EditText editText) {
            this.editText = editText;
            editText.addTextChangedListener(textWatcher);
        }

        void setListItem(ListItem item) {
            this.item = item;
            editText.setText(item.getValue());
        }
    }
    public View getView(final int position, View convertView, final ViewGroup parent) {
        Activity activity = (Activity) this.context;
        final ListItem item = (ListItem) this.getItem(position);
        final MyListAdapter.ViewHolder holder;

        if (convertView == null) {
            convertView = activity.getLayoutInflater().inflate(this.resource, null);
            holder = new MyListAdapter.ViewHolder(convertView.findViewById(R.id.editText));
            convertView.setTag(holder);
        } else {
            holder = (MyListAdapter.ViewHolder) convertView.getTag();
        }

        holder.setListItem(item);

        return convertView;
    }

変更ポイントは次の通りです。

  • TextWatcher の実装を ViewHolder の内部に移動しました。getView() が呼び出されるたびに新しい TextWathcer を作成して EditText に設定するのではなく、ViewHolder の作成時に一回だけ TextWatcher を作成して EditText に設定するようにしています
  • ListItem を ViewHolder.item で保持するようにしました。TextWatcher が更新対象としている ListItem を、ViewHolder.item で保持しています

どうでしょうか。不明な点があれば追加で説明しますので、コメントいただければと思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/04 09:57

    お示し頂いたコードで実験したところ、意図した動作になりました。
    私の環境では以下の行でキャストが必要でした。
    holder = new MyListAdapter.ViewHolder(convertView.findViewById(R.id.editText));
                           ↓
    holder = new MyListAdapter.ViewHolder((EditText)convertView.findViewById(R.id.editText));

    ViewHolder側にTextWatcherを保持すること、Viewを再利用していることを意識することの重要性が大変よく分かりました。

    ありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.77%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る