2011年06月14日

カスタムListViewのチェックボックスの値の取得方法が分からない

<本エントリーは正しくないので、2011-07-26 23:47 「シンプルな答え(ListView + CheckBox)」をご覧ください。2011/08/08>

いや、ホントにもう駄目。(プログラムから)足を洗いたいと思いました。
だって、こんなの簡単にできるんだろうと思いこんでいたわけですよ。
主にファイルマネージャとかで、ファイルの複数選択とかで良く使われる機能じゃないですか。
定型的にできなくても、さほど難しいはずはないと思いましたし、そうでなくともググれば情報が見つかることに疑問を抱いていませんでした。
けれど、今回はSQLiteデータベースをSDカードに置く方法を調べた時以上にはまりました。
それはもう嫌になるくらい。
根本的な問題として、アダプターとViewの関係が自分で良く分かっていないことが挙げられます。いや、むしろクラス変数というものが良く分かっていないのでしょうか?というより、それらがごちゃごちゃなのが一番の問題か……。結局情報が見つかっても書いてあることが理解できないし、ヒントすら得られない。
内容からしてそのものズバリのソースがどこにもないのが不思議な気がしますが、見つけ方が良くなかったのか、やはり見つかった情報で理解せねばならないものなのか?
ここら辺がプラグラマーの限界で、分かんなきゃやめろよ!の世界なんでしょうね。

しかし、公開済みのアプリまで中途半端な状態で足を洗って良いものか……(まあ本当に一握りではありますが(笑))悩むところではありますが、やはり僕が本格的なプログラミングをするのには無理があるのでしょう。
今後もどれほど悩むのか?ひとつ悩むのにいちいち二週間とかかかっていたら、ひとつのアプリがいつになったら完成するのか茫然としてしまいます。

で、結局どうなったかというと、先日ようやく怪しいながらも動くようになりました。
でも、本当にこれで良いのか分からないし、論理的に理解して動いているのではないところが残念なところ。
分かっていれば、もっと高揚感もあるのでしょうけど、気分は落ち込みっぱなしです。

さて、具体的な内容に踏み込むと、元々やりたかったこととしては

1.端末にインストールされているアプリの一覧とそのアプリ名、アイコンなどをカスタマイズされたListViewに表示する
2.一覧から(後に利用するための)アプリケーションを選択させるためにCheckboxを各項目に表示する
3.Checkboxでチェックのついているアプリケーションの情報をデータベースに保管する

ということです。

カスタマイズしたListViewは、アイテム(行として表示される項目)となるセッターやゲッターを備えた変数のクラスを用意し、ArrayAdapterまたはBaseAdapterを拡張したAdapterをListViewに載せる訳ですが(言ってること合ってるかな?笑)、Checkedかどうかの情報をAdapterに持たせるのか、変数のクラスのインスタンスに持たせるのかという原理自体が理解できませんでした。
セッターを設ける以上アイテムとなるクラスだろうな、と思って最初自分で変数のクラスにチェックされているかどうかのbooleanなセッターとゲッターを用意したけれど、うまくいきませんでした。

そうなると、ArrayAdapterを拡張したクラスに、ということになりますが、ListViewに設けたCheckBoxとどうやって連動させるのかが分からない……
カスタマイズしたListViewの場合アイテムのクリックがCheckboxに反映しないらしく、どこを(各アイテムの行 or CheckBox)クリックすればアイテムがCheckedになるのかが判断をつけづらいということもありました。(値の取得方法もうまくいっているのか分からなかったため)

解決のポイントの一つとしてはListViewにはlistView.setItemsCanFocus(false)を設定することでアイテムにフォーカスが当たらないようにすることと、チェックの有無の取得にはSparseBooleanArrayを使うことでした。
SparseBooleanArrayについてはitmammoth様のサイト(http://d.hatena.ne.jp/itmammoth/20110505/1304568153)を参考にさせていただき、ListView中のCheckedな値を取り出せるということで、ListViewがどのようにチェックされているかをログ出力で観察しました。
すると、どうやらチェックされている値が拾えている模様。
(ちなみに、SparseBooleanArrayのsize()メソッドはクリックされているアイテムの数と一致しないので、ListView中でCheckedかどうかのbooleanを返すだけのような感じで、初めて見る動きでした)

しかし、目で見えているCheckBoxに反映していないので、ArrayAdapterを拡張したクラスのgetViewメソッドにCheckboxを登録した後、以下のように設定。

final int p = position;
checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
public void onCheckedChanged(CompoundButton cb, boolean isChecked) {
listView.setItemChecked(p, isChecked);
}
});

すると、なぜかCheckboxをタップした時メチャクチャなところ(全然別な項目)にチェックがつくという奇妙な現象が発生。
法則性も理由も分からなかったのですが、適当に

