This is the first tutorial in a series of three on how to make a cool looking list in an android application. My name is Anders Ericson and I have been working with user interface (UI), for example the one in the Timescape™ application that you can find in the Xperia™ X10 mini and Xperia X10™ mini pro. Since the UI is one of the first things users will notice when they try an application the first time I wanted to create a tutorial that allow any android developer to create their own custom list view, similar to the one in Timescape™, with 3D feel and dynamics.
In this first part of the tutorial we will create a basic list and in the two following parts more and more functionality and features will be added. I will also show you how you can use the basic structure of the list and change it into whatever works best for your app. Below is a link to the source code of part 1, prepared for you to set up your own project in e.g. Eclipse. Don’t forget to download the Sony Ericsson Tutorials application from Android Market where you can try the sample apps from each step of the tutorial. I look forward to see your comments and questions.
The standard Android list view has support for quite a lot of things and covers almost all the use-cases you could think of. However, the look of the list is quite ordinary and while you can do some things by extending it, you will not be able to do that much in the end. Another disadvantage of the standard ListView is the lack of good physics (and the ability to change it). Therefore, if you want your UI to be a bit less ordinary looking, you simply need to implement your own view.
Since this is quite a lot of code to go through in one article, I have divided it up into three parts. The first part (this one) starts with creating a basic list. It’s quite a lot of material to cover, but I want to get it out of the way so we can focus more on the fun stuff later. In the second part we’ll look into changing the appearance of the list and doing some 3D-like graphics. In the final part we’ll change the behavior of the list and add some “physics” to it, something that will improve the the look and feel of our list a lot!
Although the techniques used here are the same as the ones used in the X10 Mini, the purpose of the tutorial is not to just copy a specific look but to show you how to implement your own list view. I’m sure you all have a lot of ideas on how you would like your list to look like, behave and what to use it for.
Hello AdapterView
Since we’re aiming for a list (that will show other views) we need to extend a ViewGroup and the most fitting of those are AdapterView. (The reason, or rather one reason, we’re not extending AbsListView is that it will not allow us to do bounce effects on the list.) So let’s start by creating a new Android project and create a class, MyListView, that extends AdapterView<Adapter>. AdapterView has four abstract methods that we need to implement: getAdapter(), setAdapter(), getSelectedView() and setSelection(). getAdapter() and setAdapter() are straight forward to implement. The other two will for the moment just throw an exception.
@Override
public Adapter getAdapter() {
return mAdapter;
}
@Override
public void setSelection(int position) {
throw new UnsupportedOperationException(“Not supported”);
}
@Override
public View getSelectedView() {
throw new UnsupportedOperationException(“Not supported”);
}
}
[/java]
The only thing here worth mentioning is the setAdapter method. When we get a new adapter we clear all the views we might have had previously and then we request a layout to get and position the views from the adapter. If we at this point create a test activity and an adapter with some fake data and use our new view, we will not get anything on the screen. This is because if we want to get something on the screen we need to override the onLayout() method.
Showing our first views
It is in onLayout where we get the views from the adapter and add them as child views.
[java]
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// if we don’t have an adapter, we don’t need to do anything
if (mAdapter == null) {
return;
}
if (getChildCount() == 0) {
int position = 0;
int bottomEdge = 0;
while (bottomEdge < getHeight() && position < mAdapter.getCount()) {
View newBottomChild = mAdapter.getView(position, null, this);
addAndMeasureChild(newBottomChild);
bottomEdge += newBottomChild.getMeasuredHeight();
position++;
}
}
positionItems();
}
[/java]
So, what happens here? First a call to super and a null check are performed, and then we continue with the actual code. If we haven't added any children yet, we start by doing that. The while statement loops through the adapter until we've added enough views to cover the screen. When we get a view from the adapter, we start by adding it as a child and then we need to measure it in order for the view to get it's correct size. After we've added all the views, we position them in the correct place.
[java]
/**
* Adds a view as a child view and takes care of measuring it
*
* @param child The view to add
*/
private void addAndMeasureChild(View child) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
addViewInLayout(child, -1, params, true);
int itemWidth = getWidth();
child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED);
}
/**
* Positions the children at the "correct" positions
*/
private void positionItems() {
int top = 0;
for (int index = 0; index < getChildCount(); index++) {
View child = getChildAt(index);
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
int left = (getWidth() – width) / 2;
child.layout(left, top, left + width, top + height);
top += height;
}
}
[/java]
The code here is straight forward and self-explanatory so I won't go into details. I've taken some shortcuts when measuring the child view but the code works quite well in most cases. positionItems() starts at the top (0) and layouts the child views one after the other without any padding between them. Also worth noting is that we're ignoring the possible padding that the list can have.
Scrolling
If we run this code we will now get something on the screen. However, it’s not very interactive. It does not scroll when we touch the screen and we can’t click on any item. To get touch working in the list we need to override onTouchEvent().
The touch logic for just scrolling is pretty simple. When we get a down event, we save both the position of the down event and the position of the list. We will use the top of the first item to represent the list position. When we get a move event we calculate how far we are from the down event and then re-position the list using the difference in distance from the start position. If we have no child views, we just return false.
[java]
@Override
public boolean onTouchEvent(MotionEvent event) {
if (getChildCount() == 0) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchStartY = (int)event.getY();
mListTopStart = getChildAt(0).getTop();
break;
case MotionEvent.ACTION_MOVE:
int scrolledDistance = (int)event.getY() – mTouchStartY;
mListTop = mListTopStart + scrolledDistance;
requestLayout();
break;
default:
break;
}
return true;
}
[/java]
The position of the list is now determined by mListTop and whenever it changes we need to request a layout in order to actually re-position the views. Our previous implementation of positionItems() always started to layout from 0. Now we need to change it so that it starts from mListTop.
If we try this out now the scrolling will work fine but we can also spot some obvious problems with our list. First, the scrolling has no limits so we can scroll the list so that all items are outside of the screen. We will need some kind of limits check to prevent us from doing that. Second, if we scroll down we also see that only the items that we had from the beginning are displayed. No new items are displayed even though the adapter contains more items. We postpone the fix of the first problem to a later tutorial but let’s fix the second problem right away.
Handling all the items
The reason why no new items appear when we scroll down is because of our code in onLayout(). The code there only adds views if no views haven’t already been added. One of the requirements on a list component should be that it must work just as well with ten items as it does with ten thousand items. With that in mind, we can’t just add all the items from the adapter as child views from the start, we need to make sure we handle the views efficiently. In order to be effective we should only have as many child views as we need to display the visible part of the list. It’s also a good thing to keep a small cache of views so that we can let the adapter reuse views instead of always inflating from xml.
The place to handle these issues are in onLayout(). The new verson of onLayout() looks like this.
[java]
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// if we don’t have an adapter, we don’t need to do anything
if (mAdapter == null) {
return;
}
positionItems();
invalidate();
}
[/java]
fillListDown() is more or less the same thing as the while loop that was there before. Also added is a method that does the same but adds views in the top called fillListUp(). Both of these are called from fillList(). removeNonVisibleViews() removes views (at the top and the bottom) that are outside of the visible area. To keep track of the views and to be able to connect them to the correct position in the adapter, two variables have to be added: mFirstItemPosition and mLastPosition. These are the adapter positions of the first and last currently visible views. They are updated whenever we remove or add a view. Also, since the scrolling of the list is tied to the top of the first visible item, we need to update the list position whenever we add a new view at the top or remove the top view.
To compensate for the fact that positionItems() will move the list up and down we need to let removeVisibleViews() and fillList() know how much the list will be moved. This is the offset variable. Otherwise we might not remove items that will move outside the visible area during positionItems() or we could forget to add items that should turn visible. Also, since mListTop is defined as the top of the first item, even if it’s not visible, we need to keep track of the distance from the currently first visible item to the place where the first item would have been.
If you’ve ever implemented an adapter, you know that to increase performance you should check and use the convertView argument instead of inflating a new view from xml everytime. Now we are implementing the other side, that is, we will call getView() rather than implement it, and we need to make sure we let the adapter re-use views. What we need is a cache for views that we can re-use. The standard ListView has support for different types of views but, for now, we’ll assume that all item-views are the same. As a cache we’ll just use a LinkedList of views. Whenever we remove a child view (in removeNonVisibleViews()) we add it to the cache and whenever we ask the adapter getView (in fillListDown() and fillListUp()) we send in a cached view (if we have one) as convertView.
Clicking and long-pressing
For a list to be useful the items in the list need to be clickable. AdapterView implements methods that sets OnItemClickListener and OnItemLongClickListener and we should make sure to call these listeners when appropriate. To support clicking of item views we need to do three things: 1) detect a click event, 2) find the item that was clicked and 3) call the listener (if set) with the correct arguments. So let’s start from the top and implement a click-detector.
Android provides a GestureDetector class that can be used for this but I would actually recommend not using it. One reason is that I’ve found it to be quite unreliable, especially for long press gestures and fling gestures. Another reason is that if you delegate gesture detecting to another class you might not be able to keep track of the touch state and you will most likely need to know your touch state for a lot of things.
First, lets define a few touch states
[java]
/** User is not touching the list */
private static final int TOUCH_STATE_RESTING = 0;
/** User is touching the list and right now it’s still a “click” */
private static final int TOUCH_STATE_CLICK = 1;
/** User is scrolling the list */
private static final int TOUCH_STATE_SCROLL = 2;
[/java]
We have previously overridden onTouchEvent() and now we are going to make some additions to handle the new states.
[java]
@Override
public boolean onTouchEvent(final MotionEvent event) {
if (getChildCount() == 0) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startTouch(event);
break;
case MotionEvent.ACTION_MOVE:
if (mTouchState == TOUCH_STATE_CLICK) {
startScrollIfNeeded(event);
}
if (mTouchState == TOUCH_STATE_SCROLL) {
scrollList((int)event.getY() – mTouchStartY);
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_CLICK) {
clickChildAt((int)event.getX(), (int)event.getY());
}
endTouch();
break;
default:
endTouch();
break;
}
return true;
}
[/java]
Most of this is the same as before the additions. The code that handles down events has been refactored out to a method, startTouch(). What also happens here is that we change the state to TOUCH_STATE_CLICK. Right now we don’t know if the user intends to click or scroll the view, but until we recognize a scroll, let’s treat it as a click.
The recognition of a scroll is handled in startScrollIfNeeded() which is called for move events. It compares the current touch coordinate with the coordinates for the down event and if the user has moved the finger more than a threshold value it then changes the state to TOUCH_STATE_SCROLL. I’ve used a threshold value of 10 pixels which works fine. You can also use the value from the ViewConfiguraion class by calling getScaledTouchSlop().
If we are scrolling, then it’s the same code as without the additions for click/state, though it’s now in a separate method. The list top is modified and a layout is requested to re-position the list.
To support clicking we need to handle ACTON_UP events as well and we also need to make sure we separate them from ACTION_CANCEL and ACTION_OUTSIDE events. For all events except down and move, we need to reset the touch state to TOUCH_STATE_RESTING, which is done in endTouch(), but it’s only for ACTION_UP that we should call the click listener. And of course, we should only call the click listener if we are in the click state and not in the scrolling state.
[java]
private void clickChildAt(final int x, final int y) {
final int index = getContainingChildIndex(x, y);
if (index != INVALID_INDEX) {
final View itemView = getChildAt(index);
final int position = mFirstItemPosition + index;
final long id = mAdapter.getItemId(position);
performItemClick(itemView, position, id);
}
}
private int getContainingChildIndex(final int x, final int y) {
if (mRect == null) {
mRect = new Rect();
}
for (int index = 0; index < getChildCount(); index++) {
getChildAt(index).getHitRect(mRect);
if (mRect.contains(x, y)) {
return index;
}
}
return INVALID_INDEX;
}
[/java]
clickChildAt() is responsible for calling the listener (if any) with the position for the child at the specified coordinates. To find the correct view clickChildAt() uses getContainingChildIndex() which loops through the child views and for each view checks if the coordinates given are contained within the hit-rect of the view or not.
When we have click handling in place, adding a check for long press is quite simple. A convenient way of checking for a long press is to create a Runnable that calls the long press listener. Then whenever we get a down event we post this Runnable with a delay on the view). Whenever we get an up event or when we switch to scrolling, we know it’s not going to be a long press any more so then we simply remove the Runnable by calling removeCallbacks(). How long to check for a long-press is up to you and the specific view you are implementing, but if it’s not something special, it’s a good idea to use the same delay as the rest of the system. Use ViewConfiguration.getLongPressTimeout() to get it.
In order to be able to scroll the list even if the child views respond to touch events, you need to intercept touch events when you want to start scrolling. This is done by overriding onInterceptTouchEvent() which lets us monitor all the touch events passed to our children and lets us, if we want to, intercept the touch events at any point.
[java]
@Override
public boolean onInterceptTouchEvent(final MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startTouch(event);
return false;
case MotionEvent.ACTION_MOVE:
return startScrollIfNeeded(event);
default:
endTouch();
return false;
}
}
[/java]
The implementation of onInterceptTouchEvent() looks quite like onTouchEvent(). If we get a down event, we save the position of the list then return false to let the motion event pass to wherever it’s going. When we get move events, we check if we’ve moved far enough for this to be counted as a scroll move. If we have moved enough, we set the state to scrolling and then we return true to intercept future events.
To be continued…
What we’ve made so far is a very simple list. We handle views efficiently and a user can scroll it and click and long press items. If we stop here however, we could just as well have used ListView (though we’ve learned a bit about implementing a view group). In the next part of this tutorial, we will take a look at canvas transformations and give the list a more 3D look and after that we will look into the dynamics of the list like bounce and fling effects.
Why would a developer want to do this? This cuts out all the other android devices in favor of a highly limited market. I understand you get a (subjective) great looking UI but you lose at on a majority of the market share.
Poorly-rated. 03
By Robert 22nd May 2010. 20:05
Wow – this is like so cool, I really cant wait to see some ‘cool 3d lists’ on my x10 – not. Why dont you guys stop wasting time on things like this, and actually push out an upgrade to the X10′s firmware to android 2.1/2.2, before the phone becomes totally obsolete…….Ive only had my X10 just over a week and a half, but barely a day goes by when I dont find some other bug in the firmware – and Im getting a bit fed up.
You cannot use the excuse of ‘integrating Mediascape/ Timescape’ into android to explain your delays – as essentially they are nothing more than (admittedly nice, but SLOW) apps and do nothing more than something you could download off the market. So please, pull your fingers out. Ive had SE phones for around 6 years now – but I really am starting to with Id gone with HTC – and if you bother to follow any of the various forums on the web – youd realise that there are thousands of users who feel the same…
00
By powder 23rd May 2010. 08:45
Where is the 2 and 3 part of the source?
00
By Anders Ericson 24th May 2010. 10:08
@Scott: I think there is a misunderstanding here. A developer does in no way limit himself/herself to Sony Ericsson devices by using this tutorial. The code only uses the standard SDK and works just as well on any Android device.
20
By Anders Ericson 24th May 2010. 10:20
@powder: The second and third part of the source will be published along with the tutorials. The second part will be posted at the end of this week or beginning of next.
00
By Berger 24th May 2010. 15:12
I can’t download the tutorial application on Android Market because you’ve put minimal api version as 4 and my api version is 3.
00
By Anders Ericson 24th May 2010. 16:02
@Berger: Unfortunately, in order to scale well to different screen sizes, the reference application only works on Android 1.6 (API version 4) and above. If a firmware upgrade to 1.6 is not available for your phone, you could download the source code (for each part) and remove the dependencies to API version 4.
00
By cotko 26th May 2010. 12:06
Thank you very much, can’t wait to see the 3rd part
I really enjoyed this! As stated above your tutorial is very well written! I would like to add a “feature request” though.. I have a similar idea in mind though less complicated and certainly less graphically stunning… I am trying to assign differing icons to each list item as well and can’t seem to get it down… something setting icons already in the res/drawable folders
Glad you are sharing this with everyone! Very glad to see “big business” supporting this as well
Sort by