Skip to main content

Fling Scroller

September 27, 2007

{cs.r.title}






Writing an application that is appealing to the user is not an
easy task. While there are many cool and easy-to-use applications
out there, there are even more applications that
users are not very happy with. Part of the problem is that the
target audience keeps changing, continuously bringing new
challenges and always leaving space for improvement. What this
article tries to show you will not transform any application into a
glamorous super tool, but will definitively ease the pain of using
it, especially for mobile users. The change we will discuss here is
subtle and easy to miss in the application when used from the
desktop; however, its benefits become more apparent when used from
notebooks via touchpad or from touchscreens. Since the use of these
devices increases every day, the importance of making applications easy
to use for users of such devices increases as well.

What is this mysterious feature that will appeal to your
mobile users? It's an enhancement to the default scrolling
functionality. Let's have a closer look at lists, which seem to be
the most common case of scrollable components in applications. If
you have tried scrolling through a moderately long list of items in
any application using the touchpad on your notebook, you know it can
be a real pain.

Recently I watched the video showing the "http://blogs.sun.com/jag/entry/the_green_ui">GreenUI (posted
by James Gosling on his blog). He made one very interesting point
in the video: "Everything is about scrollbars." GreenUI had built-in support for gestures used heavily when scrolling through lists
of items. This was exactly the kind of thing that made scrolling
easy. What does such gesture support look like? Imagine
for a moment the list is actually a big wheel with all its items
written on the outside of it, one by one in regular intervals. The
supported gesture for scrolling is then what looks like an attempt
to spin such a big wheel. How do you spin a wheel, you ask? You
grab one of the items and drag it in direction in which you want
the wheel to spin. And the wheel will keep spinning for a while
even after you have released it. The speed and duration of such
spinning depends on how vigorously you spun the wheel.

Imagine this functionality in the context of a long list and
a touchpad. First, you don't have to navigate to the scrollbar on a
side of the list and second, if the list is a bit longer, you just
spin it and it will continue rolling for a while, even after your
finger has run off the touchpad itself. In recent testing, users
not only think it is useful, they think it's cool, and keep spinning
the lists subconsciously while thinking about something else with
the application open.

If you skip to the Resources section right now,
you can watch a short video showing the final effect.

Let's have a look now at how difficult is it to do the same with
state-of-the-art Swing components.

A Quick Look at JList

We will start with a simple example of the list to examine the
default behavior of JList. Then we will enhance it to
provide support for spinning, and in the end we will have a look to see if
and how the same effect can be applied to other components or
further modified to suit your application needs.

To scroll items in JList, you can either use
scrollbars or you can press the mouse button over one of the items
and move the mouse up or down. This latter behavior is the one we
will try to explore here. Built-in scrolling support of
JList, or anything wrapped in ScrollPane, for that
matter, makes it scroll as long as the mouse is pressed and moved
above/below the list. So we don't have anything to do here. The
real job starts when the mouse is released. With the default
implementation scrolling stops abruptly, and that is not exactly
what we want. What we want to do is to have the list scrolling for
while longer and then gradually slow down and eventually stop.
Since this involves timing and interpolation of time intervals
between events, we will use the "https://timingframework.dev.java.net">TimingFramework,
recently graduated to version 1.0, to make our job a bit
easier.

First, let's create a small demo to see the default behavior:


import java.awt.Dimension;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;

public class XListDemo {

  private static final String[] ITEMS = new String[] {"200",
    "AbstractAction","ActionEvent","BorderLayout","CLOSE",
    "Click","Dimension","EXIT","ITEMS","JButton","JFrame",
    "JList","JXPanel","ListModel","ON","SOUTH","String","XList",
    "actionPerformed","add","args","awt","b","class","e",
    "event","extends","f","final","import","java","javax",
    "jdesktop","l","main","me","new","org","pack","param",
    "private","public","setDefaultCloseOperation",
    "setPreferredSize","setVisible","static","swing","swingx",
    "true","void"};

  public static void main(String[] args) {
    JFrame f = new JFrame();
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JList list = new JList(ITEMS);
    f.add(new JScrollPane(list));
    f.setPreferredSize(new Dimension(200, 200));
    f.pack();
    f.setVisible(true);
  }
}

This simple application has the default scrolling behavior where
if one clicks over the item in the list and drags the mouse above
or below the list, it will start scrolling and continue scrolling
until the mouse button is released. When using a mouse, this
behavior is tolerable, but have you ever tried to scroll through
longer lists like this while using a touchpad? It is difficult to
control the scrolling due to the sensitivity and size of the
touchpad.

Adding the Motion

What we would like do next is to add a hook to the mouse events
to be able to trigger a continuation of the scrolling after the
mouse button is released. We will emulate the scrolling behavior of
JList by incrementing setSelectedIndex().
To do so we will use an Animator from the Timing
framework. For starters, we will scroll for ten more items and we
will do so within one second.


    list.addMouseListener(new MouseAdapter() {
      public void mouseReleased(MouseEvent e) {
        // create PropertySetter, which defines the object
        // (list) and property ("selectedIndex") to be changed
        // and the values that it will animate from and to
        PropertySetter ps = new PropertySetter(list,
          "selectedIndex", list.getSelectedIndex(),
          list.getSelectedIndex() + 10);
        // Create Animator, which will animate ps
        // for 1000 milliseconds
        Animator a = new Animator(1000, ps);
        // start the timer
        a.start();
      }});

This would work, but is far from perfect. The code above scrolls
only down, not up; the selected item might run off the visible area;
and the scrolling still ends abruptly after scrolling through ten
more items. We will address all of those issues in few moments.

