Click here to Skip to main content
Click here to Skip to main content

Implementing a smoothly animated ListBox

, 12 Jun 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
An article on creating an iPhone style ListBox.

Introduction

This article shows how you can implement a smooth list box that allows intuitive, friction affected, scrolling of the list items. By friction based, I mean that the user can apply a dragging force in one direction and the list will keep scrolling after the mouse or stylus is released and then slow down by itself. I've looked at how lists behave on the iPhone and tried to mimic that to a certain extent, but I'm nowhere near anything as cool as what the iPhone presents.

The properties of the iPhone list box that I think are the neatest are the fact that you do not scroll it using a scrollbar but rather just grab the list items and drag them, much neater.

Also, on the iPhone, one is allowed to scroll beyond the bounds of the list box and the items just snap back into place. Very cool.

Note: This article and download has now been updated to include a version where you can use the keys to scroll the list as well. I didn't have access to Visual Studio 9 when implementing this, so there's been a few changes to revert back to the .NET 2.0 level of compliance.

Thanks to Mixxer, mbrucedogs, and Daniel Bass who suggested tips and implementations and pushed me to implement the key support. Unfortunately, I haven't had time to implement everything they asked for.

Background

As the standard ListBox in the .NET Compact Framework is kind of limited, I wanted to create one which fulfilled three requirements;

  1. The list must scroll smoothly, and rely on "friction" and "springs" to give it an intuitive feel.
  2. The list must be able to have list items that are more than just text and an icon, any Control must be able to act as a list item.
  3. The list must be fast enough to run on a PocketPC 2003 device.

All in all, not terribly complicated requirements, and after implementing it first for the Desktop and then porting it to the .NET Compact Framework, the requirement that gave me the most grief was actually #3.

Using the Code

The downloadable Visual Studio solution contains three C# projects;

  • Extended ListItems; This is a class library project that has two predefined list item controls that can be used to show information about music albums or Formula One teams.
  • Smooth ListBox; This project holds the actual list box implementation.
  • Smooth ListBox (Test); This is a test project that creates a sandbox application to try the SmoothListBox in three different ways.

I've implemented this solution using .NET 3.5, but it should be trivial to get it working for .NET 2.0 as well.

Creating a Custom ListBox

As my SmoothListBox was going to be completely different in both appearance and behaviour from the list box found in the System.Windows.Forms namespace, I decided to build it from "scratch" and not inherit from ListBox. As the list box should behave like any other component in the designer, it inherits from UserControl.

picture3.jpg

The SmoothListBox showing Formula One team information.

List Item Storage

