Introduction

This is the third and final article in the series of how to make your own list view. Right now we have a basic working list with some nice graphics. Click here to go to the previus part of this tutorial. In this article we will add some behavior to our list and add the fling and bounce/snap effects. Fling support is in my view mandatory for any list where you navigate by touch. As a user I wouldn’t expect that the list simply stops when I lift my finger from the touch screen. If I give the list a velocity, I expect it to continue scrolling for a while, and gradually slow down until it comes to a halt. Fortunately, supporting fling is no big deal. In fact it’s very simple. Below is the source code for this part of the tutorial ready to be set up in e.g. Eclipse. And as usual: Don’t forget to download the ‘Sony Ericsson Tutorials’ app from Android market where all sample apps for this and other tutorials are collected.

[Download] 3D List sample project – Part 3 (37kb)

Adding dynamics

To make our list more customizable we are going to delegate the dynamics of the flinging to another class and letting the application set the specific dynamic class it wants to our list. It’s quite easy to imagine that different usages of a list could use different dynamics so doing like this will let us easily change the dynamics as well as reusing them for other views.

For the list to be able to handle different dynamics we need of course to have a common interface that the list can use. Below is an abstract class that an application can extend and implement.
[java]
public abstract class Dynamics {

private static final int MAX_TIMESTEP = 50;
protected float mPosition;
protected float mVelocity;
protected long mLastTime = 0;

public void setState(final float position, final float velocity, final long now) {
mVelocity = velocity;
mPosition = position;
mLastTime = now;
}

public float getPosition() {
return mPosition;
}

public float getVelocity() {
return mVelocity;
}

public boolean isAtRest(final float velocityTolerance) {
return Math.abs(mVelocity) < velocityTolerance
}

public void update(final long now) {
int dt = (int)(now – mLastTime);
if (dt > MAX_TIMESTEP) {
dt = MAX_TIMESTEP;
}

onUpdate(dt);

mLastTime = now;
}

abstract protected void onUpdate(int dt);
}
[/java]
It’s pretty simple. It has a position and a velocity. It also stores the last time it was updated to be able to calculate the time between to updates. There’s a set method that the list will use to initialize the dynamic each time we start an animation, get methods for position and velocity, a method to find out if the dynamics is at rest and finally a function to update position and velocity based on a new time. The update method computes the delta time, then it calls the protected method onUpdate(). It is this function that actually update the position and velocity and it’s up to the extending class to implement this.

For this application we will use a pretty simple dynamics.
[java]
class SimpleDynamics extends Dynamics {
private float mFrictionFactor;

public SimpleDynamics(final float frictionFactor) {
mFrictionFactor = frictionFactor;
}

@Override
protected void onUpdate(final int dt) {
mPosition += mVelocity * dt / 1000;
mVelocity *= mFrictionFactor;
}
}
[/java]
This is an implementation that models friction as something that decreases the velocity by a fixed number of percent each frame. The position is calculated from the velocity by using standard Euler integration. I didn’t mention it before but the unit for the velocity is pixels per second, that’s the reason for the divide by 1000. This is about as simple as it gets, but it works very well in my opinion.

Moving without touching

One thing that’s common for both fling and bounce/snap is that we need a mechanism to move the list without relying on touch. Right now, the only time we move the list is when we receive touch move events, in other words, only when the user is touching the screen. What we need is a way to continuously update the position for as long as we want that is separated from touch events.

A convenient way to do that is to use a Runnable. Whenever we want to start an animation we post the runnable, and in the run method we update the position of the list. Then we check if we need another frame of the animation. If we do, the runnable post itself to the view. Below is a Runnable that uses the dynamics class.
[java]
mDynamicsRunnable = new Runnable() {
public void run() {
// set the start position
mListTopStart = getChildTop(getChildAt(0)) – mListTopOffset;

// calculate the new position
mDynamics.update(AnimationUtils.currentAnimationTimeMillis());

// update the list position
scrollList((int)mDynamics.getPosition() – mListTopStart);

// if we are not at rest…
if (!mDynamics.isAtRest(VELOCITY_TOLERANCE)) {
// …schedule a new frame
postDelayed(this, 16);
}
}
};
[/java]
Why use AnimationUtils.currentAnimationTimeMillis()? Well, for animation purposes like this it’s a bad idea to use System.currentTimeMillis() since it can be changed at any time by the user or any other application. CurrentAnimationTimeMillis() instead uses SystemClock.uptimeMillis() which won’t change unexpectedly (and if we want to we could just as well used that one directly).

