ListViewでAdapterをカスタマイズしてgetChildAtでのnull回避
AndroidのListViewはAdapterを介してコレクションを表示する。
ただ、ListViewは画面内に表示できる数の行を作成してAdapterに登録している情報を表示しているだけ。
Adapterがその行であるViewの情報を入れ替えてスクロールに対応している。
その使いまわしに気をつけた実装をしなければいけないのはAdapter#getView関連で調べると出てくる。
ユーザーから見てListViewは固定された透明な外箱の画面を通して、
その先にAdapterというスクロール可能な内箱が並べてあり、
各内箱の中に情報が入っているような感じのイメージで理解している。
前回の項目選択時の背景色変更に加えてボタン操作で項目の入れ替えをしようと画策。
そのため、項目と言うかListViewの子要素(View)を取得したいのだけど、
上記理由からListVew#getChildAtで取得しようにも初期表示の子要素は取得できるが、
スクロール後の子要素はnullになってしまい取得できない。
ようはButtonでのonClick(View view)の利用などListView外部からの動的変更のために
ListViewの子要素(View)を取得したい。
雑なお試し
public class sample extends Activity{ 省略 int position = -1; ArrayAdapter<String> adapter; ListView list; 省略 adapter = new ArrayAdapter<String>(this, R.layout.aaa, bbb); list = (ListView)this.findViewById(R.id.xxx); list.setAdapter(adapter); list.setOnItemClickListener(new OnItemClickeListener(){ @Override public void onItemClick((AdapterView<?> adapterView, View view, int position, long id){ setPosition(position); } }); Button button = (Button)this.findViewById(R.id.zzz); button.setOnClickListener(new OnClickListener(){ @Override public void onClick(View view){ /*クリックした要素の前後2要素までを対象*/ int count = -2; /*getChildAtの場合*/ for(int i = 0;i < 5;i++){ System.out.println(count + ": getChildAt : " +list.getChildAt(position + count)); count++; } /*viewの場合*/ System.out.println(count + ": view : " + view); } }); 省略 public void setPosition(int position){ this.position = position; } public int getPosition(){ return position; } 省略 }
実行結果
初期表示のままクリック -2: getChildAt : android.widget.LinearLayout@405f6e68 -1: getChildAt : android.widget.LinearLayout@405f7830 0: getChildAt : android.widget.LinearLayout@405f81f8 1: getChildAt : android.widget.LinearLayout@405f8bc0 2: getChildAt : android.widget.LinearLayout@405f9588 0: view : android.widget.LinearLayout@405f81f8 スクロール後にクリック -2: getChildAt : null -1: getChildAt : null 0: getChildAt : null 1: getChildAt : null 2: getChildAt : null 0: view : android.widget.LinearLayout@405fa918
どうにか手間をかけずにこのままどうにかしようと思ったけど諦めた。
スクロールに対応した子要素の取得のために、カスタムアダプターを作成。
カスタムアダプター
public class CustomAdapter extends ArrayAdapter<SampleRow>{ 省略 Map<Integer, View> positionView = new HashMap<Integer, View>(); /*もしくは SparseArray<View> positionView = new SparseArray<View>();*/ @Override public View getView(int position, View convertView, ViewGroup parent) { View view = convertView; ViewHolder holder; SampleRow sr = (SampleRow)getItem(position); if(view == null){ view = inflater.inflate(R.layout.sample_row, parent, false); holder = new ViewHolder(); holder.nameView = (TextView)view.findViewById(R.id.sample_row_name); view.setTag(holder); }else{ holder = (ViewHolder)view.getTag(); } holder.nameView.setText(sr.getName()); /*キーの重複不可を利用してgetViewで生成されるViewを格納*/ positionView.put(position, view); return view; } public View getPositionView(int targetPosition){ View targetView = positionView.get(targetPosition); return targetView; } 省略 }
修正後
public class Sample extends Activity{ int position = -1; ArrayAdapter<String> adapter; ListView list; 省略 adapter = new Customadapter(this, 0, bbb); list = (ListView)this.findViewById(R.id.xxx); list.setAdapter(adapter); list.setOnItemClickListener(new OnItemClickeListener(){ @Override public void onItemClick((AdapterView<?> adapterView, View view, int position, long id){ setPosition(position); } }); Button button = (Button)this.findViewById(R.id.zzz); button.setOnClickListener(new OnClickListener(){ @Override public void onClick(View view){ /*処理対象となるListView内の子要素を取得*/ View clicked = adapter.getPositionView(getPosition()); 処理 /*上記処理でgetViewが呼び出される場合、getViewの処理を先に終わらせてから対象となる部分を取得しなおす*/ list.post(new Runnable(){ public void run(){ /*処理後に改めて取得*/ View clicked = adapter.getPositionView(getPosition()); /*処理後に改めて選択*/ clicked.setSelected(true); } }); } }); 省略 public void setPosition(int position){ this.position = position; } public int getPosition(){ return position; } 省略 }
カスタムアダプターを生成してgetView中にconvertViewをMapに格納していく。
Mapはキーの重複時に上書きするので、スクロールでサイズが増えていくことも防げる。
とりあえず素人考えで実装はしてみた。
勿論きちんと期待通りに動く。
効率がいいかはわからない。