The first thing to solve is the storage of the list items. I was first planning on using a List<Control> for this, but found that I could just as well use the Controls property provided by UserControl. This also makes sense from the point of view that the list items are really nothing other than Controls owned and displayed by the SmoothListBox. For future purposes, such as adding a scrollbar (the current implementation does not support a scrollbar as I didn't really see the need for it), list items are not added directly to the SmoothListBox's Controls. To accommodate more than just the list items being owned by the SmoothListBox, the control holds an internal Panel that directly owns the list items. This, in turn, means that doing mySmoothListBox.Controls.Add(new MyListItem()) won't work as it will not add the list items to the correct container. Therefore, two new methods, AddItem and RemoveItem, need to be implemented. This is actually quite good as the semantics makes more sense that way (adding items to a list is different from manipulating controls in a container).

Selection State Handling

In order to keep track of which items are currently selected (as the SmoothListBox supports multiple selection), a look-up dictionary is used. This maps list items to a selected state like so:

private Dictionary<Control, bool> selectedItemsMap = new Dictionary<Control, bool>();

Two boolean members define the selection model for the list box;

  • MultiSelectEnabled: when true, the selection model allows several items to be selected at the same time. If set to false, an already selected list item is automatically de-selected when a new one is selected.
  • UnselectEnabled: when true, the user can explicitly de-select a selected value; if set to false, the only way to do this is to select another value.

And, as this is a .NET 3.5 project, these are declared as automatic properties:

/// <span class="code-SummaryComment"><summary></span>
/// If set to <span class="code-SummaryComment"><c>True</c> multiple items can be selected at the same</span>
/// time, otherwise a selected item is automatically de-selected when
/// a new item is selected.
/// <span class="code-SummaryComment"></summary></span>
public bool MultiSelectEnabled
{
    get;
    set;
}

/// <span class="code-SummaryComment"><summary></span>
/// If set to <span class="code-SummaryComment"><c>True</c> then the user can explicitly unselect a</span>
/// selected item.
/// <span class="code-SummaryComment"></summary></span>
public bool UnselectEnabled
{
    get;
    set;
}

List Event Handling

A custom event that fires whenever a list item is clicked is also defined:

/// <span class="code-SummaryComment"><summary></span>
/// Delegate used to handle clicking of list items.
/// <span class="code-SummaryComment"></summary></span>
public delegate void ListItemClickedHandler(SmoothListbox sender, 
                     Control listItem, bool isSelected);

class SmoothListBox
{    
    /// <span class="code-SummaryComment"><summary></span>
    /// Event that clients hooks into to get item clicked events.
    /// <span class="code-SummaryComment"></summary></span>
    public event ListItemClickedHandler ListItemClicked;
    
    ...
}

Basic List Box Definition

So, having defined all the necessary parts (well, kind of) for the list box, the class definition can be constructed:

namespace Bornander.UI
{
    public delegate void ListItemClickedHandler(SmoothListbox sender, 
                         Control listItem, bool isSelected);

    public partial class SmoothListbox : UserControl
    {
        public event ListItemClickedHandler ListItemClicked;

        private Dictionary<control, /> selectedItemsMap = new Dictionary<control, />();

        private void FireListItemClicked(Control listItem)
        {
            if (ListItemClicked != null)
                ListItemClicked(this, listItem, selectedItemsMap[listItem]);
        }

        /// <span class="code-SummaryComment"><summary></span>
        /// Adds a new item to the list box.
        /// <span class="code-SummaryComment"></summary></span>
        public void AddItem(Control control)
        {
            ...
        }

        /// <span class="code-SummaryComment"><summary></span>
        /// Removes an item from the list box.
        /// <span class="code-SummaryComment"></summary></span>
        public void RemoveItem(Control control)
        {
            ...
        }

        public bool MultiSelectEnabled
        {
            get;
            set;
        }

        public bool UnselectEnabled
        {
            get;
            set;
        }

        public List<Control> SelectedItems
        {
            get
            {
                List<Control> selectedItems = new List<Control>();
                foreach (Control key in selectedItemsMap.Keys)
                {
                    if (selectedItemsMap[key])
                        selectedItems.Add(key);
                }
                return selectedItems;
            }
        }
    }
}

Making it Smooth

OK, so storing some information and firing an event doesn't really make the list box scroll smoothly in any way. To get that part working, quite a lot of mouse handling and animation code has to be added to the implementation.

Mouse Handling

When scrolling, I do not want to have to grab a handle on a scroll bar and drag that around, I want to be able to "grab" anywhere in the list item and drag to scroll (with certain limitations that will be explained later). On the iPhone, the user just "flicks" through the list using his/her finger anywhere in the list. This is a very intuitive way of scrolling, and I want my list box to behave the same way.

According to requirement #2, the list box must support any list item that inherits from Control. This means that a list item can have any number of nested child controls, and regardless of whether the "base" list item control is the control that receives the mouse events or if it is one of the list item's child control, the events need to be handled in a uniform way. To accommodate this, list box global mouse event listeners are added recursively to any list item being added using the AddItem method. In WPF, this could have been handled using bubbling up or filtering down of events, but in WinForms, we have to do it ourselves. The SmoothListBox has three members, all MouseEventHandlers, that listens to mouse down, up, and move events. The recursive adding of the listeners is done by a static helper class called Utils:

namespace Bornander.UI
{
    static class Utils
    {
        public static void SetHandlers(
        Control control, 
        MouseEventHandler mouseDownEventHandler, 
        MouseEventHandler mouseUpEventHandler, 
        MouseEventHandler mouseMoveEventHandler)
        {
            control.MouseDown -= mouseDownEventHandler;
            control.MouseUp -= mouseUpEventHandler;
            control.MouseMove -= mouseMoveEventHandler;

            control.MouseDown += mouseDownEventHandler;
            control.MouseUp += mouseUpEventHandler;
            control.MouseMove += mouseMoveEventHandler;

            foreach (Control childControl in control.Controls)
            {
                SetHandlers(
            childControl, 
            mouseDownEventHandler, 
            mouseUpEventHandler, 
            mouseMoveEventHandler);
            }
        }

        public static void RemoveHandlers(
        Control control, 
        MouseEventHandler mouseDownEventHandler, 
        MouseEventHandler mouseUpEventHandler, 
        MouseEventHandler mouseMoveEventHandler)
        {
            control.MouseDown -= mouseDownEventHandler;
            control.MouseUp -= mouseUpEventHandler;
            control.MouseMove -= mouseMoveEventHandler;

            foreach (Control childControl in control.Controls)
            {
                RemoveHandlers(
            childControl, 
            mouseDownEventHandler, 
            mouseUpEventHandler, 
            mouseMoveEventHandler);
            }
        }
    }
}

So, in the AddItem method, Utils.SetHandlers is called, and after that, the list box is aware of every mouse related thing that happens to a list item or the list item's children. We can now go ahead and implement the dragging.

Dragging

MouseDown

The first thing, naturally, to handle when processing mouse dragging is the mouse down event. As far as the smooth scrolling goes, nothing really happens in the mouse down event except the storing of some information that'll later prove to be vital:

/// <span class="code-SummaryComment"><summary></span>
/// Handles mouse down events by storing a set of <c />Point</c />s that
/// will be used to determine animation velocity.
/// <span class="code-SummaryComment"></summary></span>
private void MouseDownHandler(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        mouseIsDown = true;
        // Since list items move when scrolled all locations are 
        // in absolute values (meaning local to "this" rather than to "sender".
        mouseDownPoint = Utils.GetAbsolute(new Point(e.X, e.Y), sender as Control, this);
        previousPoint = mouseDownPoint;
    }
}

