/*
* Copyright (C) 2010 Cyril Mottier (http://www.cyrilmottier.com)
*
* 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 greendroid.widget;
import greendroid.widget.item.DescriptionItem;
import greendroid.widget.item.DrawableItem;
import greendroid.widget.item.Item;
import greendroid.widget.item.LongTextItem;
import greendroid.widget.item.ProgressItem;
import greendroid.widget.item.SeparatorItem;
import greendroid.widget.item.SubtextItem;
import greendroid.widget.item.SubtitleItem;
import greendroid.widget.item.TextItem;
import greendroid.widget.item.ThumbnailItem;
import greendroid.widget.itemview.ItemView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.util.Xml;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
/**
* <p>
* A {@link ListAdapter} that acts like an {@link ArrayAdapter}. It manages a
* ListView that is backed by an array of {@link Item}s. This is more advanced
* than a simple {@link ArrayAdapter} because it handles different types of
* cells internally. Adding, removing items from the internal array is also
* possible.
* </p>
* <p>
* The {@link ListView} can be notified manually using
* {@link notifyDataSetChanged} or automatically using the notifyOnChange flag.
* </p>
* <p>
* Finally, an ItemAdapter can be created via XML code using the createFromXml
* method. This is a very powerful feature when you want to display static data
* or if you want to prepopulate your ItemAdapter.
* </p>
*
* @author Cyril Mottier
*/
public class ItemAdapter extends BaseAdapter {
private static final int DEFAULT_MAX_VIEW_TYPE_COUNT = 10;
private static class TypeInfo {
int count;
int type;
}
private List<Item> mItems;
private HashMap<Class<? extends Item>, TypeInfo> mTypes;
private Context mContext;
private boolean mNotifyOnChange;
private int mMaxViewTypeCount;
/**
* Constructs an empty ItemAdapter.
*
* @param context The context associated with this array adapter.
*/
public ItemAdapter(Context context) {
this(context, new ArrayList<Item>());
}
/**
* Constructs an ItemAdapter using the specified items.
* <p>
* <em>Note</em> : Using this constructor implies the internal array will be
* immutable. As a result, adding or removing items will result in an
* exception.
* </p>
*
* @param context The context associated with this array adapter.
* @param items The array of Items use as underlying data for this
* ItemAdapter
*/
public ItemAdapter(Context context, Item[] items) {
this(context, Arrays.asList(items), DEFAULT_MAX_VIEW_TYPE_COUNT);
}
/**
* Constructs an ItemAdapter using the specified items.
* <p>
*
* @param context The context associated with this array adapter.
* @param items The list of Items used as data for this ItemAdapter
*/
public ItemAdapter(Context context, List<Item> items) {
this(context, items, DEFAULT_MAX_VIEW_TYPE_COUNT);
}
/**
* Constructs an ItemAdapter using the specified items.
* <p>
* <em>Note</em> : Using this constructor implies the internal array will be
* immutable. As a result, adding or removing items will result in an
* exception.
* </p>
* <p>
* <em><strong>Note:</strong> A ListAdapter doesn't handle variable view type
* count (even after a notifyDataSetChanged). An ItemAdapter handles several
* types of cell are therefore use a trick to overcome the previous problem.
* This trick is to fool the ListView several types exist. If you already
* know the number of item types you can have, simply set it using this method</em>
* </p>
*
* @param context The context associated with this array adapter.
* @param items The array of Items use as underlying data for this
* ItemAdapter
* @param maxViewTypeCount The maximum number of view type that may be
* generated by this ItemAdapter
*/
public ItemAdapter(Context context, Item[] items, int maxViewTypeCount) {
this(context, Arrays.asList(items), maxViewTypeCount);
}
/**
* Constructs an ItemAdapter using the specified items.
* <p>
* <em><strong>Note:</strong> A ListAdapter doesn't handle variable view type
* count (even after a notifyDataSetChanged). An ItemAdapter handles several
* types of cell are therefore use a trick to overcome the previous problem.
* This trick is to fool the ListView several types exist. If you already
* know the number of item types you can have, simply set it using this method</em>
* </p>
*
* @param context The context associated with this array adapter.
* @param items The list of Items used as data for this ItemAdapter
* @param maxViewTypeCount The maximum number of view type that may be
* generated by this ItemAdapter
*/
public ItemAdapter(Context context, List<Item> items, int maxViewTypeCount) {
mContext = context;
mItems = items;
mTypes = new HashMap<Class<? extends Item>, TypeInfo>();
mMaxViewTypeCount = Integer.MAX_VALUE;
for (Item item : mItems) {
addItem(item);
}
mMaxViewTypeCount = Math.max(1, Math.max(mTypes.size(), maxViewTypeCount));
}
private void addItem(Item item) {
final Class<? extends Item> klass = item.getClass();
TypeInfo info = mTypes.get(klass);
if (info == null) {
final int type = mTypes.size();
if (type >= mMaxViewTypeCount) {
throw new RuntimeException("This ItemAdapter may handle only " + mMaxViewTypeCount
+ " different view types.");
}
final TypeInfo newInfo = new TypeInfo();
newInfo.count = 1;
newInfo.type = type;
mTypes.put(klass, newInfo);
} else {
info.count++;
}
}
private void removeItem(Item item) {
final Class<? extends Item> klass = item.getClass();
TypeInfo info = mTypes.get(klass);
if (info != null) {
info.count--;
if (info.count == 0) {
// TODO cyril: Creating a pool to keep all TypeInfo instances
// could be a great idea in the future.
mTypes.remove(klass);
}
}
}
/**
* Returns the context associated with this array adapter. The context is
* used to create views from the resource passed to the constructor.
*
* @return The Context associated to this ItemAdapter
*/
public Context getContext() {
return mContext;
}
/**
* Returns the current number of different views types used in this
* ItemAdapter. Having a <em>getCurrentViewTypeCount</em> equal to
* <em>getViewTypeCount</em> means you won't be able to add a new type of
* view in this adapter (The Adapter class doesn't allow variable view type
* count).
*
* @return The current number of different view types
*/
public int getActualViewTypeCount() {
return mTypes.size();
}
/**
* Adds the specified object at the end of the array.
*
* @param object The object to add at the end of the array.
*/
public void add(Item item) {
mItems.add(item);
addItem(item);
if (mNotifyOnChange) {
notifyDataSetChanged();
}
}
/**
* Inserts the specified object at the specified index in the array.
*
* @param item The object to insert into the array.
* @param index The index at which the object must be inserted.
*/
public void insert(Item item, int index) {
mItems.add(index, item);
addItem(item);
if (mNotifyOnChange) {
notifyDataSetChanged();
}
}
/**
* Removes the specified object from the array.
*
* @param object The object to remove.
*/
public void remove(Item item) {
if (mItems.remove(item)) {
removeItem(item);
if (mNotifyOnChange) {
notifyDataSetChanged();
}
}
}
/**
* Remove all elements from the list.
*/
public void clear() {
mItems.clear();
mTypes.clear();
if (mNotifyOnChange) {
notifyDataSetChanged();
}
}
/**
* Sorts the content of this adapter using the specified comparator.
*
* @param comparator The comparator used to sort the objects contained in
* this adapter.
*/
public void sort(Comparator<? super Item> comparator) {
Collections.sort(mItems, comparator);
if (mNotifyOnChange) {
notifyDataSetChanged();
}
}
/**
* Control whether methods that change the list ({@link #add},
* {@link #insert}, {@link #remove}, {@link #clear}) automatically call
* {@link #notifyDataSetChanged}. If set to false, caller must manually call
* notifyDataSetChanged() to have the changes reflected in the attached
* view. The default is true, and calling notifyDataSetChanged() resets the
* flag to true.
*
* @param notifyOnChange if true, modifications to the list will
* automatically call {@link #notifyDataSetChanged}
*/
public void setNotifyOnChange(boolean notifyOnChange) {
mNotifyOnChange = notifyOnChange;
}
/**
* Creates an ItemAdapter from a given resource ID
*
* @param context The Context in which the ItemAdapter will be used in
* @param xmlId The resource ID of an XML file that describes a set of
* {@link Item}
* @return a new ItemAdapter constructed with the content of the file
* pointed by <em>xmlId</em>
* @throws XmlPullParserException
* @throws IOException
*/
public static ItemAdapter createFromXml(Context context, int xmlId) throws XmlPullParserException, IOException {
return createFromXml(context, context.getResources().getXml(xmlId));
}
/**
* Creates an ItemAdapter from a given XML document. Called on a parser
* positioned at a tag in an XML document, tries to create an ItemAdapter
* from that tag.
*
* @param context The Context in which the ItemAdapter will be used in
* @param xmlId The resource ID of an XML file that describes a set of
* {@link Item}
* @return a new ItemAdapter constructed with the content of the file
* pointed by <em>xmlId</em>
* @throws XmlPullParserException
* @throws IOException
*/
public static ItemAdapter createFromXml(Context context, XmlPullParser parser) throws XmlPullParserException,
IOException {
AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) {
// Empty loop
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
if (!parser.getName().equals("item-array")) {
throw new XmlPullParserException("Unknown start tag. Should be 'item-array'");
}
final List<Item> items = new ArrayList<Item>();
final int innerDepth = parser.getDepth() + 1;
final Resources r = context.getResources();
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth) {
continue;
}
String name = parser.getName();
Item item;
if (name.equals("text-item")) {
item = new TextItem();
} else if (name.equals("longtext-item")) {
item = new LongTextItem();
} else if (name.equals("description-item")) {
item = new DescriptionItem();
} else if (name.equals("separator-item")) {
item = new SeparatorItem();
} else if (name.equals("progress-item")) {
item = new ProgressItem();
} else if (name.equals("drawable-item")) {
item = new DrawableItem();
} else if (name.equals("subtitle-item")) {
item = new SubtitleItem();
} else if (name.equals("subtext-item")) {
item = new SubtextItem();
} else if (name.equals("thumbnail-item")) {
item = new ThumbnailItem();
} else {
// TODO cyril: Remove that so that we can extend from
// ItemAdapter and creates our own items via XML?
throw new XmlPullParserException(parser.getPositionDescription() + ": invalid item tag " + name);
}
// TODO cyril: Here we should call a method that children may
// override to be able to create our own Items
if (item != null) {
item.inflate(r, parser, attrs);
items.add(item);
}
}
return new ItemAdapter(context, items);
}
public int getCount() {
return mItems.size();
}
public Object getItem(int position) {
return mItems.get(position);
}
public long getItemId(int position) {
return position;
}
@Override
public int getItemViewType(int position) {
return mTypes.get(getItem(position).getClass()).type;
}
@Override
public boolean isEnabled(int position) {
return ((Item) getItem(position)).enabled;
}
@Override
public int getViewTypeCount() {
return mMaxViewTypeCount;
}
public View getView(int position, View convertView, ViewGroup parent) {
final Item item = (Item) getItem(position);
ItemView cell = (ItemView) convertView;
if (cell == null) {
cell = item.newView(mContext, null);
cell.prepareItemView();
}
cell.setObject(item);
return (View) cell;
}
}