スポンサーリンク

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 を考えます。
public class MainActivity extends Activity {

    private static final String[] FRUITS = {
        "Apple", "Banana", "Lemon", "Orange", "Grape"
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ListView listView = new ListView(this);
        listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);

        ArrayAdapter<String> adapter =
            new ArrayAdapter<>(this, android.R.layout.simple_list_item_single_choice);
        adapter.addAll(FRUITS);
        listView.setAdapter(adapter);

        setContentView(listView);
    }
}


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

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

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

public class CheckableLinearLayout extends LinearLayout implements Checkable {

    private RadioButton mRadioButton;

    public CheckableLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mRadioButton = (RadioButton) findViewById(R.id.radio_button);
    }

    @Override
    public void setChecked(boolean b) {
        mRadioButton.setChecked(b);
    }

    @Override
    public boolean isChecked() {
        return mRadioButton.isChecked();
    }

    @Override
    public void toggle() {
        mRadioButton.toggle();
    }
}

Checkable インタフェースの処理は全て RadioButton に委譲します。
次にこのレイアウトを使ったレイアウトXML (item_layout_checkable.xml) を作成します。

<com.example.singlechoicetest.CheckableLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:id="@+id/text_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        />

    <!-- フォーカスを持ったり、クリックイベントを拾わないようにする -->
    <RadioButton
        android:id="@+id/radio_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:focusable="false"
        android:focusableInTouchMode="false"
        android:clickable="false"
        />

</com.example.singlechoicetest.CheckableLinearLayout>

ここで気をつけなくてはいけないのは、RadioButton がフォーカスを持ったり、クリックイベントを拾わないようにすることです。これをしないとクリックイベントが ListView に伝わらず、SingleChoice 動作ができません。
更にこのレイアウトを使う ArrayAdapter を作成します。
public final class FruitAdapter extends ArrayAdapter<String> {

    private static final String[] FRUITS = {
        "Apple", "Banana", "Lemon", "Orange", "Grape"
    };

    private final LayoutInflater mInflater;

    FruitAdapter(Context context) {
        super(context, 0);
        addAll(FRUITS);

        mInflater = LayoutInflater.from(context);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = convertView;
        if (view == null) {
            view = mInflater.inflate(R.layout.item_layout_checkable, parent, false);
        }

        TextView textView = (TextView) view.findViewById(R.id.text_view);

        String fruit = getItem(position);
        textView.setText(fruit);

        return view;
    }
}

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

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

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ListView listView = new ListView(this);
        listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);

        ListAdapter adapter = new FruitAdapter(this);

        listView.setAdapter(adapter);
        setContentView(listView);
    }
}

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

ArrayAdapter に Single Choice 機能を実装する

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

public final class SingleChoiceAdapter extends ArrayAdapter<String> {

    private static final String[] FRUITS = {
        "Apple", "Banana", "Lemon", "Orange", "Grape"
    };

    private final LayoutInflater mInflater;

    private int mSelectedIndex = -1;

    SingleChoiceAdapter(Context context) {
        super(context, 0);
        addAll(FRUITS);

        mInflater = LayoutInflater.from(context);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = convertView;
        if (view == null) {
            view = mInflater.inflate(R.layout.item_layout, parent, false);
        }

        // 本当はここで ViewHolder パターンを使うべきだが省略
        TextView textView       = (TextView)    view.findViewById(R.id.text_view);
        RadioButton radioButton = (RadioButton) view.findViewById(R.id.radio_button);

        String fruit = getItem(position);
        textView.setText(fruit);

        radioButton.setChecked(position == mSelectedIndex);

        return view;
    }

    void setSelectedIndex(int index) {
        mSelectedIndex = index;
        notifyDataSetChanged();
    }
}

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

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:id="@+id/text_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        />

    <!-- フォーカスを持ったり、クリックイベントを拾わないようにする -->
    <RadioButton
        android:id="@+id/radio_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:focusable="false"
        android:focusableInTouchMode="false"
        android:clickable="false"
        />

</LinearLayout>

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

後はこのカスタム ArrayAdapter を ListView に設定してやります。
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ListView listView = new ListView(this);
        listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);

        final SingleChoiceAdapter adapter = new SingleChoiceAdapter(this);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                adapter.setSelectedIndex(i);
            }
        });

        listView.setAdapter(adapter);
        setContentView(listView);
    }
}

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

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



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

0 件のコメント :

コメントを投稿