Module 4 Task 2: Creating Multiple Walls of Thumbnails


Launch JavaFX Media Browser Application Download NetBeans Project

Introduction

In previous sections, the tutorial demonstrated how to display media by using only one wall, which is tied to a web service API in Main.fx. In this section, the application is modified to use multiple walls to display the thumbnails of media.

The walls can be viewed in two modes:

  • Single-wall view - One wall is brought to the front and expanded to fit the Stage area.

  • Deck view - The walls are arranged so that they appear stacked, one atop another.

By default the application uses two walls, but more walls can be added.

Running the Project

The main scene of the application displays the three walls in the deck view.

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

  2. Run the project.

  3. While in the deck view mode, press the up or down arrow key to rearrange the order of the walls .

  4. Press the space bar, click on one of the walls, or click on the icon in the lower right corner of the stage to toggle between a single-wall viewmode and the deck view.

Note: You might encounter errors when trying to load or play any of the media displayed on the walls. The following are possible causes for the errors:

  • The web search results included dead links or URLs that return HTTP 404 Not Found error message.
  • The resulting media might be in a format that cannot be played.
  • A network error was encountered.

If an error was encountered, a message is displayed (refer to Photo.fx and Video.fx). The NetBeans IDE Output window, or the Java console if you're running the application from a web page, shows the cause of the error.

Architecture

This task adds one new class, the FlickAPI.fx, and modifies existing classes as shown in light blue in Figure 1.

Architecture for Module 4 Task 2 Figure 1

Keyboard Handling

Several different animation sequences handle the switching between single-wall and deck view or rotating through the deck. When the application is in the deck view mode, these animations are initiated from the following Main.fx source code snippet.

Source Code
      // Handle keyboard navigation
      onKeyPressed: function(evt: KeyEvent): Void {
          if (not playing) {
              if (evt.code == KeyCode.VK_SPACE) {
                  // The space bar changes to wall view on the front card.
                  zoomIn();
              } else if (evt.code == KeyCode.VK_UP) {
                  if (focusRotationStatus == RotateFocusStop) {
                      if (++focusIndex >= sizeof walls) {
                          focusIndex = 0;
                      }
                      focusRotationStatus = RotateFocusUp;
                      hopFocussedWall();
                  }
              } else if (evt.code == KeyCode.VK_DOWN) {
                  if (focusRotationStatus == RotateFocusStop) {
                      if (--focusIndex < 0) {
                          focusIndex = sizeof walls -1;
                      }
                      focusRotationStatus = RotateFocusDown;
                      hopFocussedWall();
                  }
              }
          }
      }

      onKeyReleased: function(evt: KeyEvent): Void {
          focusRotationStatus = RotateFocusStop;
      }

The playing variable simply prevents more than one animation from occurring at the same time since the animations might be at cross-purposes (zooming out while zooming in, for example). Notice that the focusRotationStatus variable is reset to RotateFocusStop when the key is released. This comes into play in the following Rotating Focus discussion.

When in the single-wall view mode, the keyboard handling in Wall.fx invokes the zoomOut function.

Rotating Focus

As the keyboard handling code shows, pressing and releasing the Up or Down arrow key results in a call to the hopFocussedWall function. This function causes the wall that is brought into focus to bounce and then a call to the rotateWalls function is made. Several lines of code in the hopFocussedWalls function are not actually executed. The intent of the code is to enable the user to press the Up or Down arrow key and bounce each wall in turn. When the wall that is to be brought to the front is bounced, the user could release the key and the wall would be brought to the front. But this functionality is disabled by the logic in the onKeyPressed code. Feel free to experiment.

Of interest here, though, is the rotateWalls function, which plays two animations. The first animation causes the wall in front to fade and gives a pan-down effect by bringing the walls closer together, as shown in the following code.

Source Code
    // The fade out animation timeline.
    def fadeOut = Timeline {
        keyFrames: [
            at (0s) {
                fadeOutStartValues
            },
            KeyFrame {
                time: 0.35s
                values: fadeOutEndValues

                // When the fade out animation completes the now invisible front wall
                // is placed in the back and the fade in animation is started.
                action: function(): Void {
                    frontWall.toBack();
                    frontWall.translateX = 0;
                    frontWall.translateY = wallReentryHeight;
                    fadeIn.play();
                }
            }
        ];
    };

The fadeOutStartValues and fadeOutEndValues variables are KeyValue sequences. You can refer to the Main.fx source file to see how the variables are initialized. Notice that the fade in animation is played from the action function that is executed when the fade-out animation is complete.

The second animation, fadeIn, causes the wall in front to fade in and undoes the pan-down effect by restoring the walls to their proper position in the deck.

