2015年3月10日火曜日

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

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

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

 ListViewの作り

ListViewに文字を表示させるには
表示させたいデータをAdapterクラスに入れて、Apapterに設定してあるレイアウトを基にViewを組立て、ListViewに表示させる
既存のAdapterでリストをつくるとこうなる
// Viewの生成に必要
Context context = this.getApplicationContext();

// Adapterの設定 Adapterのコンストラクタ(Context,LayoutのID,リストID, リストデータ)
String[] list = { "りんご", "ゴリラ", "ラッパ", "パイナップル" };
ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, android.R.layout.simple_expandable_list_item_1, list);

// AdapterをListViewにセット
ListView listView = new ListView(context);
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で表示する
こんな感じで設定

<?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="wrap_content"
    android:orientation="horizontal" >

    <TextView
        android:id="@+id/week_view"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:layout_gravity="center"
        android:layout_margin="10dip"
        android:textColor="#000000"
        android:textSize="50sp" />

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dip"
            android:background="#0FFFF0"
            android:orientation="horizontal" >

            <TextView
                android:id="@+id/time_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="#000000"
                android:textSize="20sp" />

            <TextView
                android:id="@+id/tv_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="5dip"
                android:textColor="#000000"
                android:textSize="20sp" />
        </LinearLayout>

        <TextView
            android:id="@+id/title_view"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center_vertical"
            android:singleLine="true"
            android:ellipsize="end"
            android:textColor="#000000"
            android:textSize="30sp" />
    </LinearLayout>

</LinearLayout>
アニメタイトルのTextViewが問題
TextViewは列をはみ出そうとすると自動で改行を入れてくれる。しかし、ListViewでこれをやられると各セルの高さがばらばらになる
個人的にセルの高さは統一したいので、TextViewは一行で表示、はみだした分を…となるように設定する
色つけてる箇所がポイントで、
singLineを"true"で一行表記設定
ellipsize="end"で…の位置を設定
ちなみにellipsize属性に設定できる他のフラグはこんな感じ
フラグ効果
end文末が…アニメタイ…
start文頭が……メタイトル
middle文の途中が…アニメ…トル
none…をしないアニメタイト

 Adapter作り

上記コードでは既存のAdapterを使っているが、今回は自作したレイアウトにデータを詰め込む実装がいるのでBaseAdapterを継承してオリジナルAdapterを作っていく
データクラスとしてAnime.javaを作っとく。
とりあえずAnimeAPIから得られる情報一式をメンバ変数にする
class Anime {
    public String title, url, time, station, state, next, episode, cable, today, week;
}
BaseAdapterは抽象クラスで実装しないといけないクラスがいくつかある
この辺はだいたいテンプレ。特殊なことしないならこれでOK
/**
 * アニメ表示用Adapter.
 *
 */
public class AnimeAdapter extends BaseAdapter {
    private static final int LAYOUT_ID = R.layout.list_aitem; // 上記で定義したレイアウトのID
    private Anime[] list;     // データリスト
    private Context mContext; // レイアウトIDからLayoutインスタンス取得に必要
    
    /** コンストラクタ. */
    public AnimeAdapter(final Context context, Anime[] animes) {
        this.mContext = context;
        this.list = animes;
    }
    
    @Override
    /** リストの数を取得 */
    public int getCount() {
        return list.length;
    }
    
    @Override
    /** リストから特定のAnimeを取得.戻り値Anime型にしとくのがミソ */
    public Anime getItem(int position) {
        return list[position];
    }
    
    @Override
    /** リストのID取得(まぁリストの順番でOK) */
    public long getItemId(int position) {
        return position;
    }

    @Override
    /** レイアウトにデータを設定する(一番したい処理) */
    public View getView(int position, View convertView, ViewGroup parent) {
        //TODO 色々テクニックいるので下で書く
        return null;
    }
}
次に一番やりたかった処理。レイアウトにデータを詰めて表示する処理
  1. セル用レイアウトにコンバート
  2. セル用レイアウトにデータを突っ込む処理
    ListViewは生成されたとき全てのセルを作ってるわけではない。セルのViewは画面に出たときに生成されてる
    そして、スクロールして新しいセルが表示されたとき、このメソッド(# getView())は呼ばれViewを生成する
    引数は
    ・position表示すべきリストのポジション
    ・convertViewListViewで表示するセル用レイアウトのインスタンス
    ・parentよくわからん(今回使わんので無視)
    最初convertViewはnullになっていて、実装上で設定する必要がある
    そこで必要なのがレイアウトのインスタンスを取得するLayoutInflaterクラス。システムからLayoutInflaterのインスタンスを呼び出し、# inflateメソッドでレイアウトを指定してレイアウトのViewのインスタンスを取得する
    LayoutInflater inflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    convertView = inflater.inflate(R.layout.list_aitem, null);
    
    そのレイアウトから各Viewのインスタンスを取得するには#findViewByIdメソッドをつかう
    XMLで定義したIDを指定してViewのインスタンスを得て設定する
    TextView textView = (TextView) convertView.findViewById(R.id.title_view);
    
    以上を行い、レイアウトを設定して戻り値に修正後のレイアウトを指定すればOK
    @Override
    /** レイアウトにデータを設定する(一番したい処理) */
    public View getView(int position, View convertView, ViewGroup parent) {
                if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) this.mContext
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.list_aitem, null);
        }
        Anime anime = getItem(position);
        ((TextView) convertView.findViewById(R.id.title_view)).setText(anime.title);
        ((TextView) convertView.findViewById(R.id.time_view)).setText(anime.time);
        ((TextView) convertView.findViewById(R.id.week_view)).setText(anime.week.substring(0, 1));
        ((TextView) convertView.findViewById(R.id.tv_view)).setText(anime.station);
        return convertView;
    }
    
  3. 高速処理
  4. 上記の方法でも処理できるが、毎回findViewById()をするとViewの呼び出し時間がかかってしまう難点がある
    それを回避するためにViewHolderという考え方を使う。レイアウトを使いまわす手法
    セル内のそれぞれのViewの参照値あらかじめ持たせておき、そこのインスタンスにデータを突っ込む
    @Override
    /** レイアウトにデータを設定する(一番したい処理) */
    public View getView(int position, View convertView, ViewGroup parent) {
        View v = convertView; // レイアウト
        ViewHolder holder;    // ホルダー
        if (v == null) {
            LayoutInflater inflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = inflater.inflate(R.layout.list_aitem, null);
            
            // ホルダーを設定する
            holder = new ViewHolder();
            holder.titleView = (TextView)v.findViewById(R.id.title_view);
            holder.timeView = (TextView)v.findViewById(R.id.time_view);
            holder.weekView = (TextView)v.findViewById(R.id.week_view);
            holder.tvView = (TextView)v.findViewById(R.id.tv_view);
            v.setTag(holder);
        }else{
         // レイアウトからViewフォルダーを取得
            holder = (ViewHolder)v.getTag();
        }
        Anime anime = getItem(position);
        // Viewフォルダに情報詰め込む
        holder.titleView.setText(anime.title);
        holder.timeView.setText(anime.time);
        holder.weekView.setText(anime.week.substring(0,1));
        holder.weekView.setBackgroundColor(color.get(anime.week));
        holder.tvView.setText(anime.station);
        
        return v;
    }
    // ホルダークラス
    class ViewHolder {
        TextView titleView, timeView, weekView, tvView;
    }
    
    これでOK
完成!

0 件のコメント:

コメントを投稿