satohu20xx's diary

思ったことをつらつらと

android-support-v4を使ってスワイプする

スワイプ動作をやるために必要なこと

サンプル的なコード

android-support-v4.jarを取ってくる

以下の場所にあるからコピーして場所を移してimportするなりそのままでimportするなりしましょう

android-sdk-windows\extras\android\compatibility\v4

ViewPagerを含んだLayoutファイルを作成
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

  <android.support.v4.view.ViewPager
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:id="@+id/viewpager"/>

</LinearLayout>
FragmentActivityを作成
public class TopFragmentActivity extends FragmentActivity {

    private ViewPager mViewPager;
    private TopViewPagerAdapter mPagerAdapter;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment);

        mPagerAdapter = new TopViewPagerAdapter(getSupportFragmentManager());
        mViewPager = (ViewPager) findViewById(R.id.viewpager);
        mViewPager.setAdapter(mPagerAdapter);
    }
}
ViewPagerAdapterを作成
public class TopViewPagerAdapter extends FragmentPagerAdapter {

    // こいつがページ数
    private static final int PAGE_NUM = 2;

    public TopViewPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        Fragment fragment = null;

        /*
         * このpositionに表示するViewの番号が来る
         * 初期表示は0で右にスワイプするごとにインクリメントみたいな
         * 毎回newせずにコンストラクタでnewして渡すほうがいいはず
         */
        fragment = new TopFragment();

        return fragment;
    }

    /*
     * こいつの返却数がページ数になる
     */
    @Override
    public int getCount() {
        return PAGE_NUM;
    }
}
Fragmentを作成する
public class TopFragment extends Fragment {

    private Button button;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View v = inflater.inflate(R.layout.play_list_select, container, false);
        Activity act = getActivity();

        // こんな感じでLayoutファイルのViewを取ってくる
        button= (Button)v.findViewById(R.id.button);

        /*
         * あとは好きに書いてくださいなー
         */

        return v;
    }
}

あとがき

こんな感じでスワイプ動作ができるようになるはず。
やっぱりスマホならスワイプ動作が使いやすいよね。

Spinnerの見た目をカスタマイズ

大体の書き方がわかったのでメモ程度に

やり方の基本はListViewと同じ。getView()でinflateして書き換えてあげれば大丈夫。
ただ、getDropDownView()の方も設定して上げる必要がある。

  • getView():こいつがクリックする前の描画に使われる
  • getDropDownView():こいつがクリックしたあとの描画時に使われる。

画面的に言うと、こんな感じ

f:id:satohu20xx:20120129152445p:plain

サンプルを書くまでもないけど、↓みたいに書くことになる。

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

        if (convertView == null) {
            // このhogehogeの部分を書き換える
            convertView = mInflater.inflate(R.layout.hogehoge, null);

            /*
             * ViewHolderに設定してなんちらかんちら
             */
        } else {
            holder = (ViewHolder)convertView.getTag();
        }

        /*
         * あとは好きに設定してねー
         */

        return convertView;
    }

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

        if (convertView == null) {
            // このhogehogeの部分を書き換える
            convertView = mInflater.inflate(R.layout.hogehoge, null);

            /*
             * ViewHolderに設定してなんちらかんちら
             */
        } else {
            holder = (ViewHolder)convertView.getTag();
        }

        /*
         * あとは好きに設定してねー
         */

        return convertView;
    }

getDropDownView()がわからなくてすげー手こずったけど、あとは好き勝手に出来るはず。
xmlとか使ってstyle指定してやったほうが動作は速いのかも。試してないけど。。。

画像ダウンロードでAsncTaskを立ち上げまくるのはやめましょう

ListViewでよくあるサンプルで画像をダウンロードするたびにAsyncTaskをたちあげてる奴があるけどそれはやめましょう

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

        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.search_row, null);
            holder = new ViewHolder();
            holder.Title = (TextView)convertView.findViewById(R.id.textTitle);
            holder.Thumbnail = (ImageView)convertView.findViewById(R.id.imageThumbnail);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder)convertView.getTag();
        }

        item = getItem(position);
        if(item != null){
            holder.Title.setText(item.getTitle());
            holder.Thumbnail.setTag(item.getThumbnail());
            new DownLoadAsyncTask(holder.Thumbnail).execute(item.getThumbnail());
        }

        return convertView;
    }

