モジュール 5 タスク 1: モバイルデバイス向けアプリケーションへの変更


JavaFX メディアブラウザデスクトップアプリケーションの起動 NetBeans プロジェクトのダウンロード

はじめに

メディアブラウザチュートリアルのモジュール 5 では、メディアブラウザアプリケーションをモバイルデバイスに対応させる方法について説明します。このモジュールを最大限に活用するには、Media Browser Tutorial Setup ページ から始まるメディアブラウザチュートリアルをすべて終えることをお勧めします。

このモジュール 5 タスク 1 では、「モジュール 4 タスク 1」のソースコードを変更して、モバイルデバイスでもデスクトップと同様にアプリケーションを実行できるようにします。アプリケーションのモバイル版には、デスクトップの機能をできるだけ多く残しています。注意すべき相違点の 1 つは、desktop プロファイルには javafx.scene.effect パッケージが含まれていないため、モバイル版には反射効果が使用されないということです。また、画面サイズの制約により、右のソフトキーを押したり、携帯電話画面の右下隅にある虫めがね アイコンをクリックすることで検索テキストボックスにアクセスすることができます。

desktop プロファイルと common プロファイルについて

JavaFX API を使用すると、開発者は、異なるデバイス間でシームレスに動作するユーザーインタフェースを作成できます。JavaFX API の共通プロファイルには、デスクトップおよびモバイルデバイスの両方で機能するクラスが含まれます。desktop プロファイルの追加クラスとパッケージを使用すると、デスクトップアプリケーションを拡張する特定の機能を活用できます。common プロファイルと desktop プロファイルに含まれるクラスの一覧については、JavaFX API を参照してください。ページの右上隅にある「desktop」または「common」のリンクをクリックすると、対応するプロファイルのビューが表示されます。

JavaFX SDK には、JavaFX Mobile Emulator (携帯電話シミュレータ) が含まれ、チュートリアルのこのセクションでは、メディアブラウザアプリケーションをモバイルデバイスで実行する様子をシミュレートするために使用しています。現時点では、モバイルエミュレータは Microsoft Windows プラットフォームでのみ使用できることに注意してください。モバイルエミュレータについて詳しくは、SDK Readme ファイル (<SDK-install-directory>/README.html) を参照してください。

プロジェクトの実行

  1. モジュール 5 タスク 1 NetBeans プロジェクトの圧縮ファイルをダウンロードして、解凍します。

    module05-task01_nb.zip ファイルには tutorials フォルダとそのサブフォルダが含まれています。このファイルには、次の表に示す 3 つの NetBeans プロジェクトが含まれています。

    module05-task01_nb.zip ファイルの内容
    NetBeans プロジェクトフォルダのパス NetBeans プロジェクト名 フォルダ内容の説明
    tutorials/common/mediabrowser/module05-task01 module05-task01-common mediabrowser パッケージ の NetBeans プロジェクトとソース。アプリケーションのデスクトップ版とモバイル版の両方で共通して使用するソースコードです。
    tutorials/desktop/mediabrowser/module05-task01 module05-task01-desktop mediabrowser.desktop パッケージの NetBeans プロジェクトとソース。アプリケーションのデスクトップ版固有のソースコードです。
    tutorials/mobile/mediabrowser/module05-task01 module05-task01-mobile mediabrowser.mobile パッケージの NetBeans プロジェクトとソース。アプリケーションのモバイル版固有のソースコードが含まれています。
    module05-task01-desktopmodule05-task01-mobile の両方のプロジェクトに、module05-task01-common プロジェクトのソースが含まれていますが、構成が異なります。module05-task01-common プロジェクトは、module05-task01-desktop プロジェクトにライブラリとして含まれています。これは、NetBeans IDE で複数のプロジェクトを構成する場合に典型的なファイル構成です。しかし、モバイル版では、プロジェクトで使用するすべてのクラスを単一の JAR ファイルに格納しなければなりません。そのため、module05-task01-common プロジェクトが、module05-task01-mobile プロジェクトのソースパスに含まれています。

  2. NetBeans IDE を開始して、図 1 に示すように、module05-task01-desktop プロジェクトを開き、アプリケーションのデスクトップ版を実行します。

    メディアブラウザアプリケーションのデスクトップ版 図 1


  3. module05-task01-mobile プロジェクトを IDE で開き、次の図に示すように、モバイルエミュレータでアプリケーションのモバイル版を実行します。

    module05-task01-mobile プロジェクトをビルドして実行できるのは、Windows プラットフォームだけであることに注意してください。モバイルエミュレータは現在 Windows プラットフォームでのみ使用できます。

    メディアブラウザアプリケーションのモバイル版 図 2


  4. 新しい画像を検索するには、右のソフトキーまたは携帯電話画面の右下隅にある虫めがね アイコンをクリックします。

  5. 図 3 の検索画面でテキストフィールド領域をクリックし、次の画面で図 4 のようにテキストを入力します。

    モバイルエミュレータでのメディアブラウザ検索画面 図 3

    モバイルエミュレータでの検索テキストの入力 図 4


  6. テキストを入力したら、図 5 に示すように、画面の右下隅にある「Menu」をクリックして「Save」を選択します。

    検索テキストの保存 図 5


  7. 次の画面で「Done」をクリックして、検索を開始します。

    入力したテキストに対する画像のサムネイルが、モバイルエミュレータの画面に表示されます。図 6 には、蝶の画像のサムネイルが表示されています。

    検索後に蝶の画像が表示されたモバイルエミュレータ図 6