Source Code
def fadeIn = Timeline {
    keyFrames: [
        at (0s) {
            fadeInStartValues
        },
        KeyFrame {
             time: 0.35s
             values: fadeInEndValues

             action: function(): Void {
                var index = wallOrder[0];
                delete wallOrder[0];
                insert index into wallOrder;
                --focusIndex;
                frontWall.frontWall = false;
                walls[wallOrder[0]].frontWall = true;
                if (focusIndex > 0) {
                    rotateWalls(zoom);
                } else {
                    if (zoom) {
                        zoomIn();
                    } else {
                        playing = false;
                    }
                }
            }
        }
    ];
};

Again, the fadeInStartValues and fadeInEndValues variables are KeyValue sequences. Notice that the action at the end of the timeline calls rotateWalls again. This call causes the rotateWalls animations to be played over until the desired wall comes to the front.

The zoomIn function is called if the application's view is changing from deck to single-wall. This change occurs if the user clicks the title bar of one of the walls. If the wall is not the front wall, the rotateWalls function is called to bring the wall to the front, then the zoomIn function is called.

Zooming In From Deck View to Single-Wall View

If the user presses the space bar, clicks a wall, or clicks the zoom icon in the lower right corner, the zoomIn function is called and the animations are played.

When the walls are zoomed in and out, the frame size remains constant but the thumbnails are scaled. When the application is in the deck view mode (zoomed out), the walls are moved to a staggered position and the thumbnails are scaled down 50%. When the application is in the single-wall view mode (zoomed in), the walls are all placed in the same position with the focused wall brought to the front.

Source Code
Timeline {
    keyFrames: [
        at (0s) {
            startValues
        },
        at (0.5s) {
            midValues
        },
        KeyFrame {
            time: 1.0s
            values: endValues

            // When the animation completes set the visibility and view mode on 
            // all walls.
            action: function(): Void {
                var singleWall = walls[wallOrder[0]];

                for (wall in walls where wall != singleWall) {
                    wall.visible = false;
                    wall.viewMode = Constants.WALL_VIEW_SINGLE;
                }

                singleWall.visible = true;
                singleWall.viewMode = Constants.WALL_VIEW_SINGLE;
                singleWall.wall.requestFocus();

                playing = false;
            }
        }
    ];
}.play();

The startValues, midValues, and endValues are all KeyValue sequences, as is the case with the zoom out and rotate animations. The action at the end of the animation simply sets the viewMode of each wall appropriately and ensures that the front wall has focus.

Zooming Out From the Single-Wall View to Deck View

The arrangement of the walls depends on the value of the viewMode variable. If viewMode is equal to Constants.WALL_VIEW_DECK, the function zoomOut is called. This function arranges the walls by adjusting each wall's translateX and translateY values. The zoomOut function also creates an animation that plays as the walls are being moved into position, as shown in the following code.

Source Code
Timeline {
    keyFrames: [
        at (0s) {
            startValues
        },
        at(0.5s) {
            midValues
        },
        KeyFrame {
            time: 1.0s
            values: endValues

            // When the zoom out animation completes allow the ThumbnailController 
            // to resume.
            action: function(): Void {
                playing = false;
            }
        }
    ];
}.play();

The variables startValues, midValues, and endValues are KeyValue sequences. A sequence of KeyValues is created for each wall for the translateX and translateY values and to scale the thumbnails down.

Handling Mouse Events

In order to stack the walls, it is necessary to block mouse events from the top wall from being received by the back walls. The Node API blocksMouse causes mouse events to be consumed in the current Node, but mouse events are treated somewhat differently in the deck view mode than in the single-wall view mode. In Wall.fx, then, two opaque rectangles were added: one that interecepts mouse events when the application is in deck view mode and another that intercepts mouse events in the single-wall view mode. These rectangles are wallRect and coverRect, respectively.

As shown in the following code, WallRect covers the whole scene. It sets blocksMouse to true so that it never allows mouse events to go through to other Nodes further down in the scenegraph. If the user clicks the background of the wall when single-wall view mode is in use, any open media is closed.

Source Code
def wallRect: Rectangle = Rectangle {
    x: 0
    y: 0
    width: bind maxVisibleWidth
    height: bind maxVisibleHeight

    fill: Constants.STAGE_BACKGROUND_COLOR
    strokeWidth: 2
    stroke: Constants.SCROLLCTL_COLOR

    blocksMouse: true
    smooth: false

    onMouseClicked: function(evt: MouseEvent) {
       if (viewMode == Constants.WALL_VIEW_SINGLE) {
           hideMedia();
       }
     }
};

Notice also that the smooth variable is set to false. This setting affects antialiasing, making the Rectangle paint more quickly, but with more distortion. Because wallRect is simply a solid color, though, antialiasing is not of use.

When deck view mode is in use, coverRect intercepts the mouse. Otherwise the mouse events pass through. When in the deck view mode, the mouse events are blocked from the thumbnails. If coverRect is clicked when the application is in deck view, the wall that was clicked is made the single wall.