こんなソースを書くとgetView()がよばれるたびにAsyncTaskが立ちがあることになっちゃう。
ListViewでスクロールするとそのぶんだけgetView()が呼ばれることになるから、めっちゃくっちゃAsyncTaskが立ち上がることにって端末の動きがめちゃくちゃ重くなるんだよね。

だから、Workerスレッド立ち上げて上手いこと書きましょう。

具体的には↓みたいな感じ。

/**
 * 画像ダウンローダークラス
 * @author satohu20xx
 */
public class LoadBitmapManager {

    private static final int THREAD_MAX_NUM = 3;

    private static BlockingQueue<LoadBitmapItem> downloadQueue;
    private static Handler handler;

    /**
     * 始めて使われるときに初期化される。
     */
    static {
        /*
         * 画像情報を貯めるためのキュー
         */
        downloadQueue = new LinkedBlockingQueue<LoadBitmapItem>();

        /*
         * スレッド最大数まで画像ダウンロードスレッドを作成
         */
        for(int i=0 ; i<THREAD_MAX_NUM ; i++) {
            new Thread(new DownloadWorker()).start();
        }

        /*
         * 画像ダウンロード後にメッセージを受信するハンドラーを作成
         */
        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                /*
                 * 取得したメッセージから画像情報を取得
                 */
                LoadBitmapItem item = (LoadBitmapItem)msg.obj;

                /*
                 * 画像ダウンロードがうまくいっていた場合はイメージビューに設定
                 */
                if(item.getImgView().getTag() == item.getUrl() && item.getBitmap() != null) {
                    item.getImgView().setImageBitmap(item.getBitmap());
                }
            }
        };
    }

    /**
     * 引数として渡されたurlで画像をダウンロードしてImageViewに対して
     * 画像を設定する。
     * @param imgView
     * @param url
     */
    public static void doDownloadBitmap(ImageView imgView, String url) {
        /*
         * ダウンロードキューに入れる
         */
        LoadBitmapItem item = new LoadBitmapItem();
        item.setImgView(imgView);
        item.setUrl(url);
        downloadQueue.offer(item);

        return;
    }

    /**
     * 実際に画像をダウンロードするワーカー
     * @author satohu20xx
     */
    private static class DownloadWorker implements Runnable {

        @Override
        public void run() {

            /*
             * 画像ダウンロードスレッドは常に動き続けるから無限ループ
             */
            for(;;) {
                Bitmap bitmap;
                LoadBitmapItem item;

                try {
                    /*
                     * キューに値が入ったら呼び出される
                     * nullの状態ではwaitしている
                     */
                    item = downloadQueue.take();
                } catch (Exception ex){
                    Log.e("ERROR", "", ex);
                    continue;
                }

                /*
                 * ダウンロード
                 */
                try{
                    BufferedInputStream in = new BufferedInputStream(
                                    (InputStream) (new URL(item.getUrl())).getContent());
                    bitmap = BitmapFactory.decodeStream(in);
                    in.close();
                } catch (Exception ex){
                    Log.e("ERROR", "", ex);
                }
                item.setBitmap(bitmap);

                /*
                 * 取得した画像情報でメッセージを作って投げる
                 */
                Message msg = new Message();
                msg.obj = item;
                handler.sendMessage(msg);
            }
        }
    }
}

エラー処理とかは端折ってるけど、こんな感じで書けば立ち上がるスレッド数の上限が決まるから端末に負担がかからない。
タスクを立ち上げれば立ち上げるほど早くなると思ってるかもしれないけど、CPUのパワーには上限があるからそんなことはなくて、うまーくうごく閾値を探してうまく動かしてあげないと動きが重すぎてイライラ感が募るだけかと。

なにか突っ込みがあればお待ちしております。

SDカードへとバックアップ

画像ダウンロードしてバックアップするときとかに使いましょう。

サンプルプログラムは以下。

// SDカードが使えるかを判定
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    // SDカードの中でFileインスタンスを作成
    File file =  new File(getContext().getExternalFilesDir(null), URLEncoder.encode(url));

    // Fileが存在するかを判定
    if (!file.exists()) {
        // 存在しない場合はURLからデータをダウンロード
        // ダウンロードがうまくいったらSDカードに吐き出す
        // ダウンロードは適当に実装してね!
        try{
            FileOutputStream out = new FileOutputStream(file);
            bitmap.compress (Bitmap.CompressFormat.PNG, 100, out);
            out.close();
        } catch (Exception ex){
            
        }
    } else {
        // SDカードにあったらそのまま使う
        bitmap = BitmapFactory.decodeFile(file.getPath());
    }
} else {
    // SDカードが使えなかったら全てダウンロード
    // ダウンロードは適当に実装してね!
}

