2012年8月25日土曜日

カスタムのListViewでSinglechoiceのRadioButtonを実装する

Singlechoiceのラジオボタンを持つリストに関して、【テキスト+ラジオボタン】
であれば下記の方法で簡単に実装出来る。
public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView listView = (ListView)findViewById(R.id.listview);
        listView.setAdapter(new ArrayAdapter<String>(  
            this,  
            android.R.layout.simple_list_item_single_choice,  
            new String[]{"a", "b", "c"})  
        );
    
        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
    }

}
しかし【画像+テキスト+ラジオボタン】となると話は変わってくる。
カスタムのレイアウトを作成し、ラジオボタンのクリックリスナーを設定して
Singlechoiceとなるように処理しなければならない。

以下にその一例をメモとして記載しておく。

【動作仕様】
・アプリの起動履歴を取得してリスト表示する。
・リストアイテムは【アプリアイコン+アプリ名+ラジオボタン】とする。
・ラジオボタンをクリックした時にSinglechoiceとして動作させる。
・リストアイテムをクリックした時にもSinglechoiceとして動作させる。
(リストとラジオボタンは独立してクリック可能とする)

【実装例】
・MainActivity.java
アプリの起動履歴を取得してListViewを表示するメインアクティビティクラス。リストのクリックリスナーでラジオボタンのチェック処理を行う
public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        List<RecentTaskInfo> recentTasks = activityManager.getRecentTasks(100, 0);
        final SingleChoiceAdapter adapter = new SingleChoiceAdapter(this, 0, new ArrayList<AppData>());

        for(RecentTaskInfo recentTask : recentTasks){
            Intent intent = recentTask.baseIntent;

            ResolveInfo resolveInfo = getPackageManager().resolveActivity(intent, 0);

            if(resolveInfo == null){
                continue;
            }

            AppData appData = new AppData();
            // アプリアイコン、アプリ名、ラジオボタンのチェック状態を設定する
            appData.setIcon(GraphicUtil.getBitmap(resolveInfo.loadIcon(getPackageManager())));
            appData.setLabel(resolveInfo.loadLabel(getPackageManager()).toString());
            appData.setChecked(false);
            adapter.add(appData);
        }

        if(!adapter.isEmpty()){
            // ここでは最初のリストアイテムのラジオボタンにチェックする
            adapter.getItem(0).setChecked(true);
        }

        ListView listView = (ListView)findViewById(R.id.listview);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id){
                // クリックしたリストのアイテムのみチェック状態とする
                for(int i = 0; i < adapter.getCount(); i++){
                    if(adapter.getItemId(i) == id){
                        adapter.getItem(i).setChecked(true);
                    } else{
                        adapter.getItem(i).setChecked(false);
                    }
                }

                // アダプタの内容を即時反映する
                adapter.notifyDataSetChanged();
            }

        });

    }

}
・SingleChoiceAdapter.java
ListViewに設定するカスタムアダプタクラス。ラジオボタンのクリックリスナーを設定する。
public class SingleChoiceAdapter extends ArrayAdapter<AppData>{

    private List<AppData> appList = null;

    private class ViewHolder{
        ImageView imageView;
        TextView textView;
        RadioButton radioButton;
    }

    public SingleChoiceAdapter(Context context, int resourceId, List<AppData> appList) {
        super(context, resourceId, appList);

        this.appList = appList;
    }

    @Override
    public int getCount() {
        return appList.size();
    }

    @Override
    public AppData getItem(int position) {
        return appList.get(position);
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;

        if(convertView == null) {
            LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.item, null);

            viewHolder = new ViewHolder();
            viewHolder.imageView = (ImageView)convertView.findViewById(R.id.imageview);
            viewHolder.textView = (TextView)convertView.findViewById(R.id.textview);
            viewHolder.radioButton = (RadioButton)convertView.findViewById(R.id.radiobutton);

            convertView.setTag(viewHolder);
        } else{
            viewHolder = (ViewHolder)convertView.getTag();
        }

        final AppData appData = (AppData)getItem(position);

