2015年3月10日火曜日

ListViewをカスタマイズする方法

アニメ番組表 API使った通信Androidアプリを作ってみた」でアニメのタイトル一覧をListViewで表示した。
でもタイトルだけ表示しても面白くないので、ListViewをカスタマイズする

ListViewを制する者は、AndroidのView表示を制す!
というだけあっていろいろなテクニックが必要だった

 ListViewの作り

ListViewに文字を表示させるには
表示させたいデータをAdapterクラスに入れて、Apapterに設定してあるレイアウトを基にViewを組立て、ListViewに表示させる
既存のAdapterでリストをつくるとこうなる
  1. // Viewの生成に必要
  2. Context context = this.getApplicationContext();
  3.  
  4. // Adapterの設定 Adapterのコンストラクタ(Context,LayoutのID,リストID, リストデータ)
  5. String[] list = { "りんご", "ゴリラ", "ラッパ", "パイナップル" };
  6. ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, android.R.layout.simple_expandable_list_item_1, list);
  7.  
  8. // AdapterをListViewにセット
  9. ListView listView = new ListView(context);
  10. listView.setAdapter(adapter);
「android.R.layout.simple_expandable_list_item_1」は既存のセル用レイアウトでTextViewが1個あるだけ
ArrayAdapterのコードを見てみると、レイアウト内のTextViewに文字列を入れる仕組みになってた
オリジナルのListViewを作るにはAdapterのレイアウトを組み立てる仕組みを作ってやればいい

 Layoutの定義

表示するセルのレイアウトを作ってく
プロジェクトフォルダ/res/layout/


定義は右や上のフォルダの位置にxmlファイルつくって描く
ソースコード上でも描けるけど、この方が見やすいからねw
名前は何でもいいので、「list_aitem.xml」としておく。この名前はレイアウトのIDとして後々使用するので適当すぎると困る
今回は曜日、開始時間、放送局、タイトルをTextViewで表示する
こんな感じで設定

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="fill_parent"
  4. android:layout_height="wrap_content"
  5. android:orientation="horizontal" >
  6.  
  7. <TextView
  8. android:id="@+id/week_view"
  9. android:layout_width="wrap_content"
  10. android:layout_height="fill_parent"
  11. android:layout_gravity="center"
  12. android:layout_margin="10dip"
  13. android:textColor="#000000"
  14. android:textSize="50sp" />
  15.  
  16. <LinearLayout
  17. android:layout_width="fill_parent"
  18. android:layout_height="wrap_content"
  19. android:orientation="vertical" >
  20.  
  21. <LinearLayout
  22. android:layout_width="fill_parent"
  23. android:layout_height="wrap_content"
  24. android:layout_margin="10dip"
  25. android:background="#0FFFF0"
  26. android:orientation="horizontal" >
  27.  
  28. <TextView
  29. android:id="@+id/time_view"
  30. android:layout_width="wrap_content"
  31. android:layout_height="wrap_content"
  32. android:textColor="#000000"
  33. android:textSize="20sp" />
  34.  
  35. <TextView
  36. android:id="@+id/tv_view"
  37. android:layout_width="wrap_content"
  38. android:layout_height="wrap_content"
  39. android:layout_marginLeft="5dip"
  40. android:textColor="#000000"
  41. android:textSize="20sp" />
  42. </LinearLayout>
  43.  
  44. <TextView
  45. android:id="@+id/title_view"
  46. android:layout_width="wrap_content"
  47. android:layout_height="match_parent"
  48. android:layout_gravity="center_vertical"
  49. android:singleLine="true"
  50. android:ellipsize="end"
  51. android:textColor="#000000"
  52. android:textSize="30sp" />
  53. </LinearLayout>
  54.  
  55. </LinearLayout>
アニメタイトルのTextViewが問題
TextViewは列をはみ出そうとすると自動で改行を入れてくれる。しかし、ListViewでこれをやられると各セルの高さがばらばらになる
個人的にセルの高さは統一したいので、TextViewは一行で表示、はみだした分を…となるように設定する
色つけてる箇所がポイントで、
singLineを"true"で一行表記設定
ellipsize="end"で…の位置を設定
ちなみにellipsize属性に設定できる他のフラグはこんな感じ
フラグ効果
end文末が…アニメタイ…
start文頭が……メタイトル
middle文の途中が…アニメ…トル
none…をしないアニメタイト

 Adapter作り

上記コードでは既存のAdapterを使っているが、今回は自作したレイアウトにデータを詰め込む実装がいるのでBaseAdapterを継承してオリジナルAdapterを作っていく
データクラスとしてAnime.javaを作っとく。
とりあえずAnimeAPIから得られる情報一式をメンバ変数にする
  1. class Anime {
  2. public String title, url, time, station, state, next, episode, cable, today, week;
  3. }
