2013年7月23日火曜日

SimpleExpandableListAdapter

SimpleExpandableListAdapter

Adapter系のクラスを使うメリットは、データ群を一括して取り扱えることだ。
例えば、SimpleAdapterの場合、文字列群と画像群と、そしてそれら以外のデータ(例えば、自分で作成した任意のクラス等)をも、一括して、対応するView(例えば、TextViewやImageView等)に代入することができた。

(参考)SimpleAdapterの簡単すぎる基本形
    String sFrom[] = {"Bitmap", "Title"};
    int iTo[] = new int[]{
        R.id.Image_List_Favorite,
   R.id.Text_List_Favorite_Title
    };
      sa = new SimpleAdapter(mainActivity, al, R.layout.list_keyword, sFrom, iTo);
上記は、"Bitmap"と命名した画像データを、R.id.Image_List_FavoriteというImageViewに代入&表示する。

しかし、SimpleExpandableListAdapterの場合、一括して代入できるのは文字列データだけである。画像データはできないし、それ以外のデータもできない。仕様です。

画像データを、対応するImageViewに代入する場合、それ用のプログラムをしなければならない。
また、ChildViewに、例えばButtonを配置した場合、そのButtonに対する利用者からのクリック状況の把握の仕方もプログラムしなければならない。

サンプルプログラムは下記のとおりです。

    String sFrom[] = {"Title"};
    int iTo[] = new int[]{
        R.id.Text_List_Favorite_Title//自動的に代入できるのは、文字列データだけである。画像データを取り扱うことはできない。
    };
    private ExpandableListView lv;
    private MySA sa;
    private ArrayList<ArrayList<Map<String, Object>>> ChildrenList;
 
    /* 画像データを表示させ、かつ、ChildViewにButtonを実装させるため、
    SimpleExpandableListAdapterを拡張して使う。
    */
    class MySA extends SimpleExpandableListAdapter{
        /*
         * constructorは、Eclipseが自動生成したものをそのまま使う。
         * constructorは、ごちゃごちゃしているが、何も問題は無い。見ないでおこう。
         */
        public MySA(Context context,
                List<? extends Map<String, ?>> groupData, int groupLayout,
                String[] groupFrom, int[] groupTo,
                List<? extends List<? extends Map<String, ?>>> childData,
                int childLayout, String[] childFrom, int[] childTo) {
            super(context, groupData, groupLayout, groupFrom, groupTo, childData,
            childLayout, childFrom, childTo);
        }

    /*以下はプログラミングする箇所である。*/
        @Override
        public View getChildView(int groupPosition, int childPosition,
            boolean isLastChild, View convertView, ViewGroup parent) {

            View v = super.getChildView(groupPosition, childPosition, isLastChild,
                convertView, parent);
            /*
             * superによる処理が終わった後のViewを使うこと。
    super内で文字列の代入が行われているのである。
             * 以下は、ImageViewへ画像を代入する処理である。
    つまり、文字列データの処理を先にしてから画像データの処理を行う。
             */
            RelativeLayout rl = (RelativeLayout)v;//私のPGではRelativeLayoutである
            ImageView iv = (ImageView)rl.findViewById(R.id.Image_List_Favorite);
            Map<String, Object> m = ChildrenList.get(groupPosition).get(childPosition);
            Bitmap bm = (Bitmap)m.get("Bitmap");
            iv.setImageBitmap(bm);
            
            /*
             * 下記newChildView()においてButtonにリスナーを付けクリックイベントを捕捉できるようにする。しかし、それだけでは個々のButtonを識別できない。
このため、識別できるようにするため、次のようにタグを付ける。
             */
            Button btn = (Button)rl.findViewById(R.id.Button_List_Favorite);
            long l = ExpandableListView.getPackedPositionForChild(
                    groupPosition, childPosition);
            btn.setTag(l);
            return v;
        }

        /*
         * 次のnewChildView()は、上記getChildView()のsuper.getChildView()内において、
         * アプリ開始時にChildViewが新規に生成する時に呼ばれる。
         * 一定数のChildViewを生成し終えたら、もはや生成されることはない。
         * 生成後の処理は、getChildView()で行うことになる。
         * layout用xmlにおいてViewを書いてあるので、基本的にはnewChildView()をイジル必要は無い。
         */
        @Override
        public View newChildView(boolean isLastChild, ViewGroup parent) {
            View v = super.newChildView(isLastChild, parent);
            RelativeLayout rl = (RelativeLayout)v;//私のPGではRelativeLayoutである
            Button btn = (Button)rl.findViewById(R.id.Button_List_Favorite);
            btn.setOnClickListener(FavoriteView.this);
            return v;
        }
    }
    //以上が、SimpleExpandableListAdapterの拡張クラスでした。

    public 自分のメソッド(){
        
        lv = (ExpandableListView)****.findViewById(R.id.ListFavorite);
       
        ArrayList<Map<String, String>> parentList = new ArrayList<Map<String, String>>();
        ArrayList<String> als = Group用文字列データ群を取得する。
        for(String s : als){
            Map<String, String> m = new HashMap<String, String>();
            m.put("Parent_Text", s);
            parentList.add(m);
        }
        
        ChildrenList = Child用文字列データ群を取得する。
        
        sa = new MySA(
            ****,

            //Group用文字列データ群の代入
            parentList,//データそのもの
            android.R.layout.simple_expandable_list_item_1,
            new String[]{"Parent_Text"},//入力
            new int[]{android.R.id.text1},//出力

            //Child用文字列データ群の代入
            ChildrenList,//データそのもの
            R.layout.list_favorite,//layout用xmlファイル
            sFrom,//入力
            iTo//出力
        );
        
        lv.setAdapter(sa);
        lv.setOnChildClickListener(this);
    }
    
    @Override
    public void onClick(View v) {
        if(v.getId()==R.id.Button_List_Favorite){
        //サンプルなので、ログを表示するだけです。
        //任意のChild内に表示されたボタンが押されたことを取得します。
            Button btn = (Button)v;
            long l = (Long)btn.getTag();
            int iG = ExpandableListView.getPackedPositionGroup(l);
            int iC = ExpandableListView.getPackedPositionChild(l);
if(BuildConfig.DEBUG) Log.d(TAG, "onClick(). " + iG + ". " + iC);
            return;
        }
    }

    @Override
    public boolean onChildClick(ExpandableListView arg0, View v, int iG, int iC, long arg4) {
        //サンプルなので、ログを表示するだけです。
        //任意のChildが押されたことを取得します。
if(BuildConfig.DEBUG) Log.d(TAG, "onChildClick(). " + iG + ". " + iC);
        return false;
    }


