Animation blueprints for SWT – scrolling layout
After adding such animation effects as fading, load progress and asynchronous load of images in the application window connected to the Amazon backend, it’s time to talk about smooth scrolling of the display results. In this entry i’m going to talk about loading the album art matching the specific search string and asynchronous display of the associated images. This code is part of the Granite project which aims to provide blueprints for animated SWT applications powered by the Trident animation library.
Here is a screenshot that illustrates the search results displayed as album art (and you can view the videos in the first part of this series):
The album items can be scrolled with right / left arrow keys, as well as by using the mouse wheel.
The base implementation of the layout does not use animations on the scrolling, but lays out the groundwork. It starts off by having the list of all album items, and the position of the currently leading item:
public class Stage2Components extends Stage1LoadingProgress {
/**
* The list of album overview components. Each component added with
* {@link #addOverviewComp(Item, ActivationCallback)} is added to this list.
*/
List comps;
/**
* Indicates which album overview component is displayed at the left edge of
* this container. Note that while this specific class (in its
* {@link #scrollToNext()} and {@link #scrollToPrevious()}) operate on the
* integer values, the animated scrolling will result in fractional values
* of the leading position.
*
* At the beginning the value is 0.0 - displaying the first entry in
* {@link #comps} at the left edge. When scrolling to the next album, the
* value will become 1.0 (effectively pushing the first album over the left
* edge). If the scrolling is animated, this value will be gradually
* interpolated from 0.0 to 1.0.
*
* This value is respected in the layout manager to provide the seamless
* scroll animation.
*
*/
float leadingPosition;
/**
* Contains all the album components.
*/
Composite albumComponentsHolder;
In this class, the leadingPosition
will only have discrete integer values – immediate scrolling. However, we define is as float
for the animation purposes (see below).
The constructor of this class creates the list for holding the album items, as well as registering relevant mouse wheel listener, input map and action map:
/**
* Creates the new container that can host album overview components.
*
* @param composite
* Parent composite.
*/
public Stage2Components(Composite composite) {
super(composite);
this.comps = new ArrayList();
this.albumComponentsHolder = new Composite(this, SWT.TRANSPARENT);
this.progressIndicator.moveAbove(this.albumComponentsHolder);
// register the mouse wheel listener for scrolling content
composite.addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseScrolled(MouseEvent e) {
if (e.count < 0) {
// next
scrollToNext();
} else {
// previous
scrollToPrevious();
}
}
});
// use arrow keys for scrolling
composite.getDisplay().addFilter(SWT.KeyUp, new Listener() {
@Override
public void handleEvent(Event event) {
// only scroll when the details window is not showing
if (event.keyCode == SWT.ARROW_RIGHT) {
scrollToNext();
}
if (event.keyCode == SWT.ARROW_LEFT) {
scrollToPrevious();
}
}
});
}
Next, a method to add a single album to this container:
/**
* Adds the specified album item to this album container.
*
* @param albumItem
* Description of the album item from the Amazon backend.
* @return Thew matching album overview component.
*/
public synchronized AlbumOverviewComponent addAlbumItem(Item albumItem) {
AlbumOverviewComponent comp = new AlbumOverviewComponent(
this.albumComponentsHolder, albumItem);
this.comps.add(comp);
this.albumComponentsHolder.layout(new Control[] { comp });
return comp;
}
And the implementation of discrete scrolling:
/**
* Scrolls the albums to show the next album.
*/
protected void scrollToNext() {
if (this.leadingPosition < (this.comps.size() - 1)) {
this.leadingPosition++;
this.albumComponentsHolder.layout(true);
}
}
/**
* Scrolls the albums to show the previous album.
*/
protected void scrollToPrevious() {
if (this.leadingPosition > 0) {
this.leadingPosition--;
this.albumComponentsHolder.layout(true);
}
}
The layout itself (triggered by the calls to layout() above) is quite simple – computing the X position of each album item based on the current leadingPosition:
// set custom layout manager to arrange the row of album
// components
this.albumComponentsHolder.setLayout(new Layout() {
@Override
protected Point computeSize(Composite composite, int wHint,
int hHint, boolean flushCache) {
return new Point(wHint, hHint);
}
@Override
protected void layout(Composite composite, boolean flushCache) {
if (comps.size() == 0)
return;
for (int i = 0; i < comps.size(); i++) {
float delta = i - leadingPosition;
// compute the left X based on the current leading position
int x = (int) (delta * (AlbumOverviewComponent.DEFAULT_WIDTH + 10));
comps
.get(i)
.setBounds(
x,
(getBounds().height - AlbumOverviewComponent.DEFAULT_HEIGHT) / 2,
AlbumOverviewComponent.DEFAULT_WIDTH,
AlbumOverviewComponent.DEFAULT_HEIGHT);
}
}
});
Now it’s time to add the scrolling animation to the mix. With the groundwork laid out already, the implementation is quite simple.
public class Stage3AnimatedScrolling extends Stage2Components {
/**
* Contains the target leading position - this is the index of the album
* which should appear at the left edge once the current
* {@link #scrollTimeline} is done. Note that the user scrolling can be done
* in the middle of the current scrolling animation. In this case, the field
* is updated with the new target index.
*/
float targetLeadingPosition;
/**
* The scroll timeline. Note that the user scrolling can be done in the
* middle of the current scrolling animation. In this case, the current
* timeline is cancelled, and a new one is created - this allows quick
* scrolling with multiple mouse wheel / arrow events.
*/
Timeline scrollTimeline;
The targetLeadingPosition allows us to handle multiple consecutive scrolling requests (user quickly scrolling the mouse wheel) without having multiple timelines “competing” to scroll the album items.
The constructor doesn’t do anything special:
/**
* Creates the new container that can animate the album scrolling.
*
* @param composite
* Parent composite.
*/
public Stage3AnimatedScrolling(Composite composite) {
super(composite);
this.targetLeadingPosition = 0;
}
Now we get to the “meat” of this class – adding the scrolling animations. First, we override the scrolling methods from the super class to update the target position:
@Override
protected void scrollToNext() {
if (this.targetLeadingPosition < (this.comps.size() - 1)) {
this.targetLeadingPosition++;
scrollContents();
}
}
@Override
protected void scrollToPrevious() {
if (this.targetLeadingPosition > 0) {
this.targetLeadingPosition--;
scrollContents();
}
}
Where the scrollContents aborts the existing timeline (if necessary), and plays a new one:
/**
* Scrolls the contents of this container.
*/
private synchronized void scrollContents() {
if (this.scrollTimeline != null) {
// abort the playing scroll timeline
this.scrollTimeline.abort();
}
// and dynamically create a new one to change the
// leading position
this.scrollTimeline = new Timeline(this);
this.scrollTimeline
.addPropertyToInterpolate(Timeline. property(
"leadingPosition").fromCurrent().to(
this.targetLeadingPosition));
this.scrollTimeline.setDuration(250);
this.scrollTimeline.play();
}
Canceling the old timeline makes sure that we will not have multiple timeline updating the leadingPosition
field. And the targetLeadingPosition
field holds the final value of the leadingPosition
– or at least until the user makes an additional scrolling request.
Finally, we have a public setter and getter so that the Trident engine can change the value of the leadingPosition
field:
/**
* Sets the new value for the leading position. This is called from
* {@link #scrollTimeline}.
*
* @param leadingPosition
* The new value for the leading position.
*/
public void setLeadingPosition(float leadingPosition) {
this.leadingPosition = leadingPosition;
this.albumComponentsHolder.layout(true);
}
/**
* Returns the current value of the leading position. This is called from
* {@link #scrollTimeline}.
*
* @return The current value of the leading position.
*/
public float getLeadingPosition() {
return this.leadingPosition;
}
Here we have seen how to scroll the album covers showed in the container and how to add animations to the scrolling. The next entry is going to talk about displaying larger album art and scrollable track listing when the specific album is selected.