2 * Copyright (C) 2014 Cyanogen, Inc.
4 package com.cyngn.eleven.sectionadapter;
6 import android.app.Activity;
7 import android.content.Context;
8 import android.view.LayoutInflater;
9 import android.view.View;
10 import android.view.ViewGroup;
11 import android.widget.ArrayAdapter;
12 import android.widget.BaseAdapter;
13 import android.widget.TextView;
15 import com.cyngn.eleven.R;
16 import com.cyngn.eleven.ui.MusicHolder;
17 import com.cyngn.eleven.utils.SectionCreatorUtils.Section;
18 import com.cyngn.eleven.utils.SectionCreatorUtils.SectionType;
19 import com.cyngn.eleven.widgets.IPopupMenuCallback;
21 import java.util.TreeMap;
24 * This class wraps an ArrayAdapter that implements BasicAdapter and allows Sections to be inserted
25 * into the list. This wraps the methods for getting the view/indices and returns the section
26 * heads and if it is an underlying item it flows it through the underlying adapter
27 * @param <TItem> The underlying item that is in the array adapter
28 * @param <TArrayAdapter> the arrayadapter that contains TItem and implements BasicAdapter
30 public class SectionAdapter<TItem,
31 TArrayAdapter extends ArrayAdapter<TItem> & SectionAdapter.BasicAdapter & IPopupMenuCallback>
32 extends BaseAdapter implements IPopupMenuCallback, IPopupMenuCallback.IListener {
34 * Basic interface that the adapters implement
36 public interface BasicAdapter {
38 public void buildCache();
40 public int getItemPosition(long id);
44 * The underlying adapter to wrap
46 protected TArrayAdapter mUnderlyingAdapter;
49 * A map of external position to the Section type and Identifier
51 protected TreeMap<Integer, Section> mSections;
53 protected int mHeaderLayoutId;
54 protected boolean mHeaderEnabled;
56 protected int mFooterLayoutId;
57 protected boolean mFooterEnabled;
60 * Popup menu click listener
62 protected IListener mListener;
67 protected final Context mContext;
70 * Creates a SectionAdapter
71 * @param context The {@link Context} to use.
72 * @param underlyingAdapter the underlying adapter to wrap
74 public SectionAdapter(final Activity context, final TArrayAdapter underlyingAdapter) {
76 mUnderlyingAdapter = underlyingAdapter;
77 mUnderlyingAdapter.setPopupMenuClickedListener(this);
78 mSections = new TreeMap<Integer, Section>();
79 setupHeaderParameters(R.layout.list_header, false);
80 // since we have no good default footer, just re-use the header layout
81 setupFooterParameters(R.layout.list_header, false);
85 * Gets the underlying array adapter
86 * @return the underlying array adapter
88 public TArrayAdapter getUnderlyingAdapter() {
89 return mUnderlyingAdapter;
96 public View getView(final int position, View convertView, final ViewGroup parent) {
97 if (isSection(position)) {
98 if (convertView == null) {
99 int layoutId = mHeaderLayoutId;
100 if (isSectionFooter(position)) {
101 layoutId = mFooterLayoutId;
104 convertView = LayoutInflater.from(mContext).inflate(layoutId, parent, false);
107 TextView title = (TextView)convertView.findViewById(R.id.title);
108 title.setText(mSections.get(position).mIdentifier);
110 convertView = mUnderlyingAdapter.getView(
111 getInternalPosition(position), convertView, parent);
113 Object tag = convertView.getTag();
114 if (tag instanceof MusicHolder) {
115 MusicHolder holder = (MusicHolder)tag;
116 View divider = holder.mDivider.get();
117 if (divider != null) {
118 // if it is the last item in the list, or it is an item before a section divider
119 // then hide the divider, otherwise show it
120 if (position == getCount() - 1 || isSection(position + 1)) {
121 divider.setVisibility(View.INVISIBLE);
123 divider.setVisibility(View.VISIBLE);
133 * Setup the header parameters
134 * @param layoutId the layout id used to inflate
135 * @param enabled whether clicking is enabled on the header
137 public void setupHeaderParameters(int layoutId, boolean enabled) {
138 mHeaderLayoutId = layoutId;
139 mHeaderEnabled = enabled;
143 * Setup the footer parameters
144 * @param layoutId the layout id used to inflate
145 * @param enabled whether clicking is enabled on the footer
147 public void setupFooterParameters(int layoutId, boolean enabled) {
148 mFooterLayoutId = layoutId;
149 mFooterEnabled = enabled;
156 public int getCount() {
157 return mSections.size() + mUnderlyingAdapter.getCount();
164 public Object getItem(int position) {
165 if (isSection(position)) {
166 return mSections.get(position);
169 return mUnderlyingAdapter.getItem(getInternalPosition(position));
173 * Gets the underlying adapter's item
174 * @param position position to query for
175 * @return the underlying item or null if a section header is queried
177 public TItem getTItem(int position) {
178 if (isSection(position)) {
182 return mUnderlyingAdapter.getItem(getInternalPosition(position));
189 public long getItemId(int position) {
197 public boolean hasStableIds() {
205 public int getItemViewType(int position) {
206 if (isSectionHeader(position)) {
207 // use the last view type id as the section header
208 return getViewTypeCount() - 1;
209 } else if (isSectionFooter(position)) {
210 // use the last view type id as the section header
211 return getViewTypeCount() - 2;
214 return mUnderlyingAdapter.getItemViewType(getInternalPosition(position));
221 public int getViewTypeCount() {
222 // increment view type count by 2 for section headers and section footers
223 return mUnderlyingAdapter.getViewTypeCount() + 2;
230 public void notifyDataSetChanged() {
231 super.notifyDataSetChanged();
233 mUnderlyingAdapter.notifyDataSetChanged();
240 public void notifyDataSetInvalidated() {
241 super.notifyDataSetInvalidated();
243 mUnderlyingAdapter.notifyDataSetInvalidated();
250 public boolean isEnabled(int position) {
251 if (isSectionHeader(position)) {
252 return mHeaderEnabled;
253 } else if (isSectionFooter(position)) {
254 return mFooterEnabled;
264 public boolean areAllItemsEnabled() {
269 * Determines whether the item at the position is a section header
270 * @param position position in the overall lis
271 * @return true if a section header
273 public boolean isSectionHeader(int position) {
274 return mSections.containsKey(position) && mSections.get(position).mType == SectionType.Header;
278 * Determines whether the item at the position is a section footer
279 * @param position position in the overall lis
280 * @return true if a section footer
282 public boolean isSectionFooter(int position) {
283 return mSections.containsKey(position) && mSections.get(position).mType == SectionType.Footer;
287 * Determines whether the item at the position is a section of some type
288 * @param position position in the overall lis
289 * @return true if the item is a section
291 public boolean isSection(int position) {
292 return mSections.containsKey(position);
296 * Converts the external position to the internal position. This is needed to determine
297 * the position to pass into the underlying adapter
298 * @param position external position
299 * @return the internal position
301 public int getInternalPosition(int position) {
302 if (isSection(position)) {
306 int countSectionHeaders = 0;
308 for (Integer sectionPosition : mSections.keySet()) {
309 if (sectionPosition <= position) {
310 countSectionHeaders++;
316 return position - countSectionHeaders;
320 * Converts the underlaying adapter position to wrapped adapter position
321 * @param internalPosition the position of the underlying adapter
322 * @return the position of the wrapped adapter
324 public int getExternalPosition(int internalPosition) {
325 int externalPosition = internalPosition;
326 for (Integer sectionPosition : mSections.keySet()) {
327 // because the section headers are tracking the 'merged' lists, we need to keep bumping
328 // our position for each found section header
329 if (sectionPosition <= externalPosition) {
336 return externalPosition;
340 * Sets the data on the adapter
341 * @param data data to set
343 public void setData(SectionListContainer<TItem> data) {
344 mUnderlyingAdapter.unload();
346 if (data.mSections == null) {
349 mSections = data.mSections;
352 mUnderlyingAdapter.addAll(data.mListResults);
354 mUnderlyingAdapter.buildCache();
356 notifyDataSetChanged();
360 * unloads the underlying adapter
362 public void unload() {
364 mUnderlyingAdapter.unload();
365 notifyDataSetChanged();
369 * flushes the underlying adapter
371 public void flush() {
372 mUnderlyingAdapter.flush();
373 notifyDataSetChanged();
376 public void clear() {
378 mUnderlyingAdapter.clear();
383 * Gets the item position for the given identifier
384 * @param identifier used to identify the object
385 * @return item position, or -1 if not found
387 public int getItemPosition(long identifier) {
388 int internalPosition = mUnderlyingAdapter.getItemPosition(identifier);
389 if (internalPosition >= 0) {
390 return getExternalPosition(internalPosition);
397 public void setPopupMenuClickedListener(IListener listener) {
398 mListener = listener;
402 public void onPopupMenuClicked(View v, int position) {
403 if (mListener != null) {
404 mListener.onPopupMenuClicked(v, getExternalPosition(position));