/* * Copyright (C) 2011 Virginia Tech Department of Computer Science * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package sofia.widget; 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.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; //------------------------------------------------------------------------- /** * An adapter that uses <em>decoration through annotations</em> to display * elements in a list view or spinner. This class is used internally by widgets * like {@link ListView} and {@link Spinner}, so most users won't need to use * this class directly unless they want to add Sofia-like decoration features * to other widgets that aren't yet supported. * * @param <E> the type of items managed by the adapter * * @author Tony Allevato */ public class DecoratingAdapter<E> extends BaseAdapter implements Filterable { //~ Fields ................................................................ private List<E> list; private LayoutInflater inflater; private int defaultViewResId; private DecoratingFilter filter; private Object lock = new Object(); private ArrayList<E> originalList; //~ Constructors .......................................................... // ---------------------------------------------------------- public DecoratingAdapter(Context context, int defaultViewResId, List<E> list) { this.list = list; this.inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE); this.defaultViewResId = defaultViewResId; } //~ Public methods ........................................................ // ---------------------------------------------------------- public int getCount() { return list.size(); } // ---------------------------------------------------------- public E getItem(int position) { return list.get(position); } // ---------------------------------------------------------- public long getItemId(int position) { return position; } // ---------------------------------------------------------- public View getView(int position, View convertView, ViewGroup parent) { // TODO Support other means of rendering the list contents. View view; E item = getItem(position); String title = decorate(item, ProvidesTitle.class, String.class); if (title == null) { title = item.toString(); } String subtitle = decorate(item, ProvidesSubtitle.class, String.class); int resource = defaultViewResId; if (resource == 0) { resource = (subtitle != null) ? android.R.layout.simple_list_item_2 : android.R.layout.simple_list_item_1; } if (convertView == null) { view = inflater.inflate(resource, parent, false); } else { view = convertView; } TextView textView = (TextView) view.findViewById(android.R.id.text1); textView.setText(title); if (subtitle != null) { textView = (TextView) view.findViewById(android.R.id.text2); if (textView != null) { textView.setText(subtitle); } } return view; } // ---------------------------------------------------------- @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { // TODO Support other means of rendering the list contents. View view; E item = getItem(position); int resource = android.R.layout.simple_spinner_dropdown_item; if (convertView == null) { view = inflater.inflate(resource, parent, false); } else { view = convertView; } String title = decorate(item, ProvidesTitle.class, String.class); if (title == null) { title = item.toString(); } TextView textView = (TextView) view.findViewById(android.R.id.text1); textView.setText(title); return view; } // ---------------------------------------------------------- @Override public Filter getFilter() { if (filter == null) { filter = new DecoratingFilter(); } return filter; } //~ Private methods ....................................................... // ---------------------------------------------------------- @SuppressWarnings("unchecked") private static <ResultType> ResultType decorate( Object object, Class<? extends Annotation> annotation, Class<? extends ResultType> resultType) { ResultType result = null; Method method = getAnnotatedMethod(object.getClass(), annotation); if (method != null) { try { Object r = method.invoke(object); if (r == null || resultType.isAssignableFrom(r.getClass())) { result = (ResultType) r; } } catch (InvocationTargetException e) { throw new RuntimeException(e.getCause()); } catch (Exception e) { throw new RuntimeException(e); } } return result; } // ---------------------------------------------------------- /** * TODO Replace with reflection API. */ private static Method getAnnotatedMethod( Class<?> itemClass, Class<? extends Annotation> annotation) { Method method = null; for (Method currentMethod : itemClass.getMethods()) { if (currentMethod.getAnnotation(annotation) != null) { method = currentMethod; break; } } return method; } // ---------------------------------------------------------- // FIXME This should be made general somehow, so that users can plug in // different filters. Maybe a context callback? private class DecoratingFilter extends Filter { // ---------------------------------------------------------- @Override protected FilterResults performFiltering(CharSequence prefix) { FilterResults results = new FilterResults(); if (originalList == null) { synchronized (lock) { originalList = new ArrayList<E>(list); } } if (prefix == null || prefix.length() == 0) { ArrayList<E> list; synchronized (lock) { list = new ArrayList<E>(originalList); } results.values = list; results.count = list.size(); } else { String prefixString = prefix.toString().toLowerCase(); ArrayList<E> values; synchronized (lock) { values = new ArrayList<E>(originalList); } int count = values.size(); ArrayList<E> newValues = new ArrayList<E>(); for (int i = 0; i < count; i++) { E value = values.get(i); String valueText = value.toString().toLowerCase(); // First match against the whole, non-splitted value if (valueText.startsWith(prefixString)) { newValues.add(value); } else { String[] words = valueText.split(" "); int wordCount = words.length; // Start at index 0, in case valueText starts with space(s) for (int k = 0; k < wordCount; k++) { if (words[k].startsWith(prefixString)) { newValues.add(value); break; } } } } results.values = newValues; results.count = newValues.size(); } return results; } // ------------------------------------------------------ @Override @SuppressWarnings("unchecked") protected void publishResults( CharSequence constraint, FilterResults results) { list = (List<E>) results.values; if (results.count > 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } } }