スポンサーリンク

AndroidアプリでListViewをカスタマイズし,Web上の画像を行ごとに表示するサンプルコード (SimpleAdapterクラスを独自に拡張)


Androidアプリで,リストビュー内の各行に,

Web上から動的にロードした画像をアイコン風に表示したい。


そのサンプルコード。

ファイル構成

必要なクラス:

  1. アクティビティ: リストビューにアダプタをセットする。
  2. アダプタ: リスト内の1行分の描画処理を定義する。また,画像の取得タスクを実行する。
  3. 画像取得の非同期タスク: Web上から画像をダウンロードし,リスト内に描画する。


必要なレイアウト定義ファイル:

  1. メイン: 親画面。リストビューが一つあるだけ。
  2. リストビュー内の1行分のレイアウト: テキストと画像。

サンプルコード


再利用しやすいよう,わかりやすくシンプルにコーディングしてある。


アクティビティ側のコード:

package com.example;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import com.example.R;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;

/**
 * 画像付きリストビューを表示する画面
 *
 */
public class CodeTestActivity extends Activity {

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

        // リストビューを取得
        ListView lv = (ListView)findViewById(R.id.listview1);


        // リスト中の行データを定義(適当な情報を3行分)
        ArrayList<Map<String, Object>> list_data
            = new ArrayList<Map<String, Object>>();

        HashMap<String, Object> map1 = new HashMap<String, Object>();
        map1.put("hoge", "fuga1");
        list_data.add(map1);

        HashMap<String, Object> map2 = new HashMap<String, Object>();
        map2.put("hoge", "fuga2");
        list_data.add(map2);

        HashMap<String, Object> map3 = new HashMap<String, Object>();
        map3.put("hoge", "fuga3");
        list_data.add(map3);


        // アダプタを生成
        String[] from_template = {};
        int[] to_template = {};
        ImageListAdapter il_adapter = new ImageListAdapter(
            this,
            list_data,
            R.layout.listview_one_line,
            from_template,
            to_template
        );


        // リストビューにアダプタをセット
        lv.setAdapter( il_adapter );
    }
}


リストビューの独自アダプタ:

package com.example;

import java.util.List;
import java.util.Map;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SimpleAdapter;
import android.widget.TextView;

/**
 * 画像付きリストのアダプタ
 *
 */
public class ImageListAdapter extends SimpleAdapter {

    private Context context;
    private LayoutInflater mInflater;
    private List<? extends Map<String, Object>> list_data;

    // 初期化
    public ImageListAdapter(Context context, 
            List<? extends Map<String, Object>> list_data,
            int resource, String[] from, int[] to)
    {
        super(context, list_data, resource, from, to);

        this.context = context;
        this.list_data = list_data;

        // リストの動的な描画のためにインフレータを生成
        this.mInflater =
            (LayoutInflater) context.getSystemService(
                Context.LAYOUT_INFLATER_SERVICE
            );

        Log.d("ListViewTest", "アダプタ生成完了");
    }


    // 1行を描画するたびに呼ばれるメソッド
    @Override
    public View getView(int position, View convertView,
         ViewGroup parent) {

        Log.d("ListViewTest", position + "の getView() が開始");


        /* ---------- 行を初期化 ------------ */

        // 行を表すビュー
        View v = convertView;
        if(v == null){
            Log.d("ListViewTest", position + "のvを新規生成");
            v = mInflater.inflate(R.layout.listview_one_line, null);
        }

        // この行のためのデータを読み出し
        Map<String, Object> data_for_this_line = list_data.get(position);

        // この行のためのテキストをセット
        String text_for_this_line
           = data_for_this_line.get("hoge").toString();
        Log.d("ListViewTest", position + "のtextは" + text_for_this_line);
        TextView tv = (TextView)v.findViewById(R.id.textView1);
        tv.setText( text_for_this_line );


        /* ---------- 行内の画像をロードして描画 ------------ */

        // 行内の画像ビュー
        ImageView imageView = (ImageView)v.findViewById(R.id.ImageView1);

        // 画像のURL
        String img_url = "http://k.yimg.jp/images/top/sp/logo.gif";
            // ※Yahooのロゴ

        // 非同期で画像読込を実行
        try{
            Log.d("ListViewTest", position + "の画像読み込みを開始");

            DownloadImageTask task
                = new DownloadImageTask(imageView, context);
            task.execute(img_url);
        }
        catch(Exception e){
            //
            Log.d("ListViewTest", position + "の画像読み込みに失敗");
        }

        /* ---------- 行の描画が完了 ------------ */

        return v;
    }

}


画像をロードする非同期タスク:

package com.example;

import java.io.InputStream;
import java.net.URL;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;

/**
 * Web上から画像を読み込むタスク
 *
 */
class DownloadImageTask extends AsyncTask<String,Void,Bitmap> {
    private ImageView imageView;
    private Context context;

    // 初期化
    public DownloadImageTask(ImageView imageView, Context context) {
        this.imageView = imageView;
        this.context = context;
    }