アーキテクチャー

このタスクでは、図 7 に示すように、アプリケーションを 3 つのパッケージに再構成しています。

3 つのパッケージを含むモジュール 5 タスク 1 のアーキテクチャー 図 7

コードの構成

モジュール 5 では、ソースコードを次の 3 つのパッケージに再構成しています。

  • mediabrowser - デスクトップとモバイルに共通するソースが含まれています。module05-task01-common プロジェクトには、mediabrowser パッケージのコードが含まれています。このコードは、モジュール 4 タスク 1 から派生したもので、モバイルプラットフォームに対応するために、いくつかの点が変更されています。変更点については、このドキュメント後半の「リファクタリング」セクションで説明しています。module05-task01-common プロジェクトのコードへの変更は、デスクトップ版とモバイル版の両方に影響します。そのため、アプリケーションのデスクトップ版またはモバイル版のいずれかに固有の変更は、それぞれ mediabrowser.desktop パッケージまたは mediabrowser.mobile パッケージに対して行う必要があります。

  • mediabrowser.desktop - アプリケーションのデスクトップ版固有のコードが含まれています。module05-task01-desktop プロジェクトには、mediabrowser.desktop パッケージに対するコードが含まれています。このパッケージのクラスは、mediabrowser パッケージのクラスから拡張され、デスクトップ固有の機能を用意しています。また、このパッケージにはデスクトップ版の Main クラスも含まれています。機能の大半は、mediabrowser パッケージのクラスで実装されているため、このパッケージにほかのクラスはほとんど含まれていないことに注意してください。

  • mediabrowser.mobile - アプリケーションのモバイル版に固有のコードが含まれています。module05-task01-mobile プロジェクトには、mediabrowser.mobile パッケージに対するコードが含まれています。このパッケージのクラスは、mediabrowser パッケージのクラスから拡張され、モバイル固有の機能を用意しています。また、このパッケージには、モバイル版の Main クラスが含まれています。mediabrowser.desktop パッケージと同様、このパッケージに含まれているクラスは、ごくわずかです。また、モバイル専用のアイコンとして、一部の追加リソースファイルも含まれています。

リファクタリング

このセクションでは、モジュール 4 タスク 1 で作成されたコードを変更して、アプリケーションのモバイル版に対応します。

定数の処理

このチュートリアルのこれまでのモジュールでは、デスクトップ用のメディアブラウザアプリケーションだけを作成していました。「モジュール1 タスク 1: 画像の読み込みと表示」で紹介した Constants.fx ファイルは、メディアブラウザアプリケーションが最終的にはモバイルデバイスで実行されるということを踏まえて構築されています。Constants.fx には、次に示すような、いくつかのスクリプト変数が含まれていました。

ソースコード
/** The height of a thumbnail image */ 
 package def THUMB_HEIGHT = 75;
	  
/** The width of a thumbnail image */
package def THUMB_WIDTH  = 100;

mediabrowser.Thumbnail の次の例のように、これらの変数はコードの他の部分で使用されています。

ソースコード
fitWidth: Constants.THUMB_WIDTH

fitHeight: Constants.THUMB_HEIGHT

モバイルアプリケーションの場合、サムネイルの高さと幅をかなり小さくする必要があります。ただし、これらの値をアプリケーションのモバイル版に合わせて変更すると、デスクトップアプリケーションでのバランスが悪くなります。デスクトップ用とモバイル用に、Constants.fx の 2 つのバージョンを用意することもできますが、これはコードの重複を招くため避けるべきです。より効果的な方法は、Boolean フラグ isMobile を使用して、次のように定数を定義できるようにすることです。

ソースコード
package def THUMB_HEIGHT = if (isMobile) 66 else 75;

ただし、この方法では、すべてのモバイルデバイスが同一であることを想定していますが、デバイスはそれぞれ異なる画面サイズや機能を備えている可能性があります。この方法では、各モバイルデバイスに対し別々に Constants.fx ファイルを用意することが必要になる場合があります。

