If you have ever wanted to, say, bold a particular element within a list item row, this is for you. For example, I had a situation when I was displaying a list of discussion groups, and wanted the group owner to easily see when his or her group had new comments. For the status, I simply showed “no comments”, “comments”, or “comments“. It really caught your eye when you viewed the list and saw you had new comments.
In general, this comes in handy when you are displaying a list of items containing several fields and you want to style one or more fields individually, leaving the rest of the row unchanged.
First of all, I will briefly acknowledge the use of a customized base adapter class that provides the necessary machinery to allow us to implement this feature, fast and accurate. Please see the TextViewListAdapter
Let’s start by saying that our app has discussion forums and we want to list the comments that have been posted to a chosen forum. Our comment entity looks like the following:
public class Comment {
public long Id;
public long DiscussionId;
public long CommenterId;
public String Comment;
public Date CreatedAt;
Before moving further, let’s look at the two layout views we will be using. The first one simply creates a simple ListView and is called forum_list_view.xml as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<ListView android:id="@android:id/list"
android:layout_width="fill_parent" android:layout_height="fill_parent">
</ListView>
</LinearLayout>
The second file is the row layout view used to display each individual row of items. Comments in our case. This file is called row_layout_view.xml as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" android:padding="5dp">
<TextView android:id="@+id/row_comment"
android:layout_width="290px"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/row_comment_date"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
Next, we need to create a custom viewholder that adapts our data appropriately as follows:
static class CommentViewHolder extends ViewHolder {
public CommentViewHolder(TextView comment, TextView date) {
this.comment = comment;
this.date = date;
}
TextView comment;
TextView date;
}
Let’s say our requirement is to bold, and slightly enlarge the font size of comments made the current day. Therefore, if today is 06/01/2010, comments made on 06/01/2010 will appear bolded and slightly larger than older comments.
Now let’s create our custom list adapter by deriving from TextViewListAdapter
/**
* The implementation of CommentListTextViewListAdapter
*/
class CommentListTextViewListAdapter extends TextViewListAdapter<Comment> {
private Context context;
private long ownerPartyId;
public CommentListTextViewListAdapter(Context context, int viewid,
List<Comment> objects) {
super(context, viewid, objects);
this.context = context;
}
protected void bindHolder(ViewHolder holder) {
// Binding the holder keeps our data up to date.
// In contrast to createHolder this method is called for all items
// So, be aware when doing a lot of heavy stuff here.
// we simply transfer our object's data to the list item representatives
CommentViewHolder commentsHolder = (CommentViewHolder) holder;
Comment comment = (Comment)commentsHolder.data;
if (DateHelper.compareToToday(comment.CreatedAt)) {
commentsHolder.comment.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
commentsHolder.comment.setTextSize(14.0f);
commentsHolder.date.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
commentsHolder.date.setTextSize(16.0f);
}
else {
commentsHolder.comment.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));
commentsHolder.comment.setTextSize(14.0f);
commentsHolder.date.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));
commentsHolder.date.setTextSize(14.0f);
}
commentsHolder.comment.setText(CommentFormatter.format(comment.Comment));
commentsHolder.date.setText(comment.CreatedAtString);
}
@Override
protected ViewHolder createHolder(View v) {
// createHolder will be called only as long, as the ListView is not filled
// entirely. That is, where we gain our performance:
// We use the relatively costly findViewById() methods and
// bind the view's reference to the holder objects.
TextView comment = (TextView) v.findViewById(R.id.row_comment);
TextView date = (TextView) v.findViewById(R.id.row_comment_date);
ViewHolder commentsHolder = new CommentViewHolder(comment, date);
return commentsHolder; // return our new holder
}
}
Note that the two highlighted rows above reference the field id’s from our row_layout_view.xml file described in the beginning of this post.
Lastly, we can make use of our new classes. Assume we are displaying our customized list within an Activity class that derives from Android’s android.app.ListActivity class (this should be common usage). Recall in the start of this post we are using the file row_layout_view.xml for our ListView layout. The following loads and displays the list:
// This code can be executed in onCreate(...). // Set the list view layout within this list activity. setContentView(R.layout.forum_list_view); // Instantiate a forum. Forum forum = new Forum(forumId); // Load the list of comments. ArrayList<Comment> comments = forum.loadComments(); // Load the comments into our row_layout_view using our new adapter // effectively displaying the comments. // Note that comments made today are displayed in bold and slightly larger. int viewId = R.layout.row_layout_view; setListAdapter(new CommentListTextViewListAdapter(this, viewId, comments));
That’s it! The only thing I found tricky was to remember to set the older comments to default values as well as set new comments to make them more visibly styled. If you forget to set the older comments, things do not worked as expected. However, regarding performance, this implementation is rocks!
Below is the TextViewListAdapter
public abstract class TextViewListAdapter<T> extends BaseAdapter {
private LayoutInflater inflater;
private List<T> dataObjects; // list of data objects
private int viewId;
/**
* This is the holder that will provide fast access to arbitrary objects and
* views. Use a subclass to adapt it for your needs.
*/
public static class ViewHolder {
// back reference to our list object
public Object data;
}
/**
* The constructor.
*
* @param context is the current context
* @param viewid is the resource id of your list view item
* @param dataObjects is the list data objects, or null, if you require to indicate an empty
* list
*/
public TextViewListAdapter(Context context, int viewid, List<T> dataObjectList) {
this.inflater = LayoutInflater.from(context);
this.dataObjects = dataObjectList;
this.viewId = viewid;
if (dataObjectList == null) {
this.dataObjects = new ArrayList<T>();
}
}
/**
* The number of data objects in the list.
*/
public int getCount() {
return this.dataObjects.size();
}
/**
* Get the data object.
*
* @param position (index) to retrieve
*
* @return Return the object at indicated position. Note,
*         the holder object uses a back reference to its related data
*         object. So, the user usually should use {@link ViewHolder#data}
*         for faster access.
*/
public Object getItem(int position) {
return this.dataObjects.get(position);
}
/**
* Position equals id.
*
* @return The id of the object
*/
public long getItemId(int position) {
return position;
}
/**
* Make a view to hold each row. This method is instantiated for each list
* data object. Using the Holder Pattern, avoids the unnecessary
* findViewById(...) calls.
*
* @param position (index) to retrieve
* @param view is the view
* @param parent is the associated ViewGroup
*
* @return The view associated with this row
*/
public View getView(int position, View view, ViewGroup parent) {
// A ViewHolder keeps references to children views to avoid unnecessary
// calls to findViewById(...) on each row.
ViewHolder holder;
// When the view is not null, we can reuse it directly, there is no need
// to re-inflate it. We only inflate a new View when the view supplied
// by ListView is null.
if (view == null) {
view = this.inflater.inflate(this.viewId, null);
// call the user's implementation
holder = createHolder(view);
// we set the holder as tag
view.setTag(holder);
}
else {
// get holder back...much faster than inflate
holder = (ViewHolder) view.getTag();
}
// we must update the object's reference
holder.data = getItem(position);
// call the user's implementation
bindHolder(holder);
return view;
}
/**
* Creates your custom holder that carries a reference for your particular
* view.
*
* @param v is the view for the new holder object
*
* @return The newly created ViewHolder
*
*/
protected abstract ViewHolder createHolder(View v);
/**
* Binds the data from user's object (typically an entity)
* to the holder.
*
* @param h is the holder that represents the data object
*/
protected abstract void bindHolder(ViewHolder h);
}