    // execute時のタスク本体。画像をビットマップとして読み込んで返す
    @Override
    protected Bitmap doInBackground(String... params) {
        synchronized (context){
            try {
            	String str_url = params[0];
                URL imageUrl = new URL(str_url);
                InputStream imageIs;

                // 読み込み実行
                imageIs = imageUrl.openStream();
                Bitmap bm = BitmapFactory.decodeStream(imageIs);
                Log.d("ListViewTest", "画像読み込み完了");

                return bm;
            } catch (Exception e) {
                Log.d("ListViewTest", "画像読み込みタスクで例外発生:" 
                    + e.toString());
                return null;
            }
        }
    }

    // タスク完了時
    @Override
    protected void onPostExecute(Bitmap result) {
        if(result != null){
            Log.d("ListViewTest", "ビューに画像をセット");
            imageView.setImageBitmap(result);
        }
    }
}


main.xml(メインのレイアウト):

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


    <!-- リスト -->
    <ListView
        android:id="@+id/listview1"
        android:layout_width="fill_parent"
        android:layout_height="200dp"
        android:layout_marginBottom="30dp"
    />


</LinearLayout>


listview_one_line.xml(リスト一行分のレイアウト):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linearLayout1"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:gravity="center_horizontal"
    android:orientation="horizontal"
>

    <!-- とりあえずHelloWorldを表示 -->
    <TextView
        android:id="@+id/textView1"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="@string/hello"
    ></TextView>


    <!-- Web上から読み込む画像。初期状態ではアプリのアイコンを表示 -->
    <ImageView
      android:id="@+id/ImageView1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@drawable/icon"
    ></ImageView>

</LinearLayout>


マニフェスト中でインターネット接続を許可するのを忘れずに。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.example"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="3" />

    <application android:icon="@drawable/icon" 
        android:label="@string/app_name">
        <activity android:name=".CodeTestActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category 
                    android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

    <uses-permission android:name="android.permission.INTERNET" />
</manifest>

実行

実行すると,リストビュー内に行が3つ表示される。

上から順に,fuga1, fuga2, fuga3とテキストが表示されている。

それらのテキストの隣には,アプリのアイコンが表示されている。


少し時間をおいて,上の行から順番にポコポコッと画像がロードされ,

アイコン画像がYahooのロゴ画像に置き換えられてゆく。


改善の方針:

これだと,getView() が呼ばれる度に毎回,全ての画像が再読み込みされる。

試しに,端末のタテ・ヨコを切り替えてみればわかる。

Yahooのロゴが3行ともロードし直される。

これでは,パフォーマンスが悪化する。


この問題を回避するためには,画像のキャッシュクラスを導入するとよい。

次項の参考資料を参照。


参考資料:

Context.LAYOUT_INFLATER_SERVICE とは何か:

マニュアル:
Use with getSystemService(String) to retrieve a LayoutInflater for inflating layout resources in this context.
(レイアウトの動的な描画のためにinflaterを生成するのに使う)


ListViewのgetView()メソッドとは何か:

Adapter#getViewの挙動について(図解でわかりやすい)
http://d.hatena.ne.jp/hyoromo/2009091...

  • 一行分スクロールした場合、先頭のListが画面外に行き、6行目のListが画面内に表示されます。このListが新たに表示されるタイミングで ArrayAdapter#getView メソッドが CALL されます


Android:Adapter.getViewでAsyncTaskは危険
http://319ring.net/blog/archives/1707

  • getViewは頻繁に呼ばれるメソッドです。リストをちょっとスクロールしても呼ばれます
  • スクロールしたらさらに呼ばれ続けて、スレッド数が多過ぎるというエラーでアプリが落ちる。画面に表示される行数の数が多いので、スマートフォンでは大丈夫だったけどタブレットだと落ちるという現象に出くわす可能性が高い
  • 既にキューに取得したい画像のURLが送られているときはAsyncTaskをコールしないようにすればOK


画像付きリストの具体的な実装方法とサンプルコード:

Android画像付きリストの設定(ListView)
http://lablog.lanche.jp/archives/220

  • リストビューに項目毎に画像を置くケース。メモリ使用量を抑える為、スクロールさせて実際に必要になったときだけ表示する
  • 独自のAdapterクラスを定義し,getView()メソッド内に一行分の描画処理を記述。
  • 一行ごとにgetView()内でAsyncTaskを生成し,画像を取得。取得済みの画像はキャッシュする。
  • メモリリーク対策として,画面破棄時にキャッシュをクリアするのを忘れずに。

なお,リソースファイルから静的に画像を持ってくる場合のサンプルコードは,

「Android SDK逆引きハンドブック」の

Section-058「リストビューにアイコンを表示する」の項目に掲載されている。

下記URLも参照。

[android] アイコン付きのリストを作ってみる
http://xfutures.jp/2010/02/14/197/

  • アイコン付きのリストを作るために、ArrayAdapterを継承してオリジナルのアダプタを作る


ListViewをカスタマイズする
http://labs.techfirm.co.jp/android/ch...

  • ListActivityを継承し、表示したいデータとビューをマッピングするためにListAdapterを使う