重要なのはEnvironmentクラス。
こいつでSDカード関連を操作する。

// これでSDカードが使えるかを調べる
Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
// これでSDカードのアプリ独自パスを取ってくる
// /mnt/sdcard/Android/data/AplicationName ってパスになる
Context.getExternalFilesDir(null)

この二つが重要。ってかこれだけ分かってれば使えるんじゃないかな。getExternalFilesDir()で取得できるパスはアプリ独自パスだからアンインストールするとフォルダごとAndroidが削除してくれる。だからサムネイル程度の小さなファイル程度出れば気にせずにがんがん保存してもいいんじゃないかと思う。でかいファイルの場合は時々消してあげる処理を入れてあげる必要があるんだろうね。

SDカードのご利用は計画的に!

ConvertViewの再利用の解釈

よくAdapterを使ったときにConvertViewの再利用がどーだこーだ書いてあるけど、解釈したことのメモ

ListViewを例にとって話をする。

    class ListAdapter extends ArrayAdapter<String>{
        public ListAdapter(Context context, String[] str) {
            super(context, 0, str);
        }

        public View getView(final int position, View convertView, ViewGroup parent) {

            if(convertView == null) {
                convertView = new TextView(getContext());
                ((TextView)convertView).setTextSize(32.f);
                convertView.setTag(position);
            }

            ((TextView)convertView).setText(getItem(position));

            Log.d("DEBUG", String.valueOf(position));
            Log.d("DEBUG", convertView.getTag().toString());

            return convertView;
        }
    }

ざっくりと検証用のコードを上みたいにかいてみたとして、動かすと以下の画面が出てくる。

f:id:satohu20xx:20120122214206p:plain

この画面が表示されるにはgetView()が11回呼ばれることになる。画面上に少しでもViewが出ていると判定されるとgetView()が呼ばれるみたい。んで、こいつをスクロールすると、12番目のオブジェクトが表示されることになる。1画面には11個のオブジェクトしか表示できる領域がないから12番目が表示されると0番目に表示されていたオブジェクトが見えなくなる。この見えなくなったーっていう判定をAndroidが勝手にやってくれるのがAdapterみたい。見えなくなったViewはメモリの無駄だから、この0番目のViewを使って12番目のViewを作るっていう仕組みがよく書いてある再利用という話。

Viewが再利用されると引数のconvertViewに見えなくなったView(今の例だと0番目のView)が設定されてgetViewが呼び出されることになる。convertViewがnullかどうかを判定してnullじゃなかったらわざわざnewすることなく使いまわしてやりましてやることでメモリが節約できて速度も上がって万々歳なんだとさ。

んで、よく画像を別のスレッドでダウンロードしてダウンロードし終わったらImageViewに設定してってことをやるんだけど、この再利用で0番目だったconvertViewがダウンロードしてる間にスクロールされて12番目のViewになってるとかがありえるわけさ。だからsetTagでなんらかの判別情報をいれといて再利用してるかどうかを判定してやることで別のイメージがダウンロードされても設定されないようにできるってことみたい。詳しい話は以下のURLを参考に

Android で ListView に非同期で取ってきた画像を表示したら位置がおかしい件 - slumbers

Android良く出来てる。

ブログをはじめる

とりあえず、ブログをかけるスペースだけ作ってみた。
これからちゃんと書くかはわかりませぬが、まったーりかけることを書いていこうと思います。

はてな記法が使えるって書いてあったのでそれのテストもかねて、良くあるチェーンの進め方。pointerをnextに進めてnullになるまでまわすっていう処理だけど↓みたいにwhileで書いてる人をよくみる。

// pointerの初期化
ptr = hogehoge;

while(ptr!=null) {
    // ptrを使う処理

    // ptrを次に進める
    ptr = ptr->next;
}

僕は↓みたいにforを使って書くほうが好み

for(ptr = hogehoge ; ptr != null ; ptr = ptr->next) {
    // ptrを使う処理
}

だからなんだって話だけど、テストだから気にしない