if(listView.isItemChecked(position)){
checkbox.setChecked(true);
}else{
checkbox.setChecked(false);
}

とコードを継ぎ足すと一応正常にチェックがつくようになりました。
「一応」というのは、一瞬メチャクチャな位置についた後正常な位置にチェックされているように見えるからです。。
動くことは動くけど正しいやり方ではなさそうですね、やっぱり。

しかし、とりあえずこれでSparseBooleanArray checkedArray = listView.getCheckedItemPositions()によってListViewからアイテムを拾っていくと、チェックを入れたものと一致することが確認できました。
なんだか釈然としませんが、一応これが現在の限界で、もうこれ以上悩みたくないので先へ進もうかと思います。
うまく説明できていませんが、説明を補足するために、素人のダメダメなソースの抜粋を記載しておきます。(笑←突っ込みどころ満載でしょう?)


<<SelectApp.java>>
import文省略

public class SelectApp extends Activity {

ListView listView;

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

/* 中略 */

        PackageManager pm = getPackageManager();
        Intent intent = new Intent(Intent.ACTION_MAIN,null);
        intent.addCategory(Intent.CATEGORY_LAUNCHER);

        final List<ResolveInfo> appList = pm.queryIntentActivities(intent, 0);
        Collections.sort(appList,new ResolveInfo.DisplayNameComparator(pm));

        final List<ListItem> list = new ArrayList<ListItem>();

        for(ResolveInfo resolveInfo : appList){
            ListItem listItem = new ListItem();
         listItem.setName(resolveInfo.loadLabel(pm).toString());
         listItem.setImage(resolveInfo.loadIcon(pm));
         list.add(listItem);
        }

        ListItemAdapter adapter = new ListItemAdapter(this,0,list);

        listView = (ListView)findViewById(R.id.listview);
        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); // 複数選択モード
        listView.setItemsCanFocus(false); // アイテムにフォーカスが当たらないようにする
        listView.setAdapter(adapter);

// アイテム内のCheckboxにチェックをつけていく
     for(int i = 0;i < listView.getCount();i++){
     ListItem listItem = new ListItem();
     listItem = (ListItem)listView.getItemAtPosition(i);
     String appName = listItem.getName();
     for(int i2 = 0;i2 < checkedItemName.size();i2++){
         if(appName.equals(checkedItemName.get(i2).toString())){
         listView.setItemChecked(i, true);
         }
     }
     }
}

    class ListItem{
// アイテムとなる変数クラス
     private Drawable draw;
     private String appName;

     public Drawable getImage(){
     return draw;
     }
     public void setImage(Drawable draw){
     this.draw = draw;
     }
     public String getName(){
     return appName;
     }
     public void setName(String appName){
     this.appName = appName;
     }
    }

    public class ListItemAdapter extends ArrayAdapter<ListItem> {
     private LayoutInflater mInflater;

     public ListItemAdapter(Context context,int rid,List<ListItem> custList){
     super(context,rid,custList);
     mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     }

     public View getView(int position,View convertView,ViewGroup parent){
     if(convertView == null){
     convertView = mInflater.inflate(R.layout.list_item, null);
     }
     //データを取り出す
     ListItem item = (ListItem)getItem(position);

     //チェックボックスをセット
     CheckBox checkbox = (CheckBox)convertView.findViewById(R.id.check);

     //チェックボックスが変化したら、listViewにチェックのオンオフを保存する
     final int p = position;
     checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
public void onCheckedChanged(CompoundButton cb, boolean isChecked) {
listView.setItemChecked(p, isChecked);
}
     });

     // ここを入れないとチェックが関係のない場所につく
     if(listView.isItemChecked(position)){
     checkbox.setChecked(true);
     }else{
     checkbox.setChecked(false);
     }

     //画像をセット
     ImageView image = (ImageView)convertView.findViewById(R.id.image);
     image.setImageDrawable(item.getImage());

     //アプリ名をセット
     TextView name = (TextView)convertView.findViewById(R.id.name);
     name.setText(item.getName());

     return convertView;
     }
    }

/* バックキーの処理 */
@Override
public boolean dispatchKeyEvent(final KeyEvent event){
// バックキーが押されたらデータベースにチェックされたアプリの情報を保存する

boolean status = false;
switch(event.getKeyCode()){
case KeyEvent.KEYCODE_BACK:
status = onKeyEventBack(event);
break;
}
if(status){
return true;
}
return super.dispatchKeyEvent(event);
}

public boolean onKeyEventBack(final KeyEvent event){
// 終了時にDBを保存する

if(event.getAction() == KeyEvent.ACTION_DOWN){
dbWrite();

     finish();
}
return true;
}