The scrollList() method is the same as before (roughly unchanged since the first part) but it might be a good idea to go through what will happen. scrollList() will change the mListTop variable and then request a layout. Then our onLayout() method will be called and the child views will be positioned correctly and any new views will added and views that scroll outside will be removed. Then onLayout() invalidates the list. This triggers a draw call and the list is redrawn.

Flinging

OK, now we have our abstract Dynamics class, our specific implementation of it, SimpleDynamics, and we know how to apply it using the Runnable. Now we just need to handle it in the list. We need to start the fling whenever the user lets go of the screen. Starting the fling means we need to set the velocity and position to the dynamics object and then post the runnable. In endTouch():
[java]
if (mDynamics != null) {
mDynamics.setState(mListTop, velocity, AnimationUtils.currentAnimationTimeMillis());
post(mDynamicsRunnable);
}
[/java]
We also modify the endTouch() method to take the velocity as a float. To get the velocity of the touch gesture we use the VelocityTracker. To use it we first need to obtain one. In the startTouch() method, we add the following.
[java]
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(event);
[/java]
Then we also need to make sure we recycle it when we don’t need it. In the endTouch() method we add this:
[java]
mVelocityTracker.recycle();
mVelocityTracker = null;
[/java]
For each move event we feed the velocity tracker the motion event. Then, we modify our handling of UP events to look like this.
[java]
case MotionEvent.ACTION_UP:
float velocity = 0;
if (mTouchState == TOUCH_STATE_CLICK) {
clickChildAt((int)event.getX(), (int)event.getY());
} else if (mTouchState == TOUCH_STATE_SCROLL) {
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND);
velocity = mVelocityTracker.getYVelocity();
}
endTouch(velocity);
break;

default:
endTouch(0);
break;
[/java]
What’s new here is the else statement handling up events in case of a scroll. We feed the final up event to the velocity tracker and then we calculate the velocity (in pixels per second) and get the Y-part of the velocity (we are not interested in the X-part of the velocity).

Now it’s only one thing left until we have a working fling. Whenever the user starts touching the list again, we need to stop any fling animation we have. So, in startTouch() we add a call to removeCallbacks() to remove the dynamics Runnable if it’s posted.

Accepting our limits

If we try it out now, it works nice, but an old problem that we’ve ignored until now becomes even more obvious. It is very easy to scroll and fling the list so all the items disappears. What we need is some limits to how we can scroll the list. However, instead of looking at the limits as absolute, we’re going to think of them as a bit “bendable”.

First let’s add some max and min limits to our Dynamics class.
[java]
protected float mMaxPosition = Float.MAX_VALUE;
protected float mMinPosition = -Float.MAX_VALUE;

public boolean isAtRest(final float velocityTolerance, final float positionTolerance) {
final boolean standingStill = Math.abs(mVelocity) < velocityTolerance;
final boolean withinLimits = mPosition – positionTolerance mMinPosition;
return standingStill && withinLimits;
}

public void setMaxPosition(final float maxPosition) {
mMaxPosition = maxPosition;
}

public void setMinPosition(final float minPosition) {
mMinPosition = minPosition;
}

protected float getDistanceToLimit() {
float distanceToLimit = 0;

if (mPosition > mMaxPosition) {
distanceToLimit = mMaxPosition – mPosition;
} else if (mPosition < mMinPosition) {
distanceToLimit = mMinPosition – mPosition;
}

return distanceToLimit;
}
[/java]
We’ve added set functions for the min and max positions and a method that returns how much outside the limits we are. We’ve also modified the isAtRest() method. Even if the velocity is 0, if we are not within our limits then we are not at rest.

The next step is to handle the limits in our SimpleDynamic class. And actually, the only thing (almost) we are going to do is to add this line at the top of onUpdate()
[java]
mVelocity += getDistanceToLimit() * mSnapToFactor;
[/java]
If we are outside of our limits (if getDistnaceToLimit() returns anything other than 0) we modify the velocity based on the distance to the limit. This will behave as a spring, how bouncy the spring is depends on the friction and the snap to factor. This is also about as simple as you can make this, and there are lots of more advanced algorithms that you can use instead if you want to. I like the simplicity of it and depending on the use, the behavior will often be quite nice. If you switch to modifying the position instead of the velocity you will get something that exponentially decreases the distance to the limit, looking like a critically damped spring (without any bounce).

Now, we also need to set the max and min positions in the list. Since we defined the position of the list as the position of the first item the position of the list will be 0 when we start. If we scroll up the position is going to be negative so the list position will be between 0 and some negative value which will depend on how many items we have and the height of them. However, we know that 0 is our maximum position so we can set that directly.