BaseAdapterは抽象クラスで実装しないといけないクラスがいくつかある
この辺はだいたいテンプレ。特殊なことしないならこれでOK
  1. /**
  2. * アニメ表示用Adapter.
  3. *
  4. */
  5. public class AnimeAdapter extends BaseAdapter {
  6. private static final int LAYOUT_ID = R.layout.list_aitem; // 上記で定義したレイアウトのID
  7. private Anime[] list; // データリスト
  8. private Context mContext; // レイアウトIDからLayoutインスタンス取得に必要
  9. /** コンストラクタ. */
  10. public AnimeAdapter(final Context context, Anime[] animes) {
  11. this.mContext = context;
  12. this.list = animes;
  13. }
  14. @Override
  15. /** リストの数を取得 */
  16. public int getCount() {
  17. return list.length;
  18. }
  19. @Override
  20. /** リストから特定のAnimeを取得.戻り値Anime型にしとくのがミソ */
  21. public Anime getItem(int position) {
  22. return list[position];
  23. }
  24. @Override
  25. /** リストのID取得(まぁリストの順番でOK) */
  26. public long getItemId(int position) {
  27. return position;
  28. }
  29.  
  30. @Override
  31. /** レイアウトにデータを設定する(一番したい処理) */
  32. public View getView(int position, View convertView, ViewGroup parent) {
  33. //TODO 色々テクニックいるので下で書く
  34. return null;
  35. }
  36. }
次に一番やりたかった処理。レイアウトにデータを詰めて表示する処理
  1. セル用レイアウトにコンバート
  2. セル用レイアウトにデータを突っ込む処理
    ListViewは生成されたとき全てのセルを作ってるわけではない。セルのViewは画面に出たときに生成されてる
    そして、スクロールして新しいセルが表示されたとき、このメソッド(# getView())は呼ばれViewを生成する
    引数は
    ・position表示すべきリストのポジション
    ・convertViewListViewで表示するセル用レイアウトのインスタンス
    ・parentよくわからん(今回使わんので無視)
    最初convertViewはnullになっていて、実装上で設定する必要がある
    そこで必要なのがレイアウトのインスタンスを取得するLayoutInflaterクラス。システムからLayoutInflaterのインスタンスを呼び出し、# inflateメソッドでレイアウトを指定してレイアウトのViewのインスタンスを取得する
    1. LayoutInflater inflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    2. convertView = inflater.inflate(R.layout.list_aitem, null);
    そのレイアウトから各Viewのインスタンスを取得するには#findViewByIdメソッドをつかう
    XMLで定義したIDを指定してViewのインスタンスを得て設定する
    1. TextView textView = (TextView) convertView.findViewById(R.id.title_view);
    以上を行い、レイアウトを設定して戻り値に修正後のレイアウトを指定すればOK
    1. @Override
    2. /** レイアウトにデータを設定する(一番したい処理) */
    3. public View getView(int position, View convertView, ViewGroup parent) {
    4. if (convertView == null) {
    5. LayoutInflater inflater = (LayoutInflater) this.mContext
    6. .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    7. convertView = inflater.inflate(R.layout.list_aitem, null);
    8. }
    9. Anime anime = getItem(position);
    10. ((TextView) convertView.findViewById(R.id.title_view)).setText(anime.title);
    11. ((TextView) convertView.findViewById(R.id.time_view)).setText(anime.time);
    12. ((TextView) convertView.findViewById(R.id.week_view)).setText(anime.week.substring(0, 1));
    13. ((TextView) convertView.findViewById(R.id.tv_view)).setText(anime.station);
    14. return convertView;
    15. }
  3. 高速処理
  4. 上記の方法でも処理できるが、毎回findViewById()をするとViewの呼び出し時間がかかってしまう難点がある
    それを回避するためにViewHolderという考え方を使う。レイアウトを使いまわす手法
    セル内のそれぞれのViewの参照値あらかじめ持たせておき、そこのインスタンスにデータを突っ込む
    1. @Override
    2. /** レイアウトにデータを設定する(一番したい処理) */
    3. public View getView(int position, View convertView, ViewGroup parent) {
    4. View v = convertView; // レイアウト
    5. ViewHolder holder; // ホルダー
    6. if (v == null) {
    7. LayoutInflater inflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    8. v = inflater.inflate(R.layout.list_aitem, null);
    9. // ホルダーを設定する
    10. holder = new ViewHolder();
    11. holder.titleView = (TextView)v.findViewById(R.id.title_view);
    12. holder.timeView = (TextView)v.findViewById(R.id.time_view);
    13. holder.weekView = (TextView)v.findViewById(R.id.week_view);
    14. holder.tvView = (TextView)v.findViewById(R.id.tv_view);
    15. v.setTag(holder);
    16. }else{
    17. // レイアウトからViewフォルダーを取得
    18. holder = (ViewHolder)v.getTag();
    19. }
    20. Anime anime = getItem(position);
    21. // Viewフォルダに情報詰め込む
    22. holder.titleView.setText(anime.title);
    23. holder.timeView.setText(anime.time);
    24. holder.weekView.setText(anime.week.substring(0,1));
    25. holder.weekView.setBackgroundColor(color.get(anime.week));
    26. holder.tvView.setText(anime.station);
    27. return v;
    28. }
    29. // ホルダークラス
    30. class ViewHolder {
    31. TextView titleView, timeView, weekView, tvView;
    32. }
    これでOK
完成!

0 件のコメント:

コメントを投稿