Module 5 Task 1: Modifying the Application for a Mobile Device


Launch JavaFX Media Browser Desktop Application Download NetBeans Project

Introduction

Module 5 of the Media Browser tutorial shows how the Media Browser application is adapted for a mobile device. To obtain the most benefit from this module, complete the Media Browser tutorial in its entirety, starting with the Media Browser Tutorial Setup page.

In this Module 5 Task 1 section, the source code from Module 4 Task 1 is modified so that the application runs on a mobile device, as well as on the desktop. The mobile version of the application retains as much of the desktop functionality as possible. One notable difference is that, because no javafx.scene.effect package is included in the common profile, no reflection effect occurs on the mobile version. Also, due to space constraints, the search text box is made accessible from the right soft key or by clicking the magnifying glass icon in the lower right corner of the phone screen.

About the Desktop and Common Profiles

The JavaFX API enables developers to create user interfaces that work seamlessly across different devices. The common profile of the JavaFX API includes classes that function on both the desktop and mobile devices. Additional classes and packages from the desktop profile can be used to take advantage of specific functionality that can enhance desktop applications. Refer to the JavaFX API for the list of classes included in the common and desktop profiles. Click either the desktop or common link at the top right corner of the page to display the corresponding profile view.

The JavaFX SDK includes the JavaFX Mobile Emulator, a mobile phone simulator, and is used in this section of the tutorial to simulate how the Media Browser application runs on a mobile device. Note that the mobile emulator is currently available for the Microsoft Windows platform only. Refer to the SDK Readme file (<SDK-install-directory>/README.html) for more information about the mobile emulator.

Running the Project

  1. Download and unzip the compressed file of Module 5 Task 1 NetBeans projects.

    The module05-task01_nb.zip file contains the tutorials folder and its subfolders, which include the three NetBeans projects described in the following table.

    Contents of the module05-task01_nb.zip File
    Path of NetBeans Project Folder NetBeans Project Name Description of Contents of Folders
    tutorials/common/mediabrowser/module05-task01 module05-task01-common NetBeans project and source for the mediabrowser package, which is the common source code used by both the desktop and mobile versions of the application.
    tutorials/desktop/mediabrowser/module05-task01 module05-task01-desktop NetBeans project and source for the mediabrowser.desktop package, which is the source code that is specific to the desktop version of the application.
    tutorials/mobile/mediabrowser/module05-task01 module05-task01-mobile NetBeans project and source for the mediabrowser.mobile package, which contains the source code specific to the mobile version of the application.
    Both the module05-task01-desktop and the module05-task01-mobile projects include source from the module05-task01-common project, but in different configurations. The module05-task01-common project is included as a library in the module05-task01-desktop project. This file organization is typical of how multiple projects are configured in the NetBeans IDE. In the mobile version, however, all of the classes used by a project can only be in a single JAR file. Hence, the module05-task01-common project is included in the source path for the module05-task01-mobile project.

  2. Start the NetBeans IDE and open the module05-task01-desktop project to run the desktop version of the application, as shown in Figure 1.

    Desktop Version of Media Browser App Figure 1


  3. Open the module05-task01-mobile project in the IDE to run the mobile version of the application, as shown in the mobile emulator in the following figure.

    Note that the module05-task01-mobile project can be built and run only on a Windows platform, as the mobile emulator is currently available only for the Windows platform.

    Mobile Version of Media Browser App Figure 2


  4. To search for new images, click the right soft key or the magnifying glass icon in the lower right corner of the phone screen.

  5. On the Search screen, click the text field area, as shown in Figure 3, and type text in the next screen, as shown in Figure 4.

    Media Browser Search Screen on Mobile Emulator Figure 3

    Enter Search Text on Mobile Emulator Figure 4


  6. After typing the text, click Menu on the bottom right corner of the screen, and select Save, as shown in Figure 5.

    Save Search Text Figure 5


  7. Click Done on the next screen to start the search.

    The thumbnails of images for the text you typed are displayed on the mobile emulator's screen. Figure 6 shows the thumbnails for butterfly images.

    Mobile Emulator With Butterfly Images After SearchFigure 6

