StickyListHeaders を使っての雑感
概要
コンセプトととしてすばらしく、リストの区切りごとに見出しをつけることができる。
使いやすい点として、Adapterへの実装がメインで上位に対して影響を与えないということがとてもすばらしく、多くのアプリで採用されているみたい。
そこで、よくやることとはまったことなどを記載。
ListFragmentへの実装
とりあえず以下の実装
stickylistheaderslist.xml
<?xml version="1.0" encoding="utf-8"?> <com.emilsjolander.components.stickylistheaders.StickyListHeadersListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" />
IssueView.java
onCreateViewとAdapterにExampleViewStickyListHeadersAdapterを使うこと以外は通常のListFragmentと同じ。
public class IssueView extends ListFragment { private ExampleViewStickyListHeadersAdapter adapter; public IssueView(){ super(); } @Override public void onDestroyView() { setListAdapter(null); super.onDestroyView(); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); //getListView().addFooterView(mFooter); adapter = new ExampleViewStickyListHeadersAdapter(); setListAdapter(adapter); getListView().setFastScrollEnabled(true); onRefresh(true); } protected void onRefresh(boolean isFetchRemote){ //adapter.setupParameter(intent.getConnectionId(),issue_id); adapter.notifyDataSetInvalidated(); adapter.notifyDataSetChanged(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { //mFooter = inflater.inflate(R.layout.listview_footer,null); //mFooter.setVisibility(View.GONE); return inflater.inflate(R.layout.stickylistheaderslist, container, false); } }
ExampleViewStickyListHeadersAdapter.java
BaseAdapterとほぼほぼ同じ。
具体的な違いとしてはgetHeaderViewでヘッダービューを作っているところとgetHeaderIdでIDを返していること。
getHeaderIdで戻るIDでViewを作るか判断しているような気がする。(必要確認)
public class ExampleViewStickyListHeadersAdapterextends BaseAdapter implements StickyListHeadersAdapter { @Override public View getHeaderView(int position, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) parent.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(R.layout.issuestickyheader, null); } if (convertView != null){ //必ず実装 TextView text = (TextView) convertView.findViewById(R.id.textTitle); text.setText("Header"); } return convertView; } @Override public long getHeaderId(int pos) { //必ず実装 //posはgetItemと同じ値 } @Override public int getCount() { //必ず実装 } @Override public int getItemViewType(int pos) { //必要に応じて実装 } @Override public Object getItem(int pos) { //必ず実装 } @Override public long getItemId(int pos) { return pos; //必要に応じて実装 } @Override public View getView(int pos, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) parent.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(android.R.layout.simple_list_item_1, null); } //普通に実装 return convertView; } @Override public View getDropDownView(int pos, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) parent.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(android.R.layout.simple_spinner_dropdown_item, null); } //普通に実装 return convertView; } }
いいね!マージドにしようよ!
複数のAdapterを束ねればすばらしいビューができるのではないか。
ExampleViewStickyListHeadersAdapter.java
注意点としてはgetItemViewTypeで子アダプターのビュータイプIDを返すので、子アダプターには実装しておく必要がある。
くわしくは、getItemViewTypeの説明を参照。
public class ExampleViewStickyListHeadersAdapterextends BaseAdapter implements StickyListHeadersAdapter { private final List<AggrigateAdapter> mapAdapters = new ArrayList<AggrigateAdapter>(); protected class AggrigateAdapter { public final BaseAdapter adapter; public int count; public int head; public final int res; public AggrigateAdapter(BaseAdapter a, int r){ adapter = a; res = r; //タイトルをつけたいので、StringのリソースIDを設定 } public int getInnerPos(int pos){ return pos - head; } public Object getItem(int pos){ return adapter == null ? null : adapter.getItem(getInnerPos(pos)); } public long getItemId(int pos){ return adapter == null ? 0 : adapter.getItemId(getInnerPos(pos)); } public View getView(int pos, View convertView, ViewGroup parent) { return adapter == null ? null : adapter.getView(getInnerPos(pos), convertView, parent); } public View getDropDownView(int pos, View convertView, ViewGroup parent) { return adapter == null ? null : adapter.getDropDownView(getInnerPos(pos), convertView, parent); } public int getItemViewType(int pos) { return adapter == null ? 0 : adapter.getItemViewType(getInnerPos(pos)); } } public ExampleViewStickyListHeadersAdapterextends (){ //ここで、adapterなどを追加する mapAdapters.add(new AggrigateAdapter(adapterIssue, R.string.ticket_detail)); mapAdapters.add(new AggrigateAdapter(adapterJournal, R.string.ticket_journals)); } @Override public void notifyDataSetChanged() { int current = 0; for(AggrigateAdapter adapter : mapAdapters){ adapter.adapter.notifyDataSetChanged(); adapter.head = current; adapter.count = adapter.adapter.getCount(); current += adapter.count; } super.notifyDataSetChanged(); } protected AggrigateAdapter getInner(int pos){ AggrigateAdapter lastadapter = null; for(AggrigateAdapter item : mapAdapters){ if(item.head <= pos){ lastadapter = item; } } //returns default return lastadapter; } @Override public void notifyDataSetInvalidated() { if(mapAdapters != null){ for(AggrigateAdapter adapter : mapAdapters){ adapter.adapter.notifyDataSetInvalidated(); } } super.notifyDataSetInvalidated(); } @Override public View getHeaderView(int position, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) parent.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(R.layout.issuestickyheader, null); } if (convertView != null){ //必ず実装 TextView text = (TextView) convertView.findViewById(R.id.textTitle); text.setText("Header"); } return convertView; } @Override public View getHeaderView(int position, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) parent.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(R.layout.issuestickyheader, null); } if (convertView != null){ TextView text = (TextView) convertView.findViewById(R.id.textTitle); AggrigateAdapter adapter = getInner(position); text.setText(adapter == null ? "" : convertView.getContext().getString(getInner(position).res)); } return convertView; } @Override public long getHeaderId(int pos) { AggrigateAdapter adapter = getInner(pos); return adapter == null ? 0 : adapter.res; } @Override public int getCount() { AggrigateAdapter lastadapter = null; for(AggrigateAdapter item : mapAdapters){ lastadapter = item; } return lastadapter == null ? 0 : lastadapter.head + lastadapter.count; } @Override public int getItemViewType(int pos) { //ここを実装しておくと、子アダプタのgetView内部で自分のViewなのかどうか //不要になるので実装することをお勧めする。 //getItemViewTypeで返すのはlayoutのIDでよい AggrigateAdapter adapter = getInner(pos); return adapter == null ? android.R.layout.simple_list_item_1 : adapter.getItemViewType(pos); } //getViewTypeCountは実装しない方針。 //Header部分も含めて、全部で何種類なのかを把握しないと、ArrayIndexOutOfBoundsException: length=5; index=2130903051とかあっさり起こる @Override public Object getItem(int pos) { return getInner(pos).getItem(pos); } @Override public long getItemId(int pos) { return pos; //必要に応じて実装 } @Override public View getView(int pos, View convertView, ViewGroup parent) { AggrigateAdapter adapter = getInner(pos); convertView = adapter == null ? null : adapter.getView(pos, convertView, parent); if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) parent.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(android.R.layout.simple_list_item_1, null); } //普通に実装 return convertView; } @Override public View getDropDownView(int pos, View convertView, ViewGroup parent) { AggrigateAdapter adapter = getInner(pos); convertView = adapter == null ? null : adapter.getDropDownView(pos, convertView, parent); if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) parent.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(android.R.layout.simple_spinner_dropdown_item, null); } //普通に実装 return convertView; } }
参考: 安易にgetViewTypeCountを実装した場合
FATAL EXCEPTION: main java.lang.ArrayIndexOutOfBoundsException: length=5; index=2130903051 at android.widget.AbsListView$RecycleBin.addScrapView(AbsListView.java:6171) at android.widget.ListView.layoutChildren(ListView.java:1800) at android.widget.AbsListView.onLayout(AbsListView.java:1939) at com.emilsjolander.components.stickylistheaders.StickyListHeadersListView.onLayout(StickyListHeadersListView.java:142) at android.view.View.layout(View.java:11359) at android.view.ViewGroup.layout(ViewGroup.java:4571) at android.widget.FrameLayout.onLayout(FrameLayout.java:431) at android.view.View.layout(View.java:11359) at android.view.ViewGroup.layout(ViewGroup.java:4571) (中略) at android.view.View.layout(View.java:11359) at android.view.ViewGroup.layout(ViewGroup.java:4571) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1673) at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2711) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:156) at android.app.ActivityThread.main(ActivityThread.java:5099) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:991) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:758) at dalvik.system.NativeStart.main(Native Method)
参考文献
- Y.A.M の 雑記帳: Android 「The world of ListView」 - Virtualization and adapters -
- 「異なるビュータイプを制御する」
- GitHub - emilsjolander/StickyListHeaders: An android library for section headers that stick to the top
ほかに何かあったはずだけど、思い出せないので、思い出したら追記する。