スポンサーリンク

2016年7月20日水曜日

Android カスタムレイアウトの Single Choice ListView を作る方法

Android で Single Choice の ListView を作る場合、simple_list_item_single_choice.xml という単純なレイアウトが用意されています。これは TextView + RadioButton というシンプルなものです。(昔は TextView が二段になった simple_list_item_2_single_choice.xml というのもあった筈ですが、何故か今のSDKでは使えなくなっています。) しかしカスタムレイアウトで Single Choice をやろうとすると結構面倒です。多くの人が同じ苦労を繰り返さないよう、ここにメモを残しておこうと思います。



カスタムレイアウトで Single Choice の ListView を作る方法はいくつかあります。ここでは以下の二つの方法を考えます。
  • カスタムレイアウトに Checkable インタフェースを実装する
  • ArrayAdapter に Single Choice 機能を実装する

まず以下の様な単純な ListView を考えます。
  1. public class MainActivity extends Activity {
  2. private static final String[] FRUITS = {
  3. "Apple", "Banana", "Lemon", "Orange", "Grape"
  4. };
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. ListView listView = new ListView(this);
  9. listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
  10. ArrayAdapter<String> adapter =
  11. new ArrayAdapter<>(this, android.R.layout.simple_list_item_single_choice);
  12. adapter.addAll(FRUITS);
  13. listView.setAdapter(adapter);
  14. setContentView(listView);
  15. }
  16. }

ここではまだカスタムレイアウトは使っていません。simple_list_item_single_choice を使った単純なものです。これをカスタムレイアウトを使うよう修正してみましょう。上で述べた二つの方法を別々に実装してみたいと思います。

カスタムレイアウトに Checkable インタフェースを実装する

まずレイアウトに Checkable インタフェースを実装します。レイアウトは何でも構いませんが、ここでは LinearLayout を使います。

  1. public class CheckableLinearLayout extends LinearLayout implements Checkable {
  2. private RadioButton mRadioButton;
  3. public CheckableLinearLayout(Context context, AttributeSet attrs) {
  4. super(context, attrs);
  5. }
  6. @Override
  7. protected void onFinishInflate() {
  8. super.onFinishInflate();
  9. mRadioButton = (RadioButton) findViewById(R.id.radio_button);
  10. }
  11. @Override
  12. public void setChecked(boolean b) {
  13. mRadioButton.setChecked(b);
  14. }
  15. @Override
  16. public boolean isChecked() {
  17. return mRadioButton.isChecked();
  18. }
  19. @Override
  20. public void toggle() {
  21. mRadioButton.toggle();
  22. }
  23. }
  24.  
Checkable インタフェースの処理は全て RadioButton に委譲します。
次にこのレイアウトを使ったレイアウトXML (item_layout_checkable.xml) を作成します。

  1. <com.example.singlechoicetest.CheckableLinearLayout
  2. xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="horizontal"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. >
  7. <TextView
  8. android:id="@+id/text_view"
  9. android:layout_width="0dp"
  10. android:layout_height="wrap_content"
  11. android:layout_weight="1"
  12. />
  13. <!-- フォーカスを持ったり、クリックイベントを拾わないようにする -->
  14. <RadioButton
  15. android:id="@+id/radio_button"
  16. android:layout_width="wrap_content"
  17. android:layout_height="wrap_content"
  18. android:layout_gravity="right"
  19. android:focusable="false"
  20. android:focusableInTouchMode="false"
  21. android:clickable="false"
  22. />
  23. </com.example.singlechoicetest.CheckableLinearLayout>

ここで気をつけなくてはいけないのは、RadioButton がフォーカスを持ったり、クリックイベントを拾わないようにすることです。これをしないとクリックイベントが ListView に伝わらず、SingleChoice 動作ができません。
更にこのレイアウトを使う ArrayAdapter を作成します。
  1. public final class FruitAdapter extends ArrayAdapter<String> {
  2. private static final String[] FRUITS = {
  3. "Apple", "Banana", "Lemon", "Orange", "Grape"
  4. };
  5. private final LayoutInflater mInflater;
  6. FruitAdapter(Context context) {
  7. super(context, 0);
  8. addAll(FRUITS);
  9. mInflater = LayoutInflater.from(context);
  10. }
  11. @Override
  12. public View getView(int position, View convertView, ViewGroup parent) {
  13. View view = convertView;
  14. if (view == null) {
  15. view = mInflater.inflate(R.layout.item_layout_checkable, parent, false);
  16. }
  17. TextView textView = (TextView) view.findViewById(R.id.text_view);
  18. String fruit = getItem(position);
  19. textView.setText(fruit);
  20. return view;
  21. }
  22. }