以下は、ChildViewのlayout用xmlファイルであり、ファイル名はR.layout.list_favorite.xmlです。
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
    <Button
        android:id="@+id/Button_List_Favorite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:drawableLeft="@android:drawable/ic_menu_edit"
        android:focusable="false"
        >
    </Button>
    <ImageView
        android:id="@+id/Image_List_Favorite"
        android:contentDescription="@string/app_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:focusable="false"
        >
    </ImageView>
    <TextView
        android:id="@+id/Text_List_Favorite_Title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:layout_toRightOf="@id/Image_List_Favorite"
        android:layout_toLeftOf="@id/Button_List_Favorite"
        android:layout_alignBottom="@id/Image_List_Favorite"
        android:focusable="false"
        >
    </TextView>
</RelativeLayout>

2013年7月17日水曜日

実行時にSeekBarの色を変える

実行時にSeekBarの色を変える

XMLにおいて、色を変える方法については、次のサイトで紹介されています。
AndroidのProgressBar / SeekBarに画像を使う
Cool android SeekBar, custom made by me

これらを参考にさせて頂いて、下記のとおり、実行時に色を変えるプログラムを作ってみました。

        sb = (SeekBar)llMain.findViewById(R.id.SeekBar_Volume);
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB){
        /*
        * LightではSeekBarが見えにくいので、色を着ける。
        * HONEYCOMB未満でこれを実行すると、即時の描画が行われない。
        * ClipDrawableは、描き変えや、読み出しを行えないため、新規に作成する。
        */
       LayerDrawable ld = (LayerDrawable)sb.getProgressDrawable();
       ColorDrawable dBlue = new ColorDrawable(0xff4f4fdf);
       ClipDrawable cd = new ClipDrawable(dBlue, 0x03, ClipDrawable.HORIZONTAL);
    ld.setDrawableByLayerId(android.R.id.progress, cd);
        }

Android3.0以上から、ホロというテーマが使われています。私はテーマをLightに変えました。すると、何故か、SeekBarに色が付かなくなりました。そこで、色を着けてやろうということです。

本来なら、テーマにおいて上記と同等の修正をすべきでしょう。テーマの技法を使って色を変える方法が分かる人は、各自のブログにでも載せておきましょう。

2013年7月5日金曜日

onNavigationItemSelected()

ActionBar.OnNavigationListener#onNavigationItemSelected()