Architecture

This task reorganizes the application into three packages, as shown in Figure 7.

Architecture for Module 5 Task 1 which includes three packages. Figure 7

Organizing the Code

For Module 5, the source code is reorganized into the following three packages:

  • mediabrowser - Contains the source that is common between desktop and mobile. The module05-task01-common project contains the code for the mediabrowser package. This code originated from the Module 4 Task 1 and has been modified in several areas to accommodate the mobile platform. The changes are explained in the Refactoring section discussed later in this document. Changes to the code in the module05-task01-common project affect both the desktop and the mobile versions. Therefore, any change that is specific to either the desktop or mobile version of the application should be made in the mediabrowser.desktop package or in the mediabrowser.mobile package, respectively.

  • mediabrowser.desktop - Contains the code that is specific to the desktop version of the application. The module05-task01-desktop project contains the code for the mediabrowser.desktop package. The classes in this package extend from classes in the mediabrowser package to provide desktop-specific functionality. Also included in this package is the desktop version's Main class. Notice that there are very few other classes in this package, as the bulk of the functionality is implemented in the classes in the mediabrowser package.

  • mediabrowser.mobile - Contains the code that is specific to the mobile version of the application. The module05-task01-mobile project contains the code for the mediabrowser.mobile package. The classes in this package extend from classes in the mediabrowser package to provide mobile-specific functionality. Also included in this package is the mobile version's Main class. As with the mediabrowser.desktop package, only a handful of classes are included in this package. Also included are some additional resource files for mobile-specific icons.

Refactoring

This section covers changes made to the code developed in Module 4 Task 1 to accomodate a mobile version of the application.

Handling Constants

In the previous modules of this tutorial, the Media Browser application was built only for the desktop. The Constants.fx file introduced in Module 1 Task 1: Loading and Displaying an Image was built with the understanding that the Media Browser application would eventually run on a mobile device. Constants.fx contained a number of script variables, some of which are shown here:

Source Code
/** The height of a thumbnail image */ 
 package def THUMB_HEIGHT = 75;
	  
/** The width of a thumbnail image */
package def THUMB_WIDTH  = 100;

These variables are used elsewhere in the code, such as in this example from mediabrowser.Thumbnail:

Source Code
fitWidth: Constants.THUMB_WIDTH

fitHeight: Constants.THUMB_HEIGHT

For mobile applications, the height and width of the thumbnail need to be much smaller. However, if these values are changed for the mobile version of the application, the desktop application would be askew. While it is possible to have two versions of Constants.fx, one for desktop and one for mobile, that would lead to duplication of code, which should be avoided. A better option is to create the Boolean flag, isMobile, so that it can enable the constants to be defined like the following:

Source Code
package def THUMB_HEIGHT = if (isMobile) 66 else 75;

This option assumes, however, that all mobile devices are the same, whereas each device might have different screen sizes and capabilities. This option would require a different Constants.fx file for each mobile device.

With JavaFX technology, another solution is available: data binding. Data binding, or the ability to create an immediate and direct relationship between two variables, is one of the most powerful features of the JavaFX Script programming language. The bind keyword associates the value of a target variable with the value of a bound expression. If the constants are defined so that instance is an instance of some class, the values could be easily overridden in the declaration of the instance object literal:

Source Code
/** The height of a thumbnail image */ 
 package def THUMB_HEIGHT = bind instance.thumb_height;

/** The width of a thumbnail image */
package def THUMB_WIDTH  = bind instance.thumb_width;

A drawback to using bind is that each time the bound value changes, the bind is reevaluated. In the case of Constants, however, the value changes only when the Constants object literal is initialized, so the overhead is insignificant.

The variable instance is defined in Constants.fx and is an instance of the class Constants, which is new to this module. The Constants class contains variables with the public-init access modifer. Essentially, the values given here are the default values. The public-init access modifier defines a variable that can be publicly initialized by object literals in any package. Subsequent write access, however, is allowed only at the script level.