The Utils.GetAbsolute method is a helper method that converts a point in child-local coordinates to parent-local coordinates; this is important as we're going to move list items around during dragging, and if we're not looking at the absolute coordinates, they won't behave as expected.

MouseMove

The next natural thing to handle is the mouse move event. Again, not very much is required to handle this event (at least, not conceptually), but the implementation calls for some weird things that might require further explanation. What the mouse move handler needs to do is to measure the vertical distance that the mouse has moved and then make sure that the list items are scrolled by that amount. Easy enough, right? Well, here's where I ran into some weird thing.

As the list has to be animated to give the impression of obeying friction and spring laws, it has to be periodically re-drawn whenever a specific timer event fires. But, the list also has to be re-drawn when a user mouse drag has invalidated the list items current positions. This initially caused the list to re-draw too often, causing the Mobile Device to hang or run extremely slow. One way to fix this would have been to turn off the animation timer when the list is being dragged by the mouse and enable it again when the mouse is released.

I tried that approach, but it resulted in a too long delay between when the stylus was lifted and when the list scrolled "on its own", making it look a bit unnatural. I ended up solving it using a much simpler approach. By keeping a boolean member that is set and unset by the timer event handler method and the mouse move method, I can discard updates that occur too often causing the list movement to "stall".

private void MouseMoveHandler(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        // The lock flag prevents too frequent rendering of the 
        // controls, something which becomes an issue of Devices
        // because of their limited performance.
        if (!renderLockFlag)
        {
            renderLockFlag = true;
            Point absolutePoint = Utils.GetAbsolute(new Point(e.X, e.Y), 
                                  sender as Control, this);
            int delta = absolutePoint.Y - previousPoint.Y;
            draggedDistance = delta;
            ScrollItems(delta);
            previousPoint = absolutePoint;
        }
    }
}

As seen from this snippet, the mouse move event is only handled if the renderLockFlag is set to false. This means that if a mouse move event occurs before the previous one was handled, it is discarded. The flag is reset by the animation tick handler method:

private void AnimationTick(object sender, EventArgs e)
{
    renderLockFlag = false;
    DoAutomaticMotion();
}

MouseUp

The last mouse event to handle is the mouse up event; here, the velocity that the list should continue to scroll with is calculated. Also, handling of list item selection/de-selection is taken care of when the mouse up event occurs.

To calculate the velocity the list should have is quite easy; it's simply the dragged distance (distance between the last two mouse move events) multiplied with a constant factor.

To handle the selection or de-selection of list items, a few checks have to be made as the SmoothListBox allows different selection modes; multiple items selection can be disabled or enabled as can explicit de-select of items. Further, as it is not required for a list item to implement IExtendedListItem (see a later chapter for details on this) checks have to be performed on the type of the affected list item to see if methods need to be called on that list item. The benefit of implementing IExtendedListItem is that the list items can dictate their behaviour when being selected or de-selected, as shown in my example application where they change size and or content when selected.

private void MouseUpHandler(object sender, MouseEventArgs e)
{
    // Only calculate a animation velocity and start animating if the mouse
    // up event occurs directly after the mouse move.
    if (renderLockFlag)
    {
        velocity = Math.Min(Math.Max(dragDistanceFactor * 
                     draggedDistance, -maxVelocity), maxVelocity);
        draggedDistance = 0;
        DoAutomaticMotion();
    }

    if (e.Button == MouseButtons.Left)
    {
        // If the mouse was lifted from the same location it was pressed down on 
        // then this is not a drag but a click, do item selection logic instead
        // of dragging logic.
        if (Utils.GetAbsolute(new Point(e.X, e.Y), sender as Control, 
                                         this).Equals(mouseDownPoint))
        {
            // Get the list item (regardless if it was a child Control that was clicked). 
            Control item = GetListItemFromEvent(sender as Control);
            if (item != null)
            {
                bool newState = UnselectEnabled ? !selectedItemsMap[item] : true;
                if (newState != selectedItemsMap[item])
                {
                    selectedItemsMap[item] = newState;
                    FireListItemClicked(item);

                    if (!MultiSelectEnabled && selectedItemsMap[item])
                    {
                        foreach (Control listItem in itemsPanel.Controls)
                        {
                            if (listItem != item)
                                selectedItemsMap[listItem] = false;
                        }
                    }

                    // After "normal" selection rules have been applied,
                    // check if the list items affected are IExtendedListItems
                    // and call the appropriate methods if it is so.
                    foreach (Control listItem in itemsPanel.Controls)
                    {
                        if (listItem is IExtendedListItem)
                            (listItem as IExtendedListItem).SelectedChanged(
                                                selectedItemsMap[listItem]);
                    }

                    // Force a re-layout of all items
                    LayoutItems();
                }
            }
        }
    }
    mouseIsDown = false;
}

Animating

As seen in some of the above snippets, a method called DoAutomaticMovement is used. This method animates the list motion when the mouse or stylus is not affecting it.

picture2.jpg

One thing to handle is to calculate the current speed of the list animation and update the position of the list items accordingly. Friction is applied during this process as the current speed, or velocity, is multiplied with the de-acceleration factor:

velocity *= deaccelerationFactor;
float elapsedTime = animationTimer.Interval / 1000.0f;
float deltaDistance = elapsedTime * velocity;

The elapsed time is estimated, for convenience (read laziness), to the timer interval. This probably should have been changed to an actual measured delta time between updates for an even smoother animation. A requested difference in distance, deltaDistance, is calculated because we only want to move the items and re-layout the list box if the actual distance moved is greater than one pixel.

// If the velocity induced by the user dragging the list
// results in a deltaDistance greater than 1.0f pixels 
// then scroll the items that distance.
if (Math.Abs(deltaDistance) >= 1.0f)
    ScrollItems((int)deltaDistance);
else
{
    ...
}

The ScrollItems method is the method that actually re-positions the list items by a certain distance.

The else statement is necessary because if the de-acceleration has caused the list item to no longer have any velocity, we need to check whether the list has been scrolled "out of bounds". I would like the user to be able to apply enough velocity for the list items to scroll beyond the visible area of the list box, and when they've slowed down to zero velocity, the list box will make sure they "snap" back into view.