Source Code
def coverRect: Rectangle = Rectangle {
  x: 0
  y: 0
  width: bind maxVisibleWidth
  height: bind maxVisibleHeight
  fill: Color.rgb(0, 0, 0, 0)

  blocksMouse: bind viewMode == Constants.WALL_VIEW_DECK

  onMousePressed: function(evt: MouseEvent) {
      if (viewMode == Constants.WALL_VIEW_DECK) {
          setViewMode(Constants.WALL_VIEW_SINGLE, this);
      }
   }
 };

It can be argued that only one of these rectangles is really needed, but it is the z-ordering of them that is important. Notice in the following code that wallRect is on the bottom and coverRect is on the top. So coverRect intercepts all mouse events from the other nodes when blocksMouse is true. And wallRect intercepts all mouse events that aren't intercepted by the other nodes. To use one rectangle, then, the rectangle would have to be removed and reinserted into the scene graph depending on whether the application is in single-wall view or deck view mode.

Source Code
package def wall : Group = Group {
   clip: wallRect
   content: [
       wallRect,
       thumbsGroupContainer,
       titleBar,
       searchTB,
       scrollCtl,
       coverRect
   ]
}

Performance Considerations

With a single wall, the greatest impact on performance is fetching the feed data from web services and the subsequent loading of the thumbnails. In Module 3 Task 3: Getting and Loading More Data, a load-limiting mechanism was introduced that ensures only thumbnails that are visible, or in the immediate neighborhood, are loaded.

With multiple walls, determining which thumbnails are visible becomes a little more complex as a wall can be fully or partially hidden behind another wall. A wall is partially hidden if the wall is not the front wall when the deck view is in use. A wall is fully hidden if the wall is not the front wall in the single-wall view.

If the wall is the front wall, the thumbs are loaded normally. If the wall is not the front wall, but is partially visible, then only the thumbnails that are visible will be loaded. Otherwise, the wall is not visible at all and no thumbnails are loaded.

The function resetLoadLimits in Wall.fx is the block of code that handles this logic and it works in conjunction with the ThumbnailController to load or unload thumbnails. It is called when the MetaData for the wall is first loaded, when the view mode changes (from single to multiple walls), and when the Boolean variable frontWall changes. The following source code shows the pseudo-code for the resetLoadLimits function.

Source Code
if (viewMode == Constants.WALL_VIEW_SINGLE) {
    if (frontWall) {
       // Queue the visible thumbs for priority loading.
       // Unload thumbs no longer in the load range.
       // Load a page of thumbs on each side of the visible range.
    } else {
       // The wall is obscured. Unload the thumbnails.
    }
} else {
    // Deck view
    if (frontWall) {
       // Queue the visible thumbs for priority loading.
    } else {
       // The wall is partially obscured.
       // Load top row of thumbnails 
       // Unload the thumbnails that are not visible.
    }
}

Because the ThumbnailController is used to marshall the loading of images, the walls must share a single ThumbnailController. In Wall.fx, thumbnailController is defined at the script level, as shown in the following code snippet. You do not need to synchronize access to thumbnailController between the walls, as all calls to the thumbnailController come from the event dispatch thread.

Source Code
package def thumbnailController = ThumbnailController{};

Try It

Add a fourth wall of images by making the following modifications:

  1. In Main.fx, create a another class to parse Flickr Image Search Web Service for video data. Add the following source code after the import section at the top of the source file, where all the other WebSearch classes are defined.
Source Code
/**
 * This Class is used to parse video data from Flickr Image Search Web Service.
 */
def flickrImages : WebSearch = FlickrAPI {
    searchQuery: "cars"
    mediaType: MetaData.type_video
}

  1. Create another Wall with its own WebSearch object and make it the last wall to be displayed. Add the following source code after the third wall is defined. Ensure that you set the third wall's frontWall and visible variables to false.
Source Code
Wall {
        id: "Wall flickrImages"
        webSearch: bind flickrImages
        fullView: showMedia
        hideMedia: hideMedia
        isMediaShowing: bind ( media != null )
        setViewMode: setViewMode
        maxVisibleWidth: bind stage.scene.width
        maxVisibleHeight: bind stage.scene.height
        viewMode: Constants.WALL_VIEW_SINGLE
        frontWall: true
        visible: true
    }
  1. Update the wallOrder variable to include the fourth wall.
Source Code
/**
 * The current Z order of the walls.
 */
var wallOrder:Integer[] = [3, 2, 1, 0];
  1. Add the line of code to start off the default search for flickrImages.
Source Code
flickrImages.search();
  1. Build and run the project.

Rate This Article
Discussion

We welcome your participation in our community. Please keep your comments civil and on point. You may optionally provide your email address to be notified of replies—your information is not used for any other purpose. By submitting a comment, you agree to these Terms of Use.

 

English
日本語
한국어
简体中文
русский
Português do Brasil