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 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 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:
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 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.
The fick works in the opposite direction.
2007-10-10 12:19:49 weolopez
[Reply | View]
On an iPhone flicking from top to bottom scrolls down. The example scrolls up.
That is one of the intuitive subtleties that is part of the genius of the touch interface.
The fick works in the opposite direction.
2007-10-11 01:11:55 rah003
[Reply | View]
Could you provide more details about the platfrom (java version, os, ...) on which you ran into this problem? So far it ran properly everywhere I was able to test it. If you look in the video which was created running the exactly same demo you see that flicking from top to bottom triggers indeed scrolling down and not up.
The fick works in the opposite direction.
2007-10-15 11:03:21 weolopez
[Reply | View]
My mistake on the iPhone flicking top to bottom scrolls up. The demo here flicking top to bottom scrolls down.
The fick works in the opposite direction.
2007-10-16 01:17:48 akarydas
[Reply | View]
As I see it there are two options. You can either fling the current selection or the list itself.
The example here flings the current selection. As a result, when scrolling is requiredit is in the same direction as the fling gesture.
The other option is flinging the list itself. That is what my example (and the iPhone) does. In this case scrolling would be on the opposite direction of the fling gesture. IMHO, this second approach is far more intuitive
While we are at it, here is a simple class a created long ago (when the iPhone came out :-) to immitate it's fling scrolling. It does not need the TimingFramework and is capable of smoothly scrolling any JScrollPane. To use it with a particular scroll pane simply create a FlingScroller passing the scroll pane as argument. All the rest are handled automatically by the class. NOTE that this was created for demonstration properties and it is only capable of fling scrolling on the vertical direction.
It should not be very difficult though to extend it.
/**
*
*/
package flingscroller;
It also naturally handles the inconsistencies kirillcool pointed out since it estimates the current velocity of the mouse during the gesture and it uses this as the initial velocity when the mouse is released.
It's more less the same code as in the article itself. It doesn't suffer from problem Kirill pointed out because it handles movement in mouseDragged as I've suggested in response to him. The use of timing framework although not mandatory makes it simpler to plug in any kind of animation and gives you more freedom to chain in additional effects, behavior (acceleration, deceleration) or switch underlying timer without any changes to your code.
The main point here was to show that there is something you can do to make your applications not only to look better but to also feel better, which is what you already do. Congratulation. :)
Well, i tried to make it feel better (smooth scrolling of the scroll pane's view position, calculation of the instantaneous speed, sensible acceleration). But does it actually feel better ??? I must admit that while fling scrolling feels so very natural with a touch physical interface (it sort of gives weight and inertia to objects you can 'touch' ) it is a bit awkward with the mouse. I would really appreciate your thoughts on this.
(NOTE that there is at least one more feature that needs to be implemented to make it feel more like the iPhone. However I was kind of let down with the overall feel of it and I did not went for it)
I've mentioned is several times at the begining of the article - this feature makes sense only if majority of users of your application are mobile (using touchpad) or using some other touch enabled devices (like gesture pads). However what I've noticed was that non mobile users (or mobile ones with mouse connected to their notebooks) most of the time failed to notice there was any extra scrolling gesture enabled. So in my experience, this feature adds benefit for mobile users without making situation worse for desktop users. Whether it is worth adding into your or anybody elses application depends on target audience and type of the app and have to be decided on case by case basis. That could be the reason why not that many people tried to add something like that to regular java apps. After all majority of users are still using mouse.
Great article and simple implementation. However, there is some issue with the "sensitivity threshold" of triggering the scroll. Press the mouse, move down a few elements, hold for a second and then release the mouse. The selection will scroll down when the mouse is released - this is not really natural continuation of the user interaction since the mouse stopped moving for an entire second before being released. Perhaps a little more fune-tuned mouse listener is in order.
True. Thanks for pointing that out. To do what you suggest one has to check for incremental changes in the distance over time using mouseDragged events rather then simple total distance and total time elapsed between mousePressed and mouseReleased. This is what you would want to look into for production code, but it seemed unnecesarily complicated for just showing the effect in the article.