        viewHolder.imageView.setImageBitmap(appData.getIcon());
        viewHolder.textView.setText(appData.getLabel());
        viewHolder.radioButton.setChecked(appData.isChecked());

        viewHolder.radioButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View view) {
                // 一旦全てのチェックをクリアする
                for(int i = 0; i < appList.size(); i++){
                    appList.get(i).setChecked(false);
                }

                // クリックした箇所のみチェックする
                appData.setChecked(true);
                // アダプタ内容を即時反映する
                notifyDataSetChanged();
            }

        });

        return convertView;
    }

}
・AppData.java
リスト表示するアプリ情報クラス。isCheckedがRadioButtonのチェック状態を示す。
public class AppData {

    private Bitmap icon = null;
    private String label = null;
    private boolean isChecked = false;

    public Bitmap getIcon(){
        return icon;
    }

    public String getLabel(){
        return label;
    }

    public boolean isChecked(){
        return isChecked;
    }

    public void setIcon(Bitmap bitmap){
        this.icon = bitmap;
    }

    public void setLabel(String label){
        this.label = label;
    }

    public void setChecked(boolean isChecked){
        this.isChecked = isChecked;
    }

}
・GraphicUtil.java
アプリアイコンのDrawableをBitmapに変換するクラス(メソッド)。よくBitmapDrawableでキャストしてgetBitmap()でBitmapを取得している例を見るが、アプリアイコンにはNinePatchDrawable等のDrawableが設定されている場合があり、この場合ClassCastExceptionが発生してしまう。その対策も含めて下記のように、元のDrawableの縦横サイズを取得してBitmapを生成するようにしている。
public class GraphicUtil {

    public static Bitmap getBitmap(Drawable drawable) {
        int width  = drawable.getIntrinsicWidth();
        int height = drawable.getIntrinsicHeight();
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, width, height);
        drawable.draw(canvas);

        return bitmap;
    }

}
・main.xml
ListViewのレイアウト設定ファイル。
<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" >

 <ListView
     android:id="@+id/listview"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:choiceMode="singleChoice"
     <!-- リストアイテムをスライドさせた時に背景が反転しないようにする -->
     android:scrollingCache="false" />

</RelativeLayout>
・item.xml
ListViewのアイテムのレイアウト設定ファイル。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/imageview"
        android:layout_width="42dp"
        android:layout_height="42dp"
        android:scaleType="fitCenter"
        android:layout_gravity="center_vertical"
        android:layout_margin="8dip" />

    <TextView
        android:id="@+id/textview"
        android:layout_width="0dip"
        android:layout_height="wrap_content"
        android:textSize="23sp"
        android:lines="1"
        android:ellipsize="end"
        android:layout_gravity="center_vertical"
        <!-- Weightを1に設定する事でRadioButtonを右端に寄せる -->
        android:layout_weight="1"
        android:layout_margin="8dip" />

    <RadioButton
        android:id="@+id/radiobutton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical|right"
        android:layout_margin="8dip"
        <!-- 下記2行の設定によりRadioButtonがフォーカスを奪わないようにする -->
        android:focusable="false"
        android:focusableInTouchMode="false" />

</LinearLayout>
実際に実行したイメージは以下の通りとなる。リストをクリックしても
ラジオボタンをクリックしてもSinglechoiceとして動作している。


今回はRadioButtonの全クリアと該当箇所のチェックで実装しているが、
リストアイテム数が増えると効率が悪くなる。そのため全クリアでなく、
前回のチェック箇所のみクリアするようにした方がよい。
(試行錯誤してみたがうまく動作しなかった…)

上記ファイルを含むAndroidプロジェクト一式を下記リンクに置いてみた。
インポートして適当なプロジェクト名で取り込めばそのまま使える。
(MacOS X MoutainLion(10.8.1)、eclipse Juno(4.2)、ADT 20.0.3にて動作確認済み)
https://drive.google.com/open?id=0B3bjmX6Y3dO5WXAwZi0tOEMwSFk

0 件のコメント:

コメントを投稿