As for the minimum position we can set that first when we know where it is. If our last visible item position is equal to the item count minus one we know we are showing the last item. If also the bottom of that item is less than the height of the view then we have scrolled passed the limit and can set the current position as the minimum limit. scrollList() is the only place where we change the list position so we can add the code there.
[java]
if (mLastSnapPos == Integer.MIN_VALUE && mLastItemPosition == mAdapter.getCount() – 1
&& getChildBottom(getChildAt(getChildCount() – 1)) < getHeight()) {
// then save the last snap position and snap to it
mLastSnapPos = mListTop;
mDynamics.setMinPosition(mLastSnapPos);
}
[/java]

Snapping

Due to the fact that the items of the list rotate based on the position of the list, some positions are, in a way, better than others. The positions where all items are facing towards the screen are the positions that gives the best view of the list. Let’s recognize this fact by snapping to these positions, that is, when the user lets go of the screen we animate the list to the closest position where the items are rotated so they face the screen.

We can use the same dynamics class for the snapping by simply setting both max and min positions to the snap position. However, we need to re-set this snap point every time we scroll since we might have scrolled so that another snap position is closer. The below method setSnapPoint() is called from scrollList().
[java]
private void setSnapPoint() {
final int rotation = mListRotation % 90;
int snapPosition = 0;

// set snap position that corresponds to closest 90 degree rotation
if (rotation < 45) {
snapPosition = (-(mListRotation – rotation) * getHeight()) / DEGREES_PER_SCREEN;
} else {
snapPosition = (-(mListRotation + 90 – rotation) * getHeight())
/ DEGREES_PER_SCREEN;
}

// if we haven’t set mLastSnapPos before and…
// the last item is added as a child and..
// it’s bottom edge is visible
if (mLastSnapPos == Integer.MIN_VALUE && mLastItemPosition == mAdapter.getCount() – 1
&& getChildBottom(getChildAt(getChildCount() – 1)) 0) {
snapPosition = 0;
} else if (snapPosition < mLastSnapPos) {
snapPosition = mLastSnapPos;
}
mDynamics.setMaxPosition(snapPosition);
mDynamics.setMinPosition(snapPosition);
}
[/java]
The method starts with checking the current rotation and determining what 90 degree rotation is closest and then converts this rotation to a list position. The second part is similar to the code snippet before this. It checks if this is the last snap position and if it is, saves the value. Then we make sure the snap position is at most 0 and not less than the last snap position and set the snap position as both the max and min position of the dynamics.

This is not the end

Now we have reached the end of this series of tutorials. We now have a list that 1) supports basic features (we can scroll it and click and long press on items), 2) looks nice (even a bit over the top if you ask me) and 3) has a nice behavior (we can fling it and it can both bounce at the limits or snap to certain positions).

But, hopefully, it’s not the end of this list code. There are a lot of things that can be improved or tweaked and many features are still waiting to be implemented. For example, the list as it is now does not support different types of items, scrollbars, or dividers. The list also does not register an observer on the adapter so it is not aware of changes to the content. When it comes to the graphical look of the list it can of course be modified into more or less an infinite number of designs. And why not add animations when items are clicked or long pressed?

From here on I leave it to you. This list is a base to start from and, depending on your needs, you will probably need to modify, tweak and develop the list in different directions in order to use it for your application, prototype or what ever it might be. If you use and modify it, we would really appreciate if you let us know. Mail us with a link to an blog post, make a comment here, or even better, record a YouTube video like this one. We would love to see what you can come up with.