The full DoAutomaticMotion looks like this:

private void DoAutomaticMotion()
{
    if (!mouseIsDown)
    {
        velocity *= deaccelerationFactor;
        float elapsedTime = animationTimer.Interval / 1000.0f;
        float deltaDistance = elapsedTime * velocity;

        // If the velocity induced by the user dragging the list
        // results in a deltaDistance greater than 1.0f pixels 
        // then scroll the items that distance.
        if (Math.Abs(deltaDistance) >= 1.0f)
            ScrollItems((int)deltaDistance);
        else
        {
            // If the velocity is not large enough to scroll
            // the items we need to check if the list is
            // "out-of-bound" and in that case snap it back.
            if (itemsPanel.Top != 0)
            {
                if (itemsPanel.Top > 0)
                    ScrollItems(-Math.Max(1, (int)(snapBackFactor * 
                                             (float)(itemsPanel.Top))));
                else
                {
                    if (itemsPanel.Height > ClientSize.Height)
                    {
                        int bottomPosition = itemsPanel.Top + itemsPanel.Height;
                        if (bottomPosition < ClientSize.Height)
                            ScrollItems(Math.Max(1, (int)(snapBackFactor * 
                                       (float)(ClientSize.Height - bottomPosition))));
                    }
                    else
                        ScrollItems(Math.Max(1, -((int)(snapBackFactor * 
                                                  (float)itemsPanel.Top))));
                }
            }
        }
    }
}

The items are moved by moving the entire itemsPanel in the ScrollItems method:

private void ScrollItems(int offset)
{
    // Do not waste time if this is a pointless scroll...
    if (offset == 0)
        return;

    SuspendLayout();
    itemsPanel.Top += offset;
    ResumeLayout(true);
}

Key Handling

The easiest way to get key handling working was not to try to hook key listeners to all relevant elements as I did for the mouse handling. The reason for this is that (according to Daniel Bass) the key events didn't fire as expected.

I decided instead to use an approach where I simply read the key states just prior to animating the list movement. This way, I could add velocity in either the upwards or downwards direction, and this would give the list an accelerating change in velocity.

To get the asynchronous key states, I used a DLL interop call:

public partial class SmoothListbox : UserControl
{
    [DllImport("coredll.dll")]
    public static extern int GetAsyncKeyState(int vkey); 

    ...
}

That method could then be queried in the AnimationTick method:

private void AnimationTick(object sender, EventArgs e)
{
    if (GetAsyncKeyState((int)System.Windows.Forms.Keys.Up) != 0)
        velocity += keyAcceleration;
    if (GetAsyncKeyState((int)System.Windows.Forms.Keys.Down) != 0)
        velocity -= keyAcceleration;

    //System.Diagnostics.Debug.WriteLine("Out: " + velocity);
    renderLockFlag = false;
    DoAutomaticMotion();
}

The keyAcceleration is a poorly named member that defines how much additional velocity should be added. It is accessible using the KeyAcceleration property.

Custom List Items

As I want the list box to have items that can react to being selected or de-selected, an interface to be implemented by the list item classes is required. But, as I also want this list box to be able to handle just about anything (anything that is a Control at least), the list items must also work even if they do not implement the IExtendedListItem interface.

The example application provides three different usages of the SmoothListBox, and also provides a way of comparing them:

picture4.jpg

Album Information

The album information list item, the Album class, implements IExtendedListItem to allow the list items to display different information when selected. It also renders different background colors depending on whether the item is on an even or odd row in the list box.

#region IExtendedListItem Members

public void SelectedChanged(bool isSelected)
{
    if (isSelected)
    {
        Height = 72;
        albumArtPicture.Size = new Size(64, 64);
        artist.Visible = true;
        releaseYear.Visible = true;
    }
    else
    {
        Height = 40;
        albumArtPicture.Size = new Size(32, 32);
        artist.Visible = false;
        releaseYear.Visible = false;
    }
}

public void PositionChanged(int index)
{
    if ((index & 1) == 0)
        BackColor = SystemColors.Control;
    else
        BackColor = SystemColors.ControlLight;
}

