Module 2 Task 2: Adding a Scroll Controller Feature


Launch JavaFX Media Browser Application Download NetBeans Project

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

  1. Download the Module 2 Task 2 NetBeans project and open it in the NetBeans.

  2. Run the project.

  3. Click the scroll controller and watch the wall of thumbnails scroll horizontally.

Figure 1 shows the architecture for this task.

Architecture for Module 2 Task 2 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)

Scrollbar in default position 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.

Scrollbar in far right position 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.

Scroll controller parts with callouts including facade, elevator ridge, and horizontal overlay Figure 4

The code sample includes the facade behind the elevator, the horizontal overlay, and the elevator ridges.

Source Code
    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

Elevator ridges in scroll controller that represent each column of images 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:

Source Code
      // 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:

Source Code
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.

Source Code
  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:

Source Code
  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.