Module 4 Task 1: Implementing Flick Scrolling
- Highlights
-
- Handle the mouse events
- Activate bounce animation
- Tune the flick animation
Introduction
Module 4 task 1 probes further into the capabilities of mouse events
and animation by using a physics engine to add flick scrolling to the
scroll control functionality that was introduced in Module 2 Task 2 in
the ScrollControl.fx node. Before you complete the procedures in the current task, you should review the scroll control function in Module 2 Task 2.
Flick scrolling works by transforming a mouse drag into a scroll animation when the mouse is released. The animation updates the scroll control elevator's position, thereby moving the wall of thumbnail images. If the wall of images scrolls past the end of the thumbnail array, the bounce animation effect occurs.
Running the Project
- Download the Module 4 Task 1, NetBeans project and open the NetBeans IDE.
- Run the project.
Figure 1
Architecture
This task adds flick scrolling functionality to ScrollControl.fx.
Figure 2
Handling the Mouse Events
Flick scrolling is initiated by a drag gesture, that is, click the left mouse button, hold it, drag it and then release it. When the mouse button is released, if the gesture had sufficient velocity, the flick animation is initiated. Otherwise, the drag gesture simply scrolls the thumbnails of the wall.
The local variables lastDragTime, lastDragX and dragSpeed are used to monitor the mouse motion throughout the drag gesture. Three functions are involved: onMousePressed, onMouseDragged and onMouseReleased. These variables are initialized in onMousePressed. They are updated in onMouseDragged as the user drags the mouse. Finally, they are evaluated in onMouseReleased to determine whether to play the animation.
The onMouseDragged function, shown in the following source code sample, is invoked as the user drags the mouse. The dragX value of the MouseEvent function is a delta from the previous value. Small movements of the mouse do not cause the wall to scroll, so the dragX value of the MouseEvent must exceed a certain threshold.
The calculation of dragSpeed is simply the distance the mouse moved, divided by the duration of the movement Note that distance = rate * time.
onMouseDragged: function(evt: MouseEvent): Void {
var dragTime = Duration.valueOf(java.lang.System.currentTimeMillis());
if (java.lang.Math.abs(evt.dragX) > Constants.FLICK_DRAG_THRESHOLD) {
// Set the elevator's dragging flag to prevent its
// overlay from recentering on every mouse event.
dragging = true;
position = posAnchor - evt.dragX / (contentWidth - windowWidth);
// Monitor the dragging speed.
dragSpeed = (evt.dragX - lastDragX) /
dragTime.sub(lastDragTime).toMillis();
if (dragSpeed > Constants.FLICK_MAX_SPEED) {
dragSpeed = Constants.FLICK_MAX_SPEED;
} else if (dragSpeed < -Constants.FLICK_MAX_SPEED) {
dragSpeed = -Constants.FLICK_MAX_SPEED;
}
lastDragTime = dragTime;
lastDragX = evt.dragX;
}
}
The onMouseReleased function is more complex. When the mouse button is released, onMouseReleased determines whether the flick animation should be played based on these conditions:
Did the user drag the mouse more than
Constants.FLICK_DRAG_THRESHOLDvalue? TheFLICK_DRAG_THRESHOLDis the minimum distance in pixels the mouse must be dragged to trigger scrolling.Did the user drag the wall outside of its normal scrolling limits?
Will the mouse drag speed keep the wall in motion longer than the
Constants.FLICK_TIME_THRESHOLDvalue? TheFLICK_TIME_THRESHOLDis the minimum time that the animation takes to play.
If these conditions are met, a Timeline is created to animate the
wall scrolling to its new position. The Timeline interpolates the value
of flickOffset. When the value of flickOffset changes, the position variable is updated. Because position is bound to Wall.scrollPosition with inverse, updating position causes the wall to scroll. See the Mathematical Calculations section of Module 2 Task 2.
flickTimeline = Timeline {
keyFrames: [
at (0s) {
flickOffset => scrollOffset tween flickInterpolator
},
KeyFrame {
time: Duration.valueOf(restTime)
action: bounce
values: [
flickOffset => restOffset tween flickInterpolator
]
}
];
};
The interpolator is not a javafx.animation.Interpolator, but a custom FlickInterpolator that provides a constant accleration motion formula for the animation. The FlickInterpolator extends javafx.animation.Interpolator and overrides the interpolate function. The interpolate
function is called repeatedly over the duration of a KeyFrame to
effectively create a sequence of values between the start value and end
value for the given KeyFrame.
The fraction argument represents time as a fraction of the duration of the KeyFrame. For example, if the value of fraction is .5, half of the KeyFrame's duration has elapsed. The startValue is the value at the start of the KeyFrame. The contract of the interpolate function is such that the function returns a value between startValue and endValue.
The interpolate function determines an initial velocity
of the wall that causes it to come to rest at the scroll limit using
the constant acceleration formula:
x = x0 + v0 * t + a * t^2 / 2.
So, v0 = (x0 - x + a * t^2 / 2) / t, where x0 is the initial position past the end, x is the position of the scroll limit, a is the constant acceleration, and t is the constant amount of time to bounce back.
class FlickInterpolator extends Interpolator {
// Initial position
var x0: Number;
// Initial velocity
var v0: Number;
// The time at which the object comes to rest.
var t1: Number;
override function interpolate(startValue: java.lang.Object,
endValue: java.lang.Object,fraction: Number) :
java.lang.Object {
// The sign of the acceleration parameter is based on the
// direction of motion.
var a : Number = bind if (v0 > 0) {
Constants.FLICK_ACCELERATION;
} else {
-Constants.FLICK_ACCELERATION;
}
// The interpolation always begins at t = 0 so
// t = t0 + (t1 - t0) * fraction becomes
// t = t1 * fraction
var t = fraction * t1;
// Apply the motion formula for constant acceleration.
return x0 + v0 * t + a * t * t / 2;
}
}
Bounce Animation
The bounce animation is played if either the flick animation moves
or the user drags the wall past the end of the thumbnail array. The
bounce animation is a simplification that uses the FlickInterpolator to move the wall back to the scrolling limit in a constant amount of time.
var restOffset = 0.0;
var a = -Constants.FLICK_ACCELERATION;
if (scrollOffset <= 0) {
restOffset = -(contentWidth - windowWidth);
a *= -1;
}
// Compute the initial rebound velocity based on the position and
// the defined bounce duration.
var v0 = (restOffset - scrollOffset -
a * Constants.FLICK_BOUNCE_TIME * Constants.FLICK_BOUNCE_TIME / 2) /
Constants.FLICK_BOUNCE_TIME;
// Create a motion formula interpolator for the bounce animation.
var flickInterpolator = FlickInterpolator {
x0: scrollOffset
v0: v0
t1: Constants.FLICK_BOUNCE_TIME;
}
Tuning the Flick Animation
The variables listed in the following table have been added to Constants.fx and can be used to tune the flick animation behavior.
| Constant | Meaning | Value |
|---|---|---|
FLICK_DRAG_THRESHOLD |
The minimum distance in pixels that the mouse has to be dragged to trigger scrolling. | 5 pixels |
FLICK_TIME_THRESHOLD |
The minimum duration of the flick animation. Less than this and the animation is too brief to have a visible impact. | 500 milliseconds |
FLICK_ACCELERATION |
The scrolling acceleration after a flick in pixels / ms / ms. In the physics of flick, this value (being negative) represents friction. | - 0.00035 pixels / ms / ms |
FLICK_MAX_SPEED |
This maximum limits the initial velocity so that the wall stops scrolling within (3500) ms. | - FLICK_ACCELERATION * 3500 |
FLICK_BOUNCE_LIMIT |
The distance in pixels that a flick animation can overshoot. | 100 pixels |
FLICK_BOUNCE_TIME |
The time in milliseconds that it takes to bounce (scroll back) the wall on an overshoot. | 750 milliseconds |