Now let's make sure the selected item always stays visible. To
do so we will attach another TimingTarget to the Animator and tell
it to ensure the selected index of the list is visible on the
occurrence of a timing event:


        // This TimingTarget will receive all timing events generated by the
        // Animator that we defined above
        a.addTarget(new TimingTargetAdapter() {
          public void timingEvent(float fraction) {
            list.scrollRectToVisible(
            list.getCellBounds(list.getSelectedIndex(), list.getSelectedIndex()));
          }});

Smooth and Tidy

Next we will address the smoothness of the scrolling. We will
use SplineInterpolator to slow down the pace of the
animation towards the end:


        a.setInterpolator(new SplineInterpolator(0f,.02f,0f,1f));

Last but not least, we will fix the direction of scrolling and
make all of the code more robust when we scroll towards the end of
boundaries. To do this we must also listen for the start of the
scrolling and figure out the scrolling direction. We will create a
StateControl class to manage all the changes to the
state and modify MouseListener to only set up and
trigger the animation.

Here's the StateControl class:


  static class StateControl {
    private static final int TAIL = 10;
    long stopTime;
    long startTime;
    int startY;
    int startIdx;
    int stopY;
    int stopIdx;
    private Animator a;

    public void startMotion(final JList list) {
      if (a != null && a.isRunning()) {
        a.stop();
      }
      // bail out if there was no mouse movement in between
      if (startY == stopY) {
        return;
      }
      // calculate time and distance for scrolling
      int dist = Math.abs(startIdx - stopIdx);
      // bail out if there was change in item selection
      if (dist == 0) {
        return;
      }
      int tail = Math.min(TAIL, dist);
      int stopInt = startY < stopY
        ? Math.min(list.getSelectedIndex() + tail,
            list.getModel().getSize())
        : Math.max(list.getSelectedIndex() - tail, 0);
      // create property setter to change selected list item
      PropertySetter ps = new PropertySetter(list,
        "selectedIndex", list.getSelectedIndex() , stopInt);
      a = new Animator((int)((stopTime-startTime)*tail/dist),
        ps);
      // add extra target to ensure selected item stays visible.
      a.addTarget(new TimingTargetAdapter() {
        public void timingEvent(float fraction) {
          list.scrollRectToVisible(
            list.getCellBounds(list.getSelectedIndex(),
              list.getSelectedIndex()));
        }});
      // mimic slowdown at the end of scrolling
      a.setInterpolator(new SplineInterpolator(0f,.02f,0f,1f));
      // finally start the timer
      a.start();
    }
  }

Here's the change to the mouse listener:


    final StateControl state = new StateControl();
    list.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        // store the start possition
        state.startY = e.getYOnScreen();
        state.startIdx =
          ((JList) e.getSource()).getSelectedIndex();
        state.startTime = System.currentTimeMillis();
      }

      public void mouseReleased(MouseEvent e) {
        // store the end position
        state.stopY = e.getYOnScreen();
        state.stopIdx =
          ((JList) e.getSource()).getSelectedIndex();
        state.stopTime = System.currentTimeMillis();

        state.startMotion((JList)e.getSource());
      }});

Voila. Now we have a list which will stop scrolling smoothly
after the mouse button is released and should feel quite nice.
While this is not so obvious while using the mouse, this kind of
behavior can be quite handy when invoked from a touchpad or touchscreen.

If you want to try, there is a link to the Web Start version in
the Resources section.

Transferring the Effect to Other Components

The last thing to show is how to apply the same effect to other
components besides just lists. It is actually quite easy. Let's
take our example and swap JList for
JTable. There are just few differences to handling
lists that we have to take care of:

  • We have to set it to single selection only:
    
       table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
     
  • We have to change the way we obtain the selection index in
    the startMotion(JTable table) method due to differences in
    the API of lists and tables:
    
          int stopInt = startY < stopY
            ? Math.min(table.getSelectedRow() + tail,
                table.getRowCount())
            : Math.max(table.getSelectedRow() - tail, 0);
          // create property setter to change selected
          // item in the table
          PropertySetter ps =
            new PropertySetter(table.getSelectionModel(),
              "leadSelectionIndex", table.getSelectedRow() ,
              stopInt);
          a = new Animator((int)((stopTime-startTime)*tail/dist),
            ps);
          // add extra target to ensure selected item stays visible.
          a.addTarget(new TimingTargetAdapter() {
            public void timingEvent(float fraction) {
              table.scrollRectToVisible(
                table.getCellRect(
                  table.getSelectionModel().getLeadSelectionIndex(),
                  -1, true));
            }});
    

And we are done. That was an easy one, and the same holds true
for every other scrollable component. It could get more complicated
if one wants to apply it to extend the selection instead of
scrolling a single selected item/row selection, but even then the
only extra work is to track the direction in which the selection
gets extended. The complete listing of the

decorate(JTable
table)
method is in the full listing of the code and can be
obtained from the link in the Resources section.

Conclusion

As illustrated above, you don't need a huge amount of code to
implement this interface amenity, which in my experience adds a
significant deal of comfort and utility when using alternative
pointing devices such as touchpads. It's simple and can be easily
applied--so why not try it out and just maybe make the user's life
a bit easier?

The code listed in this article should be usable in other
situations, so feel free to copy it and use it in your
applications.

The code above also highlights another problem (or solution) in
user interfaces: isn't it much easier and more pleasant to work
with applications where things are changing gradually as opposed to
abrupt flashes of different screens or elements popping in or out?
With gradual change, the brain has time to adjust and keep track of
the connection between the elements and screens shown, sparing you
those moments where your eyes have to flit around to find where in
the world the application just teleported you to. You might not be
in Kansas anymore, but at least you'll know how you got there.

Resources


width="1" height="1" border="0" alt=" " />
Jan Haderka is an independent software developer and technology consultant focusing on desktop and enterprise applications.
Related Topics >> Swing   |