これだけ単純なレイアウトであればわざわざ ArrayAdapter のサブクラスを作る必要もありませんが、一応複雑なレイアウトも想定して敢えてサブクラス化しました。また、getView() では ViewHolder パターンを使うべきですが、ここでは省略します。

最後にこれらを使って ListView を作ってみましょう。

  1. public class MainActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. ListView listView = new ListView(this);
  6. listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
  7. ListAdapter adapter = new FruitAdapter(this);
  8. listView.setAdapter(adapter);
  9. setContentView(listView);
  10. }
  11. }

はい、これで出来上がり。この方法のちょっと嫌な所は、CheckableLinearLayout と XMLレイアウトが密に結び付いていることです。

ArrayAdapter に Single Choice 機能を実装する

次に別の方法、Checkable インタフェースを使わず、ArrayAdapter に Single Choice 機能を実装する方法を考えたいと思います。
まず、ArrayAdapter のサブクラスを作ります。

  1. public final class SingleChoiceAdapter extends ArrayAdapter<String> {
  2. private static final String[] FRUITS = {
  3. "Apple", "Banana", "Lemon", "Orange", "Grape"
  4. };
  5. private final LayoutInflater mInflater;
  6. private int mSelectedIndex = -1;
  7. SingleChoiceAdapter(Context context) {
  8. super(context, 0);
  9. addAll(FRUITS);
  10. mInflater = LayoutInflater.from(context);
  11. }
  12. @Override
  13. public View getView(int position, View convertView, ViewGroup parent) {
  14. View view = convertView;
  15. if (view == null) {
  16. view = mInflater.inflate(R.layout.item_layout, parent, false);
  17. }
  18. // 本当はここで ViewHolder パターンを使うべきだが省略
  19. TextView textView = (TextView) view.findViewById(R.id.text_view);
  20. RadioButton radioButton = (RadioButton) view.findViewById(R.id.radio_button);
  21. String fruit = getItem(position);
  22. textView.setText(fruit);
  23. radioButton.setChecked(position == mSelectedIndex);
  24. return view;
  25. }
  26. void setSelectedIndex(int index) {
  27. mSelectedIndex = index;
  28. notifyDataSetChanged();
  29. }
  30. }

選択中のインデックスを保持するメンバー mSelectedIndex を用意し、getView() の中で適宜 RadioButton のチェック状態を設定します。また外部からこれを設定するメソッド setSelectedIndex(int) を作成します。このメソッドが呼ばれた時に View を更新するため、notifyDatasetChanged() を実行します。

この中で使っているレイアウトXML (item_layout.xml) は先程のものとほとんど同じですが、これも一応載せておきます。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:orientation="horizontal"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. >
  8. <TextView
  9. android:id="@+id/text_view"
  10. android:layout_width="0dp"
  11. android:layout_height="wrap_content"
  12. android:layout_weight="1"
  13. />
  14. <!-- フォーカスを持ったり、クリックイベントを拾わないようにする -->
  15. <RadioButton
  16. android:id="@+id/radio_button"
  17. android:layout_width="wrap_content"
  18. android:layout_height="wrap_content"
  19. android:layout_gravity="right"
  20. android:focusable="false"
  21. android:focusableInTouchMode="false"
  22. android:clickable="false"
  23. />
  24. </LinearLayout>

CheckableLinearLayout の代りに LinearLayout を使っているだけです。

後はこのカスタム ArrayAdapter を ListView に設定してやります。
  1. public class MainActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. ListView listView = new ListView(this);
  6. listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
  7. final SingleChoiceAdapter adapter = new SingleChoiceAdapter(this);
  8. listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  9. @Override
  10. public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
  11. adapter.setSelectedIndex(i);
  12. }
  13. });
  14. listView.setAdapter(adapter);
  15. setContentView(listView);
  16. }
  17. }

ListView の onItemClick() で先程作成した setSelectedItem(int) を呼び出すようにします。

どちらの方法を使っても外見の動作は全く同じです。一応こんな感じになります。



テストに使ったソースコードをここに置きました。
https://github.com/masamichi441/AndroidSingleChoiceTest

0 件のコメント :

コメントを投稿