public void dbWrite(){
// 既存のテーブルを破棄して、ListViewからチェックのついている項目を書き込んだテーブルを作る

     SparseBooleanArray checkedArray = listView.getCheckedItemPositions(); // ListViewからチェックされている箇所を取得

     DBHelper helper = new DBHelper(this);
     db = helper.getWritableDatabase();

     db.execSQL("drop table if exists " + DB_TABLE + ";");
     db.execSQL("create table if not exists " + DB_TABLE + " (_id integer primary key,app_name text);");

     ContentValues cv = new ContentValues();

     for(int i = 0;i < listView.getCount();i++){
     if(checkedArray.get(i)){
     ListItem item = new ListItem();
     item = (ListItem)listView.getItemAtPosition(i);
     cv.put("app_name",item.getName());
     db.insert(DB_TABLE, null,cv);
     }
     }
     db.close();
}

/* 以下略 */

}


<<selectapp.xml>> Activityのレイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
    <ListView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/listview"
    />
</LinearLayout>


<<list_item.xml>> リストを表示するためのレイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:orientation="vertical" >
<LinearLayout
  android:orientation="horizontal"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content">
  <CheckBox
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:id="@+id/check" />
      <ImageView
       android:layout_width="40dip"
       android:layout_height="40dip"
       android:id="@+id/image"
       />
  <TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:padding="5dip"
   android:id="@+id/name"
   android:textStyle="bold"
   android:textSize="20dip"
   android:gravity="center_vertical"
   />
</LinearLayout>
</LinearLayout>

posted by 白虹 at 23:18| Comment(6) | TrackBack(0) | Android開発
この記事へのコメント
こんばんは、チェックボックスの処理は特におかしくないと思います。
なんとなく疑問が解決できそうなので若干長いコメントを書きますが。。
ListViewのソースを見ればわかりますが内部でViewが使いまわされています。
初期表示ではViewが生成されていないため、getViewのconvertViewでnullが渡されます。
以下のソースはその際にViewを生成するためですね。
if(convertView == null){
convertView = mInflater.inflate(R.layout.list_item, null);
}
リストがたくさんある場合に下にスクロールしますが、その際convertViewがnullでなく
見えなくなった上のViewが使いまわされて渡ってきます。
若干分かりにくいですが図で説明すると以下のような感じです。
・2〜5を表示中([]内が画面表示中と思ってくださいorz
1,[2,3,4,5],6
・6を表示するためにスクロール
1,2,[3,4,5,6]
この際getViewのconvertViewに1を表示していたViewが渡ってきます。
そのためcheckboxを6の値に設定し直さなければ、1のcheckboxが表示されます。
アプリ名をセットする処理をconvertViewがnullでない場合処理しないようにすると使いまわされているのがわかると思います。

あと、全然関係ないですがdbへのinsert処理はtransactionを張るとかなり速くなりますよ。
http://www.atmarkit.co.jp/fsmart/articles/android06/android06_2.html


長くなりましたが疑問が解決出来れば幸いです。。
Posted by すぴ at 2011年06月16日 20:31
すぴさん
またまた、コメントありがとうございます。
私はご承知の通りど素人で、身近にプログラムができる人がいる訳でもないので、こうして教えていただけると大変大変ありがたいです。
残念ながらというか、申し訳ないことに私の理解力の方が充分に追いついていないですが、何度か繰り返し読んでみて、疑問が解けそうな気がしてきました。
transactionについてもトライしてみたいと思います。
Posted by 白虹 at 2011年06月16日 23:14
I’m attempting to start out one page using the portrait digital photography organization. Actually increased self-sufficient obtain ended up being becoming best choice?. cheap jordan shoes http://www.cheapairjordanshoesonline.com/
Posted by cheap jordan shoes at 2013年06月10日 00:39
Wink, the segment dust has already been apart from fishing boat however the distance of one inside, the segment dust just thought that breath hurtles up, at this time ……
Posted by louis vuitton handbags at 2013年08月09日 06:30
The "small brothers, small brothers ……" middle age women shout a way.Just the segment at this time dust already Liu Jia Cun, allow middle age women to shout to break throat to also have no a homework.
Posted by used louis vuitton at 2013年08月11日 03:49
Leaf's elder, God's way alliance x evil alliance in the sky estimate are very quick for these several years and then will have a big activity, call up large numbers of monks of gold Dan and dollar baby's monk, go to sea of blood to construct fairy city.The sea of blood will certainly become a huge battlefield when the time comes.Is exactly this temple to use person's moment now, you resign from at the moment all handle, the purple sword temple is then little a war capability"
Posted by red bottom high heels at 2013年08月11日 03:49
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/46012258

この記事へのトラックバック