Note: I apologize to all diligent readers of this blog but I need to give a small reminder to those who have not read the two first articles of the ‘ListView Tips & Tricks’ series. I strongly suggest to all of them to look at the previous tips and tricks I have illustrated. Reading those posts is not mandatory but highly recommended as they may be mentioned here. Do not hesitate to learn more about emptiness handling and sectioning
Note: I do not know how many articles will be written in this series of tips and tricks about the ListView
widget. In the second opus, I gathered all sample codes into a single
repository on GitHub. This project will be augmented with each code I
will be developing in this series. For instance, all of the code you
will find in this page is already available in the ListViewTipsAndTricks
project. You can clone/fork it at the following GitHub page:
http://github.com/cyrilmottier/ListViewTipsAndTricks
In the previous article, we have learned how to section data inside a
ListView. I have also given you a few tips that might have helped you
to improve performance in your application. Providing a very smooth and
responsive UI is essential to users as mobile applications are a way for
them to improve their productivity. Performance and responsiveness are
great but I’m sure all of you know users are very demanding. They also
want their applications to be user-friendly, easy-to-use and …
beautiful. In this article we will focus on creating fancy and eye-candy
ListView
s
Along this post we will display a list of cheeses. I know this is far
from being a great set of data (especially for a guy that never eat
cheese like me ^^). The reason for this is I preferred focusing on the
actual code of the ListView
rather the data-fetching code.
I’m sure, you can all imagine cheeses as relevant and have enough
imagination to consider cheeses like something more ‘accurate’: tweets,
Google+ messages, etc.
Our goal in this post is to learn few tips to create well-designed ListView
-based applications. ListView
s
are widely used on the Android platform and must be designed keeping in
mind its actual purpose. For instance, the Contact app is regularly
used but the time taken by a user to find a contact must be as quick as
possible. To fulfill these requirements, the Android team developed a
very easy-to-read ListView
: clean, containing as few texts
as possible, large texts, plain background color, white texts on black
background, etc. Even if this is great for the ‘Contact’ app, this is
not a perfect and generic solution. Conversely, the ‘Messages’ app
contains a ListView
designed to be used as often but in a
slower way, rarely flinged, differentiating each participant of a
conversation, etc. The solution to this problem has been to create a ListView
with various background colors, a small but readable font, using grey
color for less important information, etc. In other word, a smooth ListView
is not enough: you have to design it constantly thinking about the way your customers will use it.
The starting point
Let’s start by creating a ListActivity
displaying our list of cheeses. The first step is to create a layout containing our ListView
.
The code below shows you the layout we will be using afterwards in this
article. You can also find a graphic representation of this layout in
the screenshot below.
<?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="fill_parent" android:orientation="vertical"> <ListView android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" /> <LinearLayout style="@android:style/ButtonBar" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:layout_gravity="center_vertical" android:text="@string/draw_selector_on_top" android:onClick="onDrawSelectorOnTop" /> <Button android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:layout_gravity="center_vertical" android:text="@string/use_selector_as_background" android:onClick="onUseSelectorAsBackground" /> </LinearLayout> </LinearLayout> |
As you may have noticed, the layout contains a ‘bottom bar’ with two
Buttons. We could have created a very basic layout … or even not
creating one at all and letting the ListActivity
create a full-sized ListView
but we will actually use those Button
s in the rest of this post to switch between different ’styling’ methods.
The code of our basic ListActivity
is given below:
package com.cyrilmottier.android.listviewtipsandtricks; import android.app.ListActivity; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import com.cyrilmottier.android.listviewtipsandtricks.data.Cheeses; public class FancyListActivity extends ListActivity { private FancyAdapter mFancyAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fancy_list); mFancyAdapter = new FancyAdapter(Cheeses.CHEESES); setListAdapter(mFancyAdapter); } private class FancyAdapter extends BaseAdapter { private String[] mData; public FancyAdapter(String[] data) { mData = data; } @Override public int getCount() { return mData.length; } @Override public String getItem(int position) { return mData[position]; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { TextView result; if (convertView == null) { result = (TextView) getLayoutInflater().inflate(R.layout.text_item, parent, false); } else { result = (TextView) convertView; } final String cheese = getItem(position); result.setText(cheese); return result; } } } |
If we look at the overall appearance of this UI, we have a very basic
ListView with a black background. When the user taps an item, the
background of this itemview turns orange while the text turns black.
Where do those colors come from? In the source code of our application,
there is no reference to colors nor Drawable
s. The actual reason is all styles come from the Application
/Activity
theme. As we have set no theme to our application, the default theme (@android:style/Theme
) is used.
List selector
Let’s start customizing our ListView
! The first thing we
can change from the default theme is the color of the pressed itemview.
In order to do that, we will use the android:listSelector
XML attribute which is equivalent to the ListView.setSelector(int)
method. The list selector is a Drawable
drawn by the ListView
as the background of the pressed/focused itemview. The ListView
manages all types of Drawable
s but the preferred Drawable
is usually the StateListDrawable
. This Drawable
changes its appearance based on its current state (focused, pressed,
selected, etc.). The capture below shows the list selector we want to
develop in the focused and pressed states:
Note: As some of you may know, I am a huge fan of Android Drawable
s.
I think they are one of the most amazing features of the Android UI
toolkit. They are so powerful I have given a talk named “Mastering
Drawables” during the Droidcon (London - 2010) as well as the Android
Developer Labs (Paris - 2010). If you want to know more about Drawable
s, you can have a look at the slides (in French only … it seems I have lost the English version of those slides) on Slideshare and the sample codes I made on GitHub
Edit (2011-09-24): I finally succeed to find the English version of the slides. You can download them here
In this example, we will create a brand new list selector that
displays a nice gradient beneath the pressed/focused itemview. The first
thing we need to do is to create two GradientDrawable
s: the first one for the pressed state and the second one for the focused state. The GradientDrawable
s below are respectively called list_selector_pressed.xml and list_selector_focused.xml and can be found in the res/drawable
folder:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="#fb9d23" android:endColor="#ffc579" android:angle="90" /> </shape> |
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="#f5c98c" android:endColor="#f7ddb8" android:angle="90" /> </shape> |
We can now create our own list selector that references the two previous Drawable
s (this is where the magic happens ^^). This file is located in the res/drawable
folder and its name is list_selector.xml:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@drawable/list_selector_pressed" /> <item android:state_focused="true" android:drawable="@drawable/list_selector_focused" /> <item android:drawable="@android:color/transparent" /> </selector> |
The only thing we need to do now is to apply this brand new list selector to our ListView
. You can do it by adding android:listSelector="@drawable/list_selector"
to the ListView
tag in your layout.
Note: Using a color as part of your list selector may entirely fill your ListView
. This problem comes from the fact a ColorDrawable
does not respect its bounds: it entirely fills the clip rect of the Canvas
in which it is drawn into. I have always considered this as a bug but
nothing has been fixed until … Android 3.0 (Honeycomb)! As a result,
using a ColorDrawable
in your list selector is strongly
discouraged. Fortunately, this article is entitled ‘ListView tips &
tricks’ and here is an easily way to get round this bug: instead of
setting a ColorDrawable
, use a Drawable
that respect its bounds such as a GradientDrawable
or a NinePatchDrawable
. The code below shows a GradientDrawable
that can be used in replacement of a regular ColorDrawable
.
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#fb9d23" /> </shape> |
Miscellaneous styling
ListView
s provides a lot of “styling” features. In this paragraph we will focus on various methods that may be used to style your ListView
. Please note those most of those tips are given “as it” and are not part of the sample code.
Cache color hint
By default, an Android View
has a transparent
background. Although it seems natural … transparency involves a lot of
calculation when rendering your application on screen. In order to avoid
blending and make the rendering faster while scrolling/flinging, the ListView
widget uses a ’scrolling cache’ mechanism. This mechanism consists on taking an opaque screenshot of all visible child View
s (in other words, it draws the content of each itemview in a Bitmap
)
and directly draws those screenshots when necessary. The color used to
make those screenshots opaque is the one given by the cache color hint.
If your ListView
turns opaque (black for instance) when scrolling that because you need to set the cache color hint to the actual color of your ListView
background. Setting this property @android:color/transparent
will fix the problem but will also disable the optimization …
If you want to know more about the cache color hint optimization I strongly recommend you to have a look at a really great article written by Romain Guy. You can read it on the Android Developer Blog
Divider
Contrary to the ScrollView
widget, ListView
has a built-in support for dividers. By default, a divider is a 1-pixel
tall line that is used to separate two itemviews. You can style/modify
this property with the android:divider
XML attribute. Setting a new divider can be done with a color or a Drawable
. When drawing the content, the ListView
will automatically look at the height of the divider to part the
itemviews. In other words, it means you can use a very complex Drawable
as divider and your ListView
will automatically measure it and draw it when necessary.
This looks amazingly powerful and easy, isn’t it? It actually is … but there is little a problem. Some Drawable
s (for instance the ColorDrawable
) has no intrinsic height (which means a call to Drawable.getIntrinsicHeight()
returns -1). In order to by-pass this problem, ListView
provides a android:dividerHeight
XML attribute that asks for a dimension.
Note: The previously detailed problem concerning the un-respected bounds does not apply to dividers. This is odd, isn’t it? Indeed, ListView
has an internal mechanism clipping the Drawable
to the correct rectangle on screen when the divider is a ColorDrawable
. As a result you can safely use colors when styling your dividers.
Fading edges
By default, a ListView
fades away at the top and bottom edges once the content is larger than the actual size of the ListView
. This property is called fading edge and can be easily removed or modified using android:fadingEdge
and/or android:fadingEdgeLength
. You can look at the sample code of this post in which I have prefered removing this Android feature by adding a android:fadingEdge="none"
in the XML attributes of our ListView
.
ColorStateList
Sooner, we have discovered a way to create a custom list selector. If
you look closely to the image, you will notice the text has a different
color between the normal and pressed states. Changing the text color
may be mandatory in some cases where the background color of a pressed
itemview is very close to the color of the text. In order to change the
color of the text depending on the current state of the itemview, you
can use a ColorStateList
. I will not describe how to use it here but you can take a look at the framework documentation. This class and its use is described very clearly in the Android developer guide
Overscroll Drawables
Since Android 2.3, ListView
s gives a visual feedback when the user tries to scroll the content of the ListView
over its bounds. This feedback looks like a ‘halo’ at the bottom/top of the ListView
. You can set your own Drawable
using one of the setOverscroll[Footer/Header](Drawable)
method.
Customizing itemviews background
Changing the background color of a ListView
can be rather complex. The next part gives you a few tips and tricks you can use to create eye-candy and unique ListView
s.
Our goal is to change the background of our UI to a light color. We
will also try to emphasize some cheeses by setting the background of
their itemview to a more noticeable color. The screenshots below shows
our final ListView
depending on the method used:
ListView background
The simplest approach of changing the UI background color is to change the background color of the ListView
. In order to do that, we simply have to use the android:background
XML attribute or one of the setBackground[Color|Resource|Drawable]
method.
The main problem with this approach is the background is applied
globally. In other words, it means you don’t a control over each
itemview as the background applied to the entire ListView
. This works particularly great with a plain color but this method will not allow you style two itemviews differently.
Itemviews background
In the previous method, we have chosen to change the background of the entire ListView
.
If we want to have a control over each itemview appearance it makes
sense to change the background of each of our itemviews independently.
The code below shows you how to apply a background depending on whether
the underlying data is special or not:
@Override public View getView(int position, View convertView, ViewGroup parent) { TextView result; if (convertView == null) { result = (TextView) getLayoutInflater().inflate(R.layout.text_item, parent, false); result.setTextColor(Color.BLACK); } else { result = (TextView) convertView; } final String cheese = getItem(position); result.setText(cheese); result.setBackgroundResource(isSpecial(cheese) ? R.drawable.list_item_background_special : R.drawable.list_item_background_normal); return result; } |
If you compile and run this example, everything will work perfectly fine … until you try to press an itemview. Indeed, using the previous technique, the customized list selector we created never shows up. It may look like a bug at first but this is actually completely normal. Let’s look at how things are drawn the following diagram (you can click on the image to access to a larger version):
As you can see, Android first draws the ListView
background as well as the dividers. Then, the system draws the list selector. Finally, ListView
renders all itemviews on top of that. Thanks to this diagram, it is
clear the list selector will never be visible with opaque background set
to itemviews. Fortunately, I have a listed a few solutions you can
implement.
Drawing selector on top
The ListView
or more globally the AbsListView
offers a android:drawSelectorOnTop
XML attribute and its Java equivalent setDrawSelectorOnTop(boolean)
. As mentioned in the method’s name, setting this property to true will indicate the ListView
to draw the selector on top of each itemview. Even if this looks like a
good option, drawing selector on top does not allow you to have opaque
list selector as the content of your itemview will be completely hidden.
OnItemClickListener and OnItemSelectedListener
I have seen a lot of samples over the Internet using the OnItemClickListener
and OnItemSelectedListener
in order to be notified the background must be changed to transparent …
This can work but I think this is a pure hack considering the features
available in the Android framework. Even if this technique is probably
great on some mobile platforms could you please guys stop using it on
the Android platform. This is not a correct nor easy way to do it on
Android. As a result, you can probably guess why there is no line of
code in this article explaining the trick hack.
StateListDrawable
As we have seen previously, our biggest problem comes the fact the
background is opaque and hides the list selector when the itemview is
pressed. Hence, we need to find a way to make the background transparent
when pressed. This looks like something we have already done and some
of you may have understood where I want to get to … Hurray, you are
right! The trick consists on using a StateListDrawable
as the background of each itemview. This Drawable
will have the exact opposite behavior of the list selector: it will be
opaque by default and will become transparent when pressed, focused or
selected. The code below refers to the list_selector_normal.xml (the list_selector_special.xml is very similar and is not available here. Look at the sample code for more details):
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@android:color/transparent" /> <item android:state_selected="true" android:drawable="@android:color/transparent" /> <item android:state_focused="true" android:drawable="@android:color/transparent" /> <item android:drawable="@drawable/list_item_background_normal" /> </selector> |
This trick can even be pushed to its paroxysm by setting the list selector of your ListView to null
or a transparent color and letting each itemview handle the all states
itself. You only thing you need to do here is to create a final StateListDrawable
containing the background (normal state) as well as the gradient used
as visual interaction feedback (pressed/focused state). This method
works pretty well and does not suffer from the previously mentioned
problem regarding ColorDrawable (the color is drawn at the itemview
level not at the ListView
level) but it does not force you
to have consistent pressed/focused states between your itemviews. Even
if it is usually okay to have different Drawable
for the ‘normal’ background it is generally not recommended to have different Drawable
between itemviews.
Conclusion
Although we have just scratched the surface of the styling possibilities, you now have a large set of keys to design eye-candy ListView
s. Do not hesitate to use those tips always keeping in mind what the important questions are: “What’s the real purpose on this ListView
?”, “What are my users’ expectations?”, “Do I need to focus on productivity, readability, efficiency or something else?”, etc.
I hope you have appreciated this article. I know it is quite a long
post but let’s say this will be the only one I will be posting this
month of holidays ^^. Do not hesitate to leave a comment below if you
have questions, if you think I have made a mistake somewhere or simply
if you want to thank me for this! Have fun styling your ListView
s!
'Dev > Android' 카테고리의 다른 글
Android SystemInfo (0) | 2012.06.02 |
---|---|
Android 제조사별 소스 (0) | 2012.06.01 |
Android - Dialog (1) | 2012.04.23 |
Android 에서 화면 회전하는 방법 (0) | 2012.04.23 |
Android Emulator Market 2.2 (마켓버전 Image) (0) | 2012.04.22 |