Source Code
  var instance : Constants;

  public class Constants {
  
  /** The height of a thumbnail image */
  public-init var thumb_height = 75;

  /** The width of a thumbnail image */
  public-init var thumb_width  = 100;

The variable instance is initialized in the Constants class itself.

Source Code
  init {
    if ( instance == null ) instance = this;
  }

This behavior requires at least one instance of Constants to be declared. This declaration happens in mediabrowser.mobile.Main (or mediabrowser.desktop.Main for the desktop application):

Source Code
Constants {
    stage_height: 320
    stage_width: 240

    thumb_height: 66
    thumb_width: 88

    thumb_vertical_spacing: 12
    thumb_horizontal_spacing: 16

    title_font_size: 10
    error_font_size: 15

    thumb_loaders: 2
    thumb_load_range: 2

    title_bar_facade_height: 20

    search_results_max: 50

    rotate_icon: Image {
        url: "{__DIR__}resources/rotateIcon.png"
    }
    search_icon: Image {
        url: "{__DIR__}resources/searchIcon.png"
    }

    scrollctl_reserve: 20
}

This arrangement accommodates device-specific settings by providing a device-specific Main.fx file that contains the values required to initialize the Constants object literal.

Refactoring Main.fx Into MediaBrowser.fx

Main.fx was refactored into a MediaBrowser class to support subsequent subclassing for desktop and mobile. MediaBrowser extends Scene, which enables it to be used from Main as the scene for the application stage.

As part of this refactoring, the zoom animations that are played when enlarging a thumbnail were moved to Media.fx. This relocation keeps the zoom animation code together with the code on which it operates and simplifies the code in MediaBrowser.fx.

Originally, in Main.fx, the default search was initiated at the end of the script. Now the search is initiated from the postinit block of the MediaBrowser class. This block is executed after the object has been initialized:

Source Code
  postinit {
  yahoo.search();
  }                
			

Notice the use of the init block to initialize the Media Browser content variable. Recall that MediaBrowser extends Scene. The content variable is a member of Scene. The init block is called when the object is initialized:

Source Code
  init {
        content =
            Group {
                content: [
                    wall,
                    Group {
                        content: bind media
                    }
                 ]
             };
        }                
			

The use of the init block here is a matter of choice. You could have accomplished this by overriding the content variable. The mediabrowser.desktop.DesktopMediaBrowser and mediabrowser.mobile.MobileMediaBrowser classes extend mediabrowser.MediaBrowser. These classes exist to provide platform-specific instances of mediabrowser.Wall.

Handling the Reflection Effect

In the desktop application, the bottom row of thumbnails has a reflection effect. The javafx.scene.effect package is part of the desktop profile. These effects are not available in the common profile that the mobile profile uses. Therefore, the reflection effect has to be factored out of mediabrowser.Thumbnail. In the original mediabrowser.Thumbnail code discussed in Module 4 Task 2: Creating Multiple Walls of Thumbnails, reflection is a script-level variable:

Source Code
def reflection : Effect = Reflection {
   fraction: 0.6
   topOpacity: 0.6
   bottomOpacity: 0
   topOffset: 3
} 
			

The reflection variable is referenced in Thumbnail's create function:

Source Code
protected override function create() : Node {
    Group {
       effect: if (reflect) then reflection else null;
       content: [
           boundingRect,
           imageView,
           watermark,
           focusOutline
        ]
    } 
}			

To factor the reflection out of Thumbnail for the mobile application, this code was simply removed from mediabrowser.Thumbnail. The create function in Thumbnail now contains this code:

Source Code
protected override function create() : Node {
    Group {
           content: [
                boundingRect,
                imageView,
                watermark,
                focusOutline
           ]
     }
} 
			

But the reflection effect is still needed in the desktop application. To add the relection effect, the DesktopThumbnail class was created. This class extends Thumbnail:

Source Code
package mediabrowser.desktop;

import javafx.scene.effect.Effect;
import javafx.scene.effect.Reflection;
import javafx.scene.Group;
import javafx.scene.Node;
import mediabrowser.Thumbnail;

def reflection: Effect = Reflection {
    fraction: 0.6
    topOpacity: 0.6
    bottomOpacity: 0
    topOffset: 3
}

package class DesktopThumbnail extends Thumbnail {
    protected override function create() : Node {
        Group {
            effect: if (reflect) then reflection else null;
            content: [
                boundingRect,
                imageView,
                watermark,
                focusOutline
            ]
        }
    }
} 
			

Recall that Thumbnails are created in Wall.fx as results from the web search are inserted into the Wall's metaData variable. To use the DesktopThumbnail class in the application, in Wall.fx, the loop that creates the thumbnails was changed to call the function makeThumbnail:

Source Code
protected var thumbnails: Thumbnail[] = bind for (newData in metaData) {
makeThumbnail(newData, indexof newData);
} 
			

In mediabrowser.Wall, makeThumbnail returns a mediabrowser.Thumbnail, which has no reflection. For desktop, makeThumbnail can then be overridden to return a DesktopThumbnail. This override implies that a DesktopWall class that extends Wall is needed:

Source Code
public class DesktopWall extends Wall {

    // When MetaData is inserted, we create a new thumbnail.
	
    override protected function makeThumbnail(newData: MetaData, index: Integer) : 
	Thumbnail {
        var col = (index) / Constants.THUMB_ROWS;
        var row = (index) mod Constants.THUMB_ROWS;

        DesktopThumbnail {
            metaData: newData

            requestThumbFocus: function(thumbnail: Thumbnail): Void {
            setThumbFocus(thumbnail);
            }
			
            fullView: fullView

            translateX: col * columnWidth

            translateY: row * (Constants.THUMB_HEIGHT + 
			Constants.THUMB_VERTICAL_SPACING)

            reflect: (row + 1) == Constants.THUMB_ROWS
        }
    }
}
			

And, because Wall is created from MediaBrowser, a DesktopMediaBrowser is needed to create DesktopWall:

Source Code
package mediabrowser.desktop;
import mediabrowser.MediaBrowser;

public class DesktopMediaBrowser extends MediaBrowser {
   // we create a new desktop wall.
   override var wall = DesktopWall {
			

Finally, mediabrowser.desktop.Main creates DesktopMediaBrowser:

Source Code
    scene : DesktopMediaBrowser {
    fill: Constants.STAGE_BACKGROUND_COLOR
    }
			

Handling the SearchTextBox

On desktop the SearchTextBox appears on the title bar. In mobile, the SearchTextBox behaves more like a modal dialog box that is launched from the right soft key or from a mouse click on the search icon.

While the desktop and mobile search text input controls look and behave differently, they do have in common their interaction with Wall and with WebSearch. The common pieces were left in mediabrowser.SearchTextBox, which is now an abstract base class for the mediabrowser.desktop.DesktopSearchTextBox and the mediabrowser.mobile.MobileSearchTextBox.

Source Code
public abstract class SearchTextBox extends CustomNode {

    public var  clearSearchResults: function() : Void;
    public var webSearch: WebSearch;
    protected var searchTB: TextBox;

    protected function getMetaDataforSearchText() : Void {
        if ( clearSearchResults != null ) { 
            clearSearchResults();
        }
        webSearch.searchQuery = searchTB.value.trim();
        webSearch.clearSearchResults();
        webSearch.search();
    }

}
			

The function getMetaDataforSearchText is called to initiate the web search. For desktop, the web search starts when you press Enter in the TextBox. For mobile, the web search starts when you select Done from the search dialog box. See the comments in the code for details on each of the fields.

Note the use of public and protected access modifiers. The public variables are those variables that will be initialized in the object literal. The protected variable searchTB and the protected function getMetaDataforSearchText are accessible from the derived class. In Wall.fx, the variable searchTB was set to protected so that the derived class can provide the correct instance of SearchTextBox. In mediabrowser.mobile.MobileWall, searchTB is initialized as a MobileSearchTextBox, like the following:

Source Code
override var searchTB = MobileSearchTextBox {
   webSearch: bind webSearch
   clearSearchResults: clearSearchResults
   onClose: function() : Void {
   showDialog = false;
   }
}
			

Whereas, in mediabrowser.desktop.DesktopWall, searchTB is initialized as a DesktopSearchTextBox:

Source Code
override var searchTB = DesktopSearchTextBox {
  translateX : bind maxVisibleWidth - searchTB.boundsInLocal.width - 
  Constants.BORDER_WIDTH
  
  translateY : bind (titleBar.boundsInLocal.height - 
  searchTB.boundsInLocal.height)/2 webSearch: webSearch
  clearSearchResults: clearSearchResults
}

The DesktopSearchTextBox code is the same as was described in Module 3 Task 2: Adding a Text Control. The MobileSearchTextBox is of interest to this module.

The MobileSearchTextBox has a Cancel button that is linked to the left soft key and a Done button that is linked to the right soft key. Both of these buttons also accept mouse input for touch-sensitive devices. The buttons are simply Rectangles and Text grouped together. The text is positioned relative to the rectangle and the group is positioned relative to the origin of the MobileSearchTextBox.

Source Code
    def rskButton : Rectangle = Rectangle {
        height: Constants.SEARCH_BOX_HEIGHT / 2
        width: Constants.STAGE_WIDTH / 2.0 - 3.0
        fill: Constants.SCROLLCTL_COLOR_20
        stroke: Constants.SCROLLCTL_COLOR
    }

    def rskLabel: Text = Text {
        content: "Done"
        font: Font { size: rskButton.height - 4 }
        translateX: bind (rskButton.width - rskLabel.boundsInLocal.width) / 2
        translateY: bind (rskButton.height - rskLabel.boundsInLocal.height) 
	    / 2 + rskLabel.font.size - 2
        fill: Color.WHITE
    }

    def rsk : Group = Group {
        translateX: rect.width - rskButton.width - 1
        translateY: rect.height - rskButton.height - 1
        content: [ rskButton rskLabel ]
        onMousePressed: function(me: MouseEvent) : 
		  
		  Void {
               getMetaDataforSearchText();
          if ( onClose != null ) { onClose() }
        }
    }

The code for the left soft key is similar. The notable difference is in the onMousePressed function where the left soft key does not call getMetaDataforSearchText.The soft keys are handled in the onKeyPressed function.

Source Code
    override var onKeyPressed = function(ke: KeyEvent): Void {
        if (ke.code == KeyCode.VK_SOFTKEY_1 or ke.code == KeyCode.VK_SOFTKEY_2) {
            if ( ke.code == KeyCode.VK_SOFTKEY_1 ) {
                getMetaDataforSearchText();
            }
            if ( onClose != null ) {
                onClose()
            }
        } else if (ke.code == KeyCode.VK_ENTER) {
            // On select or enter, move focus to the TextBox so the
            // user can type some text.
            searchTB.requestFocus();
        }
    }

The SearchTextBox is launched from MobileWall by pressing the right soft key or by touching the search icon.

Source Code
    override function handleKeyPressed( ke : KeyEvent ) : Void {
        if (ke.code == KeyCode.VK_SOFTKEY_1) {
            showDialog = true;
        } else {
            super.handleKeyPressed(ke);
        }
    }

The handleKeyPressed function is defined in mediabrowser.Wall, which enables MobileWall to intercept and handle key press events. If the event is not for the right soft key (VK_SOFTKEY_1), it is forwarded on to Wall's implementation.

If the right soft key is pressed, the showDialog variable is set to true. This causes the search dialog box to be shown, and sets focus to the search dialog box. Notice the use of showDialog in the following code.

Source Code
   var showDialog : Boolean on replace {
        if ( showDialog ) {
            searchTB.requestFocus()
        } else {
               wall.requestFocus();
        }
    }

    override function create() : Node {
        Group {
            content: bind if ( showDialog ) searchTB else [ wall searchButton ]
        }
    }