Module 2 Task 2: Adding a Scroll Controller Feature
- Highlights
-
- Draw a scroll controller feature using the
javafx.sceneclass. - Add mouse events that let you control the scrolling behavior.
- Develop the math to calculate the
scrollOffset, which determines when the columns of thumbnails shift horizontally
- Draw a scroll controller feature using the
Introduction
This task introduces a scroll controller that displays and allows the
user to select the subset of thumbnails viewed on a wall. The scroll
controller was developed by creating a custom ScrollControl
component. The scroll controller elevator is a representation of the
available content. It consists of a set of vertical ridges with one
ridge for each column of thumbnails on the wall. You click on the
elevator to scroll the wall to the desired position. A translucent
rectangle overlays the elevator highlighting the columns currently
being displayed. The elevator does not move but resizes as the amount
of available content changes. You drag the overlay rectangle to scroll
the wall.
Running the Project
- Download the Module 2 Task 2 NetBeans project and open it in the NetBeans.
- Run the project.
- Click the scroll controller and watch the wall of thumbnails scroll horizontally.
Figure 1 shows the architecture for this task.
Figure 1
Building the Scroll Controller Functionality
The scroll controller functionality (Figure 2) is built in ScrollControl.fx and is integrated in Wall.fx.
ScrollControl.fx determines the scroll controller elevator size based on the number of thumbnail columns. ScrollControl.fx determines the scroll controller behavior based on the mouse clicks and drag.
Wall.fx controls the visibility of the scroll controller by binding its visibility flag to (fractionDisplayed < 1). The wall is often wider than the stage, fractionDisplayed is the fraction of the wall that's visible.
The position of the wall is connected to the scroll controller by binding the scrollPosition variable to the controller's position variable. Binding scrollPosition
to the scroll control's position variable with inverse allows both the
scroll controller to position the wall and wall to position the scroll
controller.
Both classes work together to create the motion of the overlay, which is constrained to the extent of the elevator. Its position is represented at the left end, as shown in the image below. The range of motion of the slider is:
0 <= po <= 1 where 1 represents (elevator width - overlay width)
Figure 2
Similarly the motion of the wall is constrained so that the entire window is filled with thumbnails. The range of motion of the wall is:
0 <= pw <= 1 where 1 represents (wall width - stage width)
Figure 3 shows that when the overlay reaches the far right end, the wall is scrolled all of the way to the left. The wall and elevator motions are constrained so that the window is always filled with thumbnails.
Figure 3
The scroll controller functionality is comprised of four main elements:
- Scroll controller graphics
- Elevator ridges
- Mouse controls
- Mathematical calculations.
Scroll Controller Graphics
The scroll controller (Figure 4) is drawn in ScrollControl.fx using the javafx.scene.paint and javafx.scene.rectangle classes.
Figure 4
The code sample includes the facade behind the elevator, the horizontal overlay, and the elevator ridges.
protected override function create(): Node {
Group {
content: [
// The translucent facade behind the elevator.
Rectangle {
x: 0,
y: 0,
width: bind width,
height: Constants.SCROLLCTL_HEIGHT,
// Make the facade translucent.
opacity: 0.2
// Fade the facade from top to bottom.
fill: LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
proportional: true
stops: [
Stop { offset: 0.0 color: Constants.SCROLLCTL_COLOR },
Stop { offset: 1.0 color: Color.BLACK }
]
}
// Draw a soft outline.
stroke: Constants.SCROLLCTL_COLOR;
},
// The elevator.
Elevator {
scrollCtl: this
position: bind position
}
];
}
}
}
Elevator Ridges
Figure 5
The elevator (Figure 5) of the scroll controller contains vertical
ridges that correspond to the number of thumbnail columns that are
displayed in the wall. The number is calculated in the ScrollControl.fx class:
// The minimum necessary change in the sequence is made when the
// desired number of ridges is changed.
if (columns > numRidges) {
insert for (k in [numRidges..columns-1]) {
Rectangle {
// Position the ridge based on its index.
translateX: ridgeWidth * k
// The ridge's size and spacing are based on the
// scrollCtl's height.
x: ridgeWidth * 0.3
y: Constants.SCROLLCTL_HEIGHT * 0.25
width: ridgeWidth * 0.4
height: Constants.SCROLLCTL_HEIGHT * 0.5
arcWidth: ridgeWidth * 0.3
arcHeight: ridgeWidth * 0.3
fill: Constants.SCROLLCTL_COLOR
opacity: 0.5
scaleY: bind ridgeScaleFunc(k)
// Make sure mouse events get through to the underlying
// rectangle when the user clicks on the elevator.
blocksMouse: false
}
} into elevatorContent;
} else {
delete elevatorContent[columns+2..numRidges+1];
}
numRidges = columns;
In the code sample the ridge indices start at 2 because elements 0 and 1 are the clickable rectangle that underlies the whole elevator and the overlay.
Mouse Controls
Clicking on the elevator causes the wall and elevator overlay to center on that position. The elevator overlay can also be dragged to any position. The mouse event relationship to the overlay centering animation is that the animation is triggered on mouse release.
The mouse controls are included in ScrollControl.fx.
There are four mouse events: onMouseClicked, onMousePressed, onMouseDragged, and onMouseReleased. The fundamental mouse event for this task is the onMouseClicked function. The other mouse events relate to the animation that makes the scroll controller overlay re-center after it is moved.
Mathematical Calculations
The key to moving the wall horizontally is by setting translateX of the thumbs in Wall.fx
whenever the elevator moves. thumbs is a var but is used as def instead
of var because thumbs is not really a variable. Once thumbs is
initialized in the assignment statement, its value is not changed. The
following code sample from Wall.fx shows the thumbs definition:
def thumbs : Group = Group {
content: thumbnails,
translateX: bind scrollOffset,
translateY: bind (maxVisibleHeight -
Constants.SCROLLCTL_HEIGHT -
thumbs.boundsInLocal.height) / 2
scrollOffset is simply the amount to translate the wall horizontally based on the scrollPosition variable. The bind formula maps the range of scrollPosition (0-1) to the posible range of motion of the wall in pixels.
def scrollOffset : Number = bind -scrollPosition * (thumbsGroupWidth - maxVisibleWidth);
The scrollPosition comes from a variable with a value between 0 and 1 that represents how much to move the wall. The minus sign (-) that precedes scrollPosition moves the wall of thumbnails in the opposite direction from the scroll controller movement.
scrollPosition is maintained by the scrollGallery function. This function is assigned to the scroll function variable ScrollControl. This function assignment allows the ScrollControl to request the Wall to scroll itself with no knowledge of Wall variables. as shown in the following code from Wall.fx:
def scrollCtl = ScrollControl {
translateX : Constants.SCROLLCTL_RESERVE
translateY : bind maxVisibleHeight - Constants.SCROLLCTL_HEIGHT
width : bind maxVisibleWidth - 2 * Constants.SCROLLCTL_RESERVE
position : bind scrollPosition with inverse
fractionDisplayed : bind fractionDisplayed
columns : bind wallColumns()
visible: bind fractionDisplayed < 1.0
}
fractionDisplayed is how much wall is visible in the stage. Fewer thumbnails means fewer elevator ridges. If the fractionDisplayed is less than 1, there are images that are hidden that are available. fractionDisplayed
is based on wall width and a calculation of stage width divided by wall
width. Wall width is a calculation based on how many columns of
thumbnails you have. If the fraction displayed is less than 1, then you
need to create the ScrollControl. The ScrollControl is only created when it is needed.
ScrollControl is the class that defines the appearance and behavior of the scroll controller.
The rectangle behind the ridges is there to handle mouse clicks. When a mouse event happens, the scroll controller re-centers itself by translating the x value during the duration of the animation, which lasts 1.5 seconds.
Try It
Creating Jump Scrolling
The scroll controller does smooth scrolling. You can make the scroll controller do jump-scrolling by making the wall move in steps. To do this, you modify how much the wall scrolls with each step. For example, you can make the wall scroll one quarter of the width of a thumbnail. You need to keep track of how far the mouse has moved to make this work.
Scrolling Vertically
Another option is to change the scroll controller so it scrolls
vertically instead of horizontally. Look for every instance of height
and width and make sure they are bound to a variable. Once the wall
moves vertically, then you can alter the wall to position the thumbs as
3 columns by n rows. The fractionDisplayed calculation has to use the height instead of the width and scrollGallery has to use translateY instead of translateX.
