Androidでリスト形式のUIを使いたい場合、ListViewやGridViewが手ごろです。
しかし、データの表示には奇怪なAdapterクラスを使用しなければいけません。
個人的にAdapterはだいっっっっきらいですが、少しだけ打ち解けたのでサンプルコードを置いておきます。
機能
- ActivityでListViewとGridViewを使用してデータを表示する。
- Adapterの動きを理解するために、ダミーデータを表示する。
手順
- ActivityのレイアウトにListViewまたはGridViewを配置する(現在は非推奨なのでLegacyコンポーネント)
- BaseAdapterを継承し独自Adapterを定義する
- ActivityのonCreateでListViewまたはGridViewをfindViewByIdする
- setAdapterを行う
- 実行する
レイアウト(activity_main.xml)
今回はLinearLayoutで上半分にGridView、下半分にListViewを配置しました。
Legacyコンポーネントからマウスでぽちぽち配置した結果のコードです。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <GridView android:id="@+id/gridTest1" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:numColumns="4" /> <ListView android:id="@+id/listTest1" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
Adapterクラス(Activityの内部クラスとして)
アイテム項目としてのレイアウトは簡単にするためTextViewが一つだけです。そのため、レイアウトファイルは用いずコードからTextViewを生成します。
アイテム項目の生成要求となるgetViewではViewHolderパターンを使用します。
ViewHolderパターンとは何度もfindViewByを使用しないために、ViewのsetTagに必要なview参照を全て含めてしまおう、というテクニックのことらしいです。特殊な便利クラスを継承してるわけじゃなく、ただの構造体なので、名前は別にViewHolderじゃなくてもいいです。
他に、Adapterの動きを見やすくするため、サンプルコードでよくあるデータクラスを省いています。通常はAdapterのコンストラクタで配列またはリスト構造のデータを受け取り内部で保持するのが一般的です。
引数のpositionはindexの事です。0 <= position < getCount()
/** 独自アダプター、ダミーデータ同梱 */ private class MyTestAdapter extends BaseAdapter { /** ViewHolderパターン、View参照を保存するだけの構造体 */ private class ViewHolder{ TextView text; } /** データの総数を返す */ @Override public int getCount() { return 15; //ダミーで15件 } /** インデックスに紐づくデータを返す */ @Override public Object getItem(int position) { return null; //今回は使わないためnull } /** インデックスに紐づくItemId(任意値)を返す */ @Override public long getItemId(int position) { return position; //通常はAdapterに設定されたデータのid項目を返す } /** * ListViewやGridViewからのインデックスに対応するView構築要求<br/> * スクロールによってViewがフレームアウト<br/> ⇒ フレームインするViewとして再利用される<br/> * @param position インデックス * @param convertView 初回のみnull、再利用時は前回生成したViewが入っているためデータを書き換える * @param parent 親の参照、特に使わない * @return 常に有効なconvertViewを返す */ @Override public View getView(int position, View convertView, ViewGroup parent) { //以下のロジックは定形文 ViewHolder vh; //ViewHolderパターン、単純な構造体クラス if(convertView==null) { //初回のView構築要求 vh = new ViewHolder(); vh.text = new TextView(Main3Activity.this); //Viewを構築 convertView = vh.text; //ルートになるViewを設定 convertView.setTag(vh); //ViewHolderパターンによってView参照を保存、Tagにはなんでも入る }else{ //前回生成したViewがある vh = (ViewHolder) convertView.getTag(); //ViewHolderパターンによってView参照を一発で取得 } vh.text.setText(position + ":0123456789"); //ダミーデータ return convertView; //必ずconvertViewを返す } }
着目すべきはgetViewの振る舞いです。
getViewはListViewやGridViewからのアイテム生成要求になりますが、毎回Viewを生成するとコストがかさむため、再利用する仕組みになっています。
引数のconvertViewには、初回生成時はnullが入っていて、再利用時は過去に生成したいずれかのindexのViewが入っています。
そのため、
- 最初にconvertViewがnullかどうか判断し必要があればViewを生成し、
- その後indexに該当するデータを設定し、
- 最後にconvertViewを返すという流れになります。
これはおまじない級の定型文です。
ActivityのonCreate
極めてシンプルです。findViewByIdしてsetAdapterのみ。
ListViewやGridViewを使うのに必要なことは、Adapterの設計が全てといっても過言ではありません。
通常は表示したいデータの取得処理を行い、それをAdapterコンストラクタへ渡す処理を書きます。
面白いのはListViewとGridViewに同じAdapterを使うことができる点です。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //UI参照取得 GridView gridTest1 = findViewById(R.id.gridTest1); ListView listTest1 = findViewById(R.id.listTest1); //Adaterによるデータ設定 gridTest1.setAdapter(new MyTestAdapter()); listTest1.setAdapter(new MyTestAdapter()); }
実行結果
縦にスクロールが可能です。
Activityの完全なソース
public class MainActivity extends AppCompatActivity { /** 独自アダプター、ダミーデータ同梱 */ private class MyTestAdapter extends BaseAdapter { /** ViewHolderパターン、View参照を保存するだけの構造体 */ private class ViewHolder{ TextView text; } /** データの総数を返す */ @Override public int getCount() { return 15; //ダミーで15件 } /** インデックスに紐づくデータを返す */ @Override public Object getItem(int position) { return null; //今回は使わないためnull } /** インデックスに紐づくItemId(任意値)を返す */ @Override public long getItemId(int position) { return position; //通常はAdapterに設定されたデータのid項目を返す } /** * ListViewやGridViewからのインデックスに対応するView構築要求
* スクロールによってViewがフレームアウト
⇒ フレームインするViewとして再利用される
* @param position インデックス * @param convertView 初回のみnull、再利用時は前回生成したViewが入っているためデータを書き換える * @param parent 親の参照、特に使わない * @return 常に有効なconvertViewを返す */ @Override public View getView(int position, View convertView, ViewGroup parent) { //以下のロジックは定形文 ViewHolder vh; //ViewHolderパターン、単純な構造体クラス if(convertView==null) { //初回のView構築要求 vh = new ViewHolder(); vh.text = new TextView(Main3Activity.this); //Viewを構築 convertView = vh.text; //ルートになるViewを設定 convertView.setTag(vh); //ViewHolderパターンによってView参照を保存、Tagにはなんでも入る }else{ //前回生成したViewがある vh = (ViewHolder) convertView.getTag(); //ViewHolderパターンによってView参照を一発で取得 } vh.text.setText(position + ":0123456789"); //ダミーデータ return convertView; //必ずconvertViewを返す } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //UI参照取得 GridView gridTest1 = findViewById(R.id.gridTest1); ListView listTest1 = findViewById(R.id.listTest1); //Adaterによるデータ設定 gridTest1.setAdapter(new MyTestAdapter()); listTest1.setAdapter(new MyTestAdapter()); } }
ただのリストを表示するためにこんな複雑なコードが必要・・・
DataBindingなる技もあるみたいだけど、これ以上複雑にしたくないので使いたくないです。
クラスファイルやレイアウトファイルなど、どんどんファイルが増えていくのがとても気に入らない。
ListViewとGridViewの親密度は40です。
RecyclerViewの親密度はまだ0です。
いつか分かり合えるのかな?
追記:斜めスクロール実現しました。