Sort by

  • By cotko
    24th June 2010.
    18:41

    thankyou thankyou thankyou :)

    Thumb up 1 Thumb down 0

  • By AndroidWorkz
    10th July 2010.
    20:48

    It appears that MyListView doesn’t react to onNotifyDataSetChanged() so if the adapter changes the ListView doesn’t change. I even tried implementing my own observer and that didn’t work… I also tried just setting the adapter again and that caused the listview to have 2 sets of the same data.

    Thumb up 0 Thumb down 0

  • By Anders Ericson
    12th July 2010.
    13:06

    Yes, the list as it is does not react to data set changed since (as I mentioned) it doesn’t register an observer. I did a quick change and added an observer to the adapter and in the onChange() call I simply reset mFirstItemPosition, mLastItemPosition, mListTop, mListTopOffset and mLastSnapPos, removed all child views, removed the mDynamicsRunnable and requested a layout. That worked fine. Though it’s not that nice that every time a change occurs, the list is scrolled back to the start. It’s certainly possible to fix this, but to do it nicely, you probably need to change the way the position of the list is represented.

    Thumb up 0 Thumb down 1

  • By AndroidWorkz
    12th July 2010.
    16:51

    Thank you for replying. :) I love this tutorial and while I am not using it for the 3d rotation, I love the dynamics… so I have applied the dynamics to the first version of the tutorial. :) I was able to get this working… however, I couldn’t follow your advice precisely. When I was resetting mFirstItemPosition, mLastItemPosition, mListTop, mListTopOffset and mLastSnapPos I kept getting indexOutOfBounds exceptions… but after I removed them from the onChange() method it worked fine. Below is what I added… just in case someone needs code examples.

    In the MyAdapter class I added a new method as below:

    public void setNotifyOnChange(boolean notifyOnChange) {
    mListView.onChange();
    }

    In MyListView.java I added this method:

    public void onChange() {
    mAdapter = getAdapter();
    removeAllViews();
    removeCallbacks(mDynamicsRunnable);
    requestLayout();
    }

    That worked. :)

    Thumb up 0 Thumb down 0

  • By Anders Ericson
    13th July 2010.
    11:38

    Glad you like it. The 3D block rotation is a bit over the top but I included it mostly to demonstrate the possibilities.
    Great that it works for you. I did the following to make it work (sort of). The code will probably be formatted without any indentation unfortunately.
    public void setAdapter(final Adapter adapter) {
    if (mAdapter != null) {
    mAdapter.unregisterDataSetObserver(mDataSetObserver);
    }

    mAdapter = adapter;
    removeAllViewsInLayout();
    requestLayout();

    if (mDataSetObserver == null) {
    mDataSetObserver = new DataSetObserver() {
    @Override
    public void onChanged() {

    // remove all views
    removeAllViewsInLayout();

    // the position of the first view
    mFirstItemPosition = 0;
    mLastItemPosition = 0;
    mListTop = 0;
    mListTopOffset = 0;
    mLastSnapPos = Integer.MIN_VALUE;
    removeCallbacks(mDynamicsRunnable);

    requestLayout();
    }
    };
    }
    mAdapter.registerDataSetObserver(mDataSetObserver);
    }
    What’s also needed for this approach to work is that you call notifyDataSetChanged() on your adapter.

    Thumb up 0 Thumb down 0

    • By Kristián Varga
      19th February 2012.
      19:21

      Hi Anders, thank you very much for this tutorial! Although I want to share a bug, maybe you already knew about it, but you have a NullPointerException when you touch the final version of the list with 2 fingers in the same time. I guess its something about the touchevent but I can’t figure out the solution. Do you have any idea? Thx

      Thumb up 0 Thumb down 0

  • By spocky
    13th July 2010.
    16:40

    Thanks for this tutorial, it’s really helpful to understand all the possibilities of the listing adapters.
    I’ve got one question : I’ve tried to mimic the behaviour seen in the video (I suppose it’s the x10 gallery browser or something like that). How do you make the middle element appear on top of elements above and below it ? I’ve tried applying a translation to the camera in that direction, but although the element appears bigger (it’s closer to the camera), it’s still partially recovered with the element just below it. I guess it’s because canvas doesn’t use depth buffer so it’s only dependent on the elements rendering order… how did you fix it ?

    Thumb up 0 Thumb down 0

  • By AndroidWorkz
    14th July 2010.
    03:26

    I think that if you keep up high quality tutorials like this you will grow a good bit of developer traffic. I have been a web developer for 10 years and you have unique content that stands apart from the rest of the fairly basic UI tutorials that can be found.

    I would love to see a 5th installment in this tutorial series about how to make the listview scroll horizontally. I haven’t seen any good tutorials about how to do that.

    Lastly, Ander… thank you for taking the time to actually answer the blog comment questions.

    Thumb up 0 Thumb down 0

  • By spocky
    15th July 2010.
    09:01

    Answering to myself (the answer was obviously in my question) : I overrided dispatchDraw() to change the order of childView.draw(), I now draw the children depending on their distance to the middle of the screen. I suppose a better way would be to override getChildDrawingOrder().
    Anyway, as AndroidWorkz said, thanks again for the high quality tutorials.

    Thumb up 0 Thumb down 0

  • By Anders Ericson
    15th July 2010.
    10:02

    spocky: Glad you like the tutorial! I assume you’re referring to the clip of Timescape? You’re right, the canvas has no concept of depth, so all that matters is rendering order. In the timescape case this was quite easy since I knew that the closer the item was to the center of the view, the “closer” they where depth-wise.

    Thumb up 0 Thumb down 0

  • By Anders Ericson
    15th July 2010.
    10:15

    AndroidWorkz: Thanks a lot for the comments! These tutorials were made to be a bit more advanced than most others since we thought that most UI tutorials were quite basic. Glad you appreciate them! We’ll try to keep posting new tutorials, though right now it’s time for some vacation for me.

    Thumb up 0 Thumb down 0