package org.azavea.otm.adapters; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class LinkedHashMapAdapter<T> extends BaseAdapter implements Filterable { public enum FilterType { WORD_PREFIX, ANYWHERE } private LinkedHashMap<CharSequence, List<T>> originalData = null; private List<Entry<T>> flattenedData; private LayoutInflater inflator; private int separatorRowLayoutId; private int separatorRowTextViewId; private int elementRowLayoutId; private int elementRowTextViewId; private Filter filter = null; private FilterType filterType = FilterType.WORD_PREFIX; private static final int ITEM_VIEW_TYPE_ELEMENT = 0; private static final int ITEM_VIEW_TYPE_SEPARATOR = 1; public LinkedHashMapAdapter(Context context, LinkedHashMap<CharSequence, List<T>> data) { this(context, data, 0, 0, 0, 0); } public LinkedHashMapAdapter(Context context, LinkedHashMap<CharSequence, List<T>> data, int separatorRowLayoutId, int separatorRowTextViewId, int elementRowLayoutId, int elementRowTextViewId) { this.inflator = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.originalData = data; this.flattenedData = getFlattenedList(data); this.separatorRowLayoutId = separatorRowLayoutId; this.separatorRowTextViewId = separatorRowTextViewId; this.elementRowLayoutId = elementRowLayoutId; this.elementRowTextViewId = elementRowTextViewId; } @Override public boolean areAllItemsEnabled() { return false; } @Override public int getViewTypeCount() { return 2; } @Override public int getCount() { return flattenedData.size(); } @Override public Entry<T> getItem(int position) { return flattenedData.get(position); } @Override public int getItemViewType(int position) { return getItem(position).value == null ? ITEM_VIEW_TYPE_SEPARATOR : ITEM_VIEW_TYPE_ELEMENT; } @Override public long getItemId(int position) { return position; } @Override public boolean isEnabled(int position) { return getItemViewType(position) != ITEM_VIEW_TYPE_SEPARATOR; } public View getElementView(int position, View convertView, ViewGroup parent) { CharSequence text = getItem(position).value.toString(); return createViewFromResource(convertView, parent, elementRowLayoutId, elementRowTextViewId, text); } public View getSeparatorView(int position, View convertView, ViewGroup parent) { CharSequence text = getItem(position).key; return createViewFromResource(convertView, parent, separatorRowLayoutId, separatorRowTextViewId, text); } protected View createViewFromResource(View convertView, ViewGroup parent, int layoutId, int textViewId, CharSequence text) { ViewHolder holder; if (convertView == null) { convertView = inflator.inflate(layoutId, parent, false); holder = new ViewHolder(); holder.textView = (TextView) convertView.findViewById(textViewId); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } // populate subviews for the instance in scope holder.textView.setText(text); return convertView; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (getItemViewType(position) == ITEM_VIEW_TYPE_SEPARATOR) { convertView = getSeparatorView(position, convertView, parent); } else { convertView = getElementView(position, convertView, parent); } return convertView; } public void setFilterType(FilterType filterType) { this.filterType = filterType; } /** * It is easier to work with a flattened list when building individual rows, but because the * data can be changed at any time from filtering, we need to reflatten the map on occasion * * @return a list of key and value pairs, with null values where each section header should go */ private List<Entry<T>> getFlattenedList(LinkedHashMap<CharSequence, List<T>> data) { List<Entry<T>> list = new ArrayList<>(); for (Map.Entry<CharSequence, List<T>> items : data.entrySet()) { if (!items.getValue().isEmpty()) { // null here signifies a separator row. list.add(new Entry<>(items.getKey(), null)); for (T item : items.getValue()) { list.add(new Entry<>(items.getKey(), item)); } } } return list; } @Override public Filter getFilter() { if (filter == null) { filter = new LinkedHashMapFilter(); } return filter; } /** * The below was adapted from the ArrayFilter inner class of ArrayAdapter, which is * unfortunately private. */ private class LinkedHashMapFilter extends Filter { @Override protected FilterResults performFiltering(CharSequence sequence) { FilterResults results; LinkedHashMap<CharSequence, List<T>> newData; synchronized (this) { newData = new LinkedHashMap<>(originalData.size()); for (CharSequence key : originalData.keySet()) { // Make a copy of each list newData.put(key, new ArrayList<>(originalData.get(key))); } } if (sequence == null || sequence.length() == 0) { results = getFilterResults(newData); } else { String substring = sequence.toString().toLowerCase(); for (Map.Entry<CharSequence, List<T>> entry : newData.entrySet()) { final List<T> values = entry.getValue(); final ArrayList<T> newValues = new ArrayList<>(); for (T value : values) { final String valueText = value.toString().toLowerCase(); // First match against the whole, non-splitted value if (valueText.startsWith(substring)) { newValues.add(value); } else if (filterType == FilterType.WORD_PREFIX) { final String[] words = valueText.split(" "); // Start at index 0, in case valueText starts with space(s) for (String word : words) { if (word.startsWith(substring)) { newValues.add(value); break; } } } else if (filterType == FilterType.ANYWHERE && valueText.contains(substring)) { newValues.add(value); } } newData.put(entry.getKey(), newValues); } results = getFilterResults(newData); } return results; } private FilterResults getFilterResults(LinkedHashMap<CharSequence, List<T>> newData) { FilterResults results; results = new FilterResults(); List<Entry<T>> flatList = getFlattenedList(newData); results.values = flatList; results.count = flatList.size(); return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { //noinspection unchecked flattenedData = (List<Entry<T>>) results.values; if (results.count > 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } } public static class Entry<T> { public final CharSequence key; public final T value; public Entry(CharSequence key, T value) { this.key = key; this.value = value; } } private static class ViewHolder { TextView textView; } }