Module 5 Task 1: Modifying the Application for a Mobile Device
- Highlights
-
- Modify the application to the run on both desktop and mobile platforms
- Use classes from the common and desktop profiles of the JavaFX API
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
- Download and unzip the compressed file of Module 5 Task 1 NetBeans projects.
Themodule05-task01_nb.zipfile contains thetutorialsfolder and its subfolders, which include the three NetBeans projects described in the following table.
Both theContents of the module05-task01_nb.zipFilePath of NetBeans Project Folder NetBeans Project Name Description of Contents of Folders tutorials/common/mediabrowser/module05-task01module05-task01-commonNetBeans 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-task01module05-task01-desktopNetBeans project and source for the mediabrowser.desktoppackage, which is the source code that is specific to the desktop version of the application.tutorials/mobile/mediabrowser/module05-task01module05-task01-mobileNetBeans project and source for the mediabrowser.mobilepackage, which contains the source code specific to the mobile version of the application.module05-task01-desktopand themodule05-task01-mobileprojects include source from themodule05-task01-commonproject, but in different configurations. Themodule05-task01-commonproject is included as a library in themodule05-task01-desktopproject. 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, themodule05-task01-commonproject is included in the source path for themodule05-task01-mobileproject. - Start the NetBeans IDE and open the
module05-task01-desktopproject to run the desktop version of the application, as shown in Figure 1.
Figure 1 - Open the
module05-task01-mobileproject in the IDE to run the mobile version of the application, as shown in the mobile emulator in the following figure.
Note that themodule05-task01-mobileproject can be built and run only on a Windows platform, as the mobile emulator is currently available only for the Windows platform.
Figure 2 - To search for new images, click the right soft key or the magnifying glass
icon in the lower right corner of the phone screen. - 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.
Figure 3
Figure 4 - After typing the text, click Menu on the bottom right corner of the screen, and select Save, as shown in Figure 5.
Figure 5 - 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.
Figure 6
Architecture
This task reorganizes the application into three packages, as shown in Figure 7.
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. Themodule05-task01-commonproject contains the code for themediabrowserpackage. 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 themodule05-task01-commonproject 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 themediabrowser.desktoppackage or in themediabrowser.mobilepackage, respectively.mediabrowser.desktop- Contains the code that is specific to the desktop version of the application. Themodule05-task01-desktopproject contains the code for themediabrowser.desktoppackage. The classes in this package extend from classes in themediabrowserpackage to provide desktop-specific functionality. Also included in this package is the desktop version'sMainclass. Notice that there are very few other classes in this package, as the bulk of the functionality is implemented in the classes in themediabrowserpackage.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 themediabrowserpackage to provide mobile-specific functionality. Also included in this package is the mobile version'sMainclass. As with themediabrowser.desktoppackage, 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:
/** 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:
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:
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:
/** 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.
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.
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):
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:
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:
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:
def reflection : Effect = Reflection {
fraction: 0.6
topOpacity: 0.6
bottomOpacity: 0
topOffset: 3
}
The reflection variable is referenced in Thumbnail's create function:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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.
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.
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.
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.
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 ]
}
}