JavaFX テクノロジでは、データバインドという別のソリューションを使用することができます。データバインディング、すなわち、2 つの変数の間に即時かつ直接の関係を構築する機能は、JavaFX Script プログラミング言語のもっとも高度な機能の 1 つです。bind キーワードは、ターゲット変数の値と結合式の値を関連付けます。instance があるクラスのインスタンスとなるように定数を定義している場合、instance オブジェクトリテラルの宣言で、値を簡単にオーバーライドできます。

ソースコード
/** 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;

bindを使用するデメリットは、バインドされた値が変更されるたびに、バインドが再評価されるということです。ただし、Constants の場合、値が変更されるのは Constants オブジェクトリテラルが初期化されるときだけなので、大きなオーバーヘッドではありません。

instance 変数は Constants.fx で定義され、このモジュールで初めて出てくる Constants クラスのインスタンスです。Constants クラスには、public-init アクセス修飾子が指定された変数が含まれています。ここで指定されている値は、基本的にデフォルト値です。public-init アクセス修飾子は、いずれかのパッケージ内のオブジェクトリテラルによって public として初期化できる変数を定義します。ただし、後続の書き込みアクセスは、スクリプトレベルでのみ許可されています。

ソースコード
  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;

instance 変数は Constants クラス自体で初期化されます。

ソースコード
  init {
    if ( instance == null ) instance = this;
  }

この動作を実現するには、Constants のインスタンスを少なくとも 1 つ宣言する必要があります。この宣言は、mediabrowser.mobile.Main (またはデスクトップアプリケーションの場合、mediabrowser.desktop.Main) で行われます。

ソースコード
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
}

このような方法で、Constants オブジェクトリテラルの初期化に必要な値を含むデバイス固有の Main.fx ファイルを指定することで、デバイス固有の設定に対応しています。

Main.fxMediaBrowser.fx へのリファクタリング

デスクトップおよびモバイル用に後からサブクラス化できるようにするため、Main.fxMediaBrowser クラスにリファクタリングされました。MediaBrowserScene を拡張して、アプリケーションステージのシーンとして Main から使用できるようにします。

このリファクタリングの一環で、サムネイルを拡大するときに表示されるズームアニメーションが Media.fx に移動されました。この配置転換により、このズームアニメーションのコードと、ズームアニメーションコードが実行するコードが一緒に保持され、MediaBrowser.fx のコードが簡素化されています。

もともと、Main.fx では、スクリプトの最後でデフォルト検索が開始されていました。ここでは、MediaBrowser クラスの postinit ブロックから検索が開始されます。このブロックは、オブジェクトが初期化されてから実行されます。

ソースコード
  postinit {
  yahoo.search();
  }                
			

init ブロックを使用して、メディアブラウザの content 変数が初期化されていることに注意してください。MediaBrowserScene を拡張していることを思い出してください。コンテンツ変数は、Scene のメンバーです。init ブロックは、オブジェクトが初期化されると呼び出されます。

ソースコード
  init {
        content =
            Group {
                content: [
                    wall,
                    Group {
                        content: bind media
                    }
                 ]
             };
        }                
			

ここで init ブロックを使用するかどうかは選択が可能です。content 変数をオーバーライドしても、この操作を実現できます。mediabrowser.desktop.DesktopMediaBrowser クラスと mediabrowser.mobile.MobileMediaBrowser クラスでは、mediabrowser.MediaBrowser が拡張されます。これらのクラスは、mediabrowser.Wall のプラットフォーム固有のインスタンスを提供するために存在します。

反射効果の処理

デスクトップアプリケーションでは、サムネイルの最下行に反射効果が適用されます。javafx.scene.effect パッケージは、desktop プロファイルの一部です。これらの効果は、モバイルプロファイルが使用する common プロファイルでは使用できません。そのため、反射効果は mediabrowser.Thumbnail から除外する必要があります。「モジュール 4 タスク 2: サムネイルの複数壁の作成」で説明している、もともとの mediabrowser.Thumbnail コードでは、reflection は、スクリプトレベルの変数です。

ソースコード
def reflection : Effect = Reflection {
   fraction: 0.6
   topOpacity: 0.6
   bottomOpacity: 0
   topOffset: 3
} 
			

reflection 変数は、Thumbnail の create 関数で参照されます。

ソースコード
protected override function create() : Node {
    Group {
       effect: if (reflect) then reflection else null;
       content: [
           boundingRect,
           imageView,
           watermark,
           focusOutline
        ]
    } 
}			

モバイルアプリケーション用に Thumbnail から reflection を除外するため、このコードは単純に mediabrowser.Thumbnail から削除されています。Thumbnailcreate 関数に、次のコードが含まれるようになりました。

ソースコード
protected override function create() : Node {
    Group {
           content: [
                boundingRect,
                imageView,
                watermark,
                focusOutline
           ]
     }
} 
			

しかし、反射効果は、デスクトップアプリケーションにはまだ必要です。反射効果を追加するために、DesktopThumbnail クラスが作成されました。このクラスは、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
            ]
        }
    }
} 
			

Web 検索の結果として Wall.fx で作成された Thumbnails が、Wall の metaData 変数に挿入されていることを思い出してください。DesktopThumbnail クラスをアプリケーションで使用するために、Wall.fx で、サムネイルを作成するループは、makeThumbnail 関数を呼び出すように変更されました。

ソースコード
protected var thumbnails: Thumbnail[] = bind for (newData in metaData) {
makeThumbnail(newData, indexof newData);
} 
			

mediabrowser.Wall では、makeThumbnail が reflection を含まない mediabrowser.Thumbnail を返します。デスクトップの場合、ここで makeThumbnail をオーバーライドして、DesktopThumbnail を返すようにすることができます。このオーバーライドでは、Wall を拡張する DesktopWall クラスが暗黙的に必要になります。

ソースコード
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
        }
    }
}
			

また、WallMediaBrowser から作成されるため、DesktopWall の作成には DesktopMediaBrowser が必要です。

ソースコード
package mediabrowser.desktop;
import mediabrowser.MediaBrowser;

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

最後に、mediabrowser.desktop.Main によって DesktopMediaBrowser が作成されます。

ソースコード
    scene : DesktopMediaBrowser {
    fill: Constants.STAGE_BACKGROUND_COLOR
    }
			

SearchTextBox の処理

デスクトップでは、SearchTextBox はタイトルバーに表示されます。モバイルでは、SearchTextBox は、右ソフトキーまたは検索アイコンのマウスクリックで起動されるモーダルダイアログボックスのように動作します。

デスクトップとモバイルでは、検索テキスト入力コントロールの外観も動作も異なりますが、Wall や WebSearch とのやり取りは共通しています。mediabrowser.SearchTextBox には、共通の要素が残され、mediabrowser.desktop.DesktopSearchTextBox および 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();
    }

}
			

getMetaDataforSearchText 関数を呼び出すと、Web 検索が開始されます。デスクトップでは、Web 検索はテキストボックスで Enter キーを押すと開始されます。モバイルでは、検索ダイアログボックスで「Done」を選択すると Web 検索が開始されます。各フィールドの詳細については、コード中のコメントを参照してください。

アクセス修飾子 publicprotected の使用方法に注意してください。public 変数は、オブジェクトリテラルで初期化される変数です。protected 変数 searchTB と protected 関数 getMetaDataforSearchText は、派生クラスからアクセスできます。Wall.fx では、searchTB 変数が protected に設定され、派生クラスは SearchTextBox の正しいインスタンスを提供できます。mediabrowser.mobile.MobileWall では、searchTB が次のように、MobileSearchTextBox として初期化されます。

ソースコード
override var searchTB = MobileSearchTextBox {
   webSearch: bind webSearch
   clearSearchResults: clearSearchResults
   onClose: function() : Void {
   showDialog = false;
   }
}
			

これにより、mediabrowser.desktop.DesktopWall では、searchTBDesktopSearchTextBox として初期化されます。

ソースコード
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
}

DesktopSearchTextBox コードは、「モジュール 3 タスク 2: テキストコントロールの追加」で説明したコードと同じです。このモジュールで扱うのは、MobileSearchTextBox です。

MobileSearchTextBox には左ソフトキーとリンクしている「Cancel」ボタン、および右ソフトキーにリンクしている「Done」ボタンがあります。これらのボタンはどちらも、タッチ入力デバイスではマウス入力と受け取ります。ボタンは単に、グループ化された RectanglesText です。テキストは長方形からの相対位置で配置され、グループは 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() }
        }
    }

左ソフトキーのコードも同様です。注目すべき相違点は、onMousePressed 関数では左ソフトキーから getMetaDataforSearchText が呼び出されないということです。ソフトキーは、onKeyPressed 関数で処理されます。

ソースコード
    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();
        }
    }

SearchTextBox は、右ソフトキーを押すか検索アイコンをタッチすると、MobileWall から呼び出されます。

ソースコード
    override function handleKeyPressed( ke : KeyEvent ) : Void {
        if (ke.code == KeyCode.VK_SOFTKEY_1) {
            showDialog = true;
        } else {
            super.handleKeyPressed(ke);
        }
    }

handleKeyPressed 関数は mediabrowser.Wall で定義されていて、このことにより、MobileWall はキーが押されたイベントをインターセプトおよび処理できます。イベントが右ソフトキー (VK_SOFTKEY_1) に対するイベントではなかった場合、Wall の実装に転送されます。

右ソフトキーが押された場合は、showDialog 変数が true に設定されます。これにより、検索ダイアログボックスが表示され、この検索ダイアログボックスにフォーカスが設定されます。次のコードの showDialog の使い方に注意してください。

ソースコード
   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 ]
        }
    }

 

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