第ゼロ番目のアイテムが(利用者の操作によって選択されたのではなく)自動的に選択され、このメソッドが呼ばれる。
ActionBar#setSelectedNavigationItem()の引数にマイナス1を代入しましたが、第ゼロ番目が選択されてしまいます。引数に、任意の項目のIDを代入すると、その代入した値の項目が選択された状態になります。
この不都合を無くすため、第ゼロ番目の項目をダミーにしました。
これでいいのか?(ここで笑う)

Spinnerを使う場合、初期化(初回)の時点で、アイテムが自動的に選択されてしまうという問題があります。
この問題の解決は次のサイトに書かれています。
Android Spinner : Avoid onItemSelected calls during initialization
spinner1に連動して変化するspinner2の初期値設定

boolean型変数を大域変数として設けておき、onItemSelected()において、初期化(初回)時点とそうでない時点を切り分けるという方法ですね。
他に方法は無いものかと考えましたが、分かりませんでした。

ちなみに、onItemSelected()は、選択が変更された場合に呼ばれます。逆に言えば、既に選択済みの項目をクリックしても呼ばれません。
つまり、上記方策を講じて、初回における自動選択問題を回避したとして、利用者の要求が自動選択済みの項目を選択したいということである場合には、その項目をクリックしても、onItemSelected()は呼ばれないということです。一旦別の項目を選択してから、要求する項目を再度クリックし直さねばならないのです。
こんなアホなアプリを作成することはできません。

この不都合を無くすため、第ゼロ番目の項目をダミーにします。
これでいいのか?(ここで再度笑う)

私は(不幸なことに)ArrayAdapter.NO_SELECTIONというのを見つけてしまいました。で、(何かを示唆するかのように)Adapterには、何の解説もありません。(無駄な)期待をして、これをActionBar#setSelectedNavigationItem()の引数に与えて実行してみました。結果、(予期したとおり)第ゼロ番目の項目が選択されたという趣旨でonNavigationItemSelected()が呼ばれました。
こんなんでいいのか?(ここで、もう一度笑う)

第ゼロ番目の項目をダミーにした場合、その必要ではない項目もドロップダウンリストに表示されてしまって不細工である。このため、ダミーをリストに表示しないようにしたいと思い、次のとおりアダプターをカスタマイズしてみました。

    public class Adapter_SR extends ArrayAdapter<String>{
        public Adapter_SR(Context context, int textViewResourceId, List<String> objects) {
            super(context, textViewResourceId, objects);
        }

        @Override
        public View getView(int iID, View v, ViewGroup vg) {
            if(iID==0){
                LayoutInflater inf = getLayoutInflater();
                TextView tv = (TextView)v;
                tv = (TextView)inf.inflate(R.layout.bar_sr, vg, false);
                tv.setText((String)getItem(iID));
                return tv;
            }
            else return super.getView(iID, v, vg);
        }

        @Override
        public View getDropDownView(int iID, View v, ViewGroup vg) {
            LayoutInflater inf = getLayoutInflater();
            TextView tv = (TextView)v;
            if(tv==null){
                if(iID==0){
                    tv = (TextView)inf.inflate(R.layout.bar_sr, vg, false);
                    tv.setTag("Zero");
                }
                else{
                    tv = (TextView)inf.inflate(android.R.layout.simple_spinner_dropdown_item, vg, false);
                    tv.setText((String)getItem(iID));
                }
            }
            else{
                if(iID==0){
                    tv = (TextView)inf.inflate(R.layout.bar_sr, vg, false);
                    tv.setTag("Zero");
                }
                else{
                    String s = (String)tv.getTag();
                    if(s!=null && s.equals("Zero")==true){
                        tv = (TextView)inf.inflate(android.R.layout.simple_spinner_dropdown_item, vg, false);
                        tv.setText((String)getItem(iID));
                    }
                }
            }
            return tv;
        }
    }


2013年7月1日月曜日

ClassNotFoundException

RuntimeException: Unable to instantiate activity ComponentInfo  java.lang.ClassNotFoundException in loader dalvik.system.PathClassLoader

ActionBar.OnNavigationListener(Added in API level 11, HONEYCOMB, Android 3.0)の実装について、これはinterfaceであるからして、Activityにinterfaceとしてimplementsしていました。
Android4.2で稼働させるには問題はありませんでした。
ところが、このアプリをAndroid2.3で実行させると、onCreate()が実行されることなく、上記エラーが発生します。

この実装をanonymousにしたところ、Android2.3でも稼働することができました。
こんなふうなanonymousの使い方ってあるのですね。

(参考)
Action Bar