#endregion

This list item changes its size when selected, but the SmoothListBox is smart enough to re-layout its list items when a selection or de-selection occurs so it is handled neatly and automatically for the user.

BigPanel

In order to show the versatility of the SmoothListBox, one example scenario is using a class called BigPanel as a single list item. BigPanel is just a panel containing a lot of Controls, and by adding such a panel to a SmoothListBox, we get a new way of scrolling UIs that are bigger than the screen area. A way that is more intuitive than the scroll bar one gets when AutoScroll is set to true.

Conclusion

All three requirements were implemented, but I would have liked the performance to be a bit better, offering even smoother scrolling. As always, any comments on the article or the code are most welcome.

Points of Interest

It's quite difficult to get a list to scroll smoothly; this is largely because of the limited CPU power of the Mobile Devices. But, also because I wanted a list box that is really easy to use, I couldn't make use of any native drawing methods, such as GDI.

History

  • 2008-03-02: First version.
  • 2008-06-12: Added key control.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Fredrik Bornander
Software Developer (Senior)
Sweden Sweden
Article videos
Oakmead Apps Android Games
 
21 Feb 2014: Best VB.NET Article of January 2014 - Second Prize
18 Oct 2013: Best VB.NET article of September 2013
23 Jun 2012: Best C++ article of May 2012
20 Apr 2012: Best VB.NET article of March 2012
22 Feb 2010: Best overall article of January 2010
22 Feb 2010: Best C# article of January 2010
Follow on   Google+   LinkedIn

Comments and Discussions

 
AnswerRe: Great Control but a little Problem... Pinmemberdavid_acr2-Dec-11 3:26 
GeneralRe: Great Control but a little Problem... PinmemberFredrik Bornander2-Dec-11 6:49 
QuestionA problem when list scrolling... Pinmemberzwliuxing18-Aug-11 19:04 
GeneralWithout animation Pinmembersmarty222-Feb-11 7:56 
GeneralRe: Without animation PinmemberFredrik Bornander20-May-11 1:06 
GeneralMy vote of 1 PinmemberUlterior198017-Aug-10 23:56 
QuestionRe: My vote of 1 PinmemberFredrik Bornander25-Apr-11 22:26 
GeneralStrange functioning Pinmemberarnoldino2-Aug-10 6:36 
GeneralKinetic Scroller stripped down PinmemberJayson Ragasa8-Jun-10 18:01 
GeneralControl Modified [modified] PinmemberJayson Ragasa3-Jun-10 18:59 
GeneralMouse selection problem PinmemberDavid Ondracek3-Jun-10 8:16 
GeneralHorizontal Scroll (panorama) [modified] PinmemberStringDotEmpty15-May-10 6:37 
GeneralScroll selected item to the top of the visible screen PinmemberMdnss29-Mar-10 22:00 
GeneralGreat ListBox PinmemberMdnss23-Mar-10 3:52 
AnswerRe: Great ListBox PinmemberFredrik Bornander24-Mar-10 2:24 
GeneralRe: Great ListBox PinmemberMdnss24-Mar-10 6:16 
GeneralRe: Great ListBox PinmemberMember 948515815-Oct-12 11:24 
QuestionMany thanks, but... Pinmemberaaronfc17-Feb-10 14:43 
AnswerRe: Many thanks, but... PinmemberFredrik Bornander18-Feb-10 0:44 
NewsTweaked parameters for better experience PinmemberNilzor24-Jan-10 6:36 
AnswerRe: Tweaked parameters for better experience PinmemberFredrik Bornander25-Jan-10 0:40 
GeneralRe: Tweaked parameters for better experience PinmemberNilzor25-Jan-10 4:28 
QuestionMenu Customizing Pinmembermahmoud ankeer20-Dec-09 21:19 
AnswerRe: Menu Customizing PinmemberFredrik Bornander22-Dec-09 1:08 
QuestionRe: Menu Customizing Pinmembermahmoud ankeer22-Dec-09 19:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141220.1 | Last Updated 12 Jun 2008
Article Copyright 2008 by Fredrik Bornander
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid