モジュール 4 タスク 2: サムネイルの複数壁の作成


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

はじめに

このチュートリアルではこれまで、Main.fx で Web サービス API と関連付けられる壁を 1 つだけ使ってメディアを表示する方法を紹介してきました。このセクションでは、アプリケーションを変更して、複数の壁を使ってメディアのサムネイルを表示します。

壁は次の 2 つのモードで表示できます。

  • 単一壁ビュー - 1 つの壁を前面に配置し、Stage 領域に合わせて拡大します。

  • デッキビュー - 壁を重ねたように配置します。

デフォルトでは、アプリケーションは 2 つの壁を使用しますが、さらに多くの壁を追加できます。

プロジェクトの実行

アプリケーションのメイン画面では、3 つの壁がデッキビューで表示されます。

  1. モジュール 4 タスク 2 NetBeans プロジェクトをダウンロードして、NetBeans IDE を開きます。

  2. プロジェクトを実行します。

  3. デッキビューモードの間は、上下の矢印キーを押すと、壁の順番を並べ替えることができます。

  4. 単一壁ビューとデッキビューのモードを切り替えるには、スペースバーを押し、いずれかの壁をクリックするか、ステージ右下隅にある アイコンをクリックします。

注: 壁に表示されているメディアのいずれかを読み込みまたは再生しようとすると、エラーが発生する場合があります。エラーには、次のような原因が考えられます。

  • Web 検索結果に含まれていたのがデッドリンクか、「HTTP 404 Not Found」エラーメッセージを返す URL である。
  • 結果のメディアが、再生できない形式である。
  • ネットワークエラーが発生した。

エラーが発生した場合は、メッセージが表示されます (Photo.fxVideo.fx を参照してください)。NetBeans IDE Output ウィンドウ、または Java コンソール (Web ページからアプリケーションを実行している場合) に、エラーの原因が表示されます。

アーキテクチャー

このタスクでは、新しいクラス FlickAPI.fx を 1 つ追加して、図 1 に水色で示した既存のクラスを変更します。

モジュール 4 タスク 2 のアーキテクチャー 図 1

キーボードの処理

単一壁ビューとデッキビューの切り替えやデッキのローテーションは、複数の異なるアニメーションシーケンスで処理されます。アプリケーションがデッキビューモードである場合、これらのアニメーションは、Main.fx の次のソースコードスニペットから開始されます。

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

playing 変数は、複数のアニメーションが同時に発生しないようにするだけの役割で、アニメーションが相反する目的で使用される (拡大中に縮小されるなど) 可能性があるために用意されています。キーが解放されたら、focusRotationStatus 変数が RotateFocusStop にリセットされることに注意してください。これは、次の「フォーカスのローテーション」で使用されます。

単一壁ビューモードでは、Wall.fx でのキーボードの処理により、zoomOut 関数が呼び出されます。

フォーカスのローテーション

キーボード処理のコードに示されているように、上下の矢印キーを押したり放したりすると、hopFocussedWall 関数への呼び出しが発生します。この関数は、壁がフォーカス内に入るとバウンドさせ、rotateWalls 関数を呼び出します。hopFocussedWalls 関数のコードの数行は、実際には実行されません。このコードの目的は、ユーザーが上下の矢印キーを押したときに、各壁を順番にバウンドさせることです。前面に移動する壁がバウンドしたときに、ユーザーがキーを放すと、壁が前面に移動されます。ただし、この機能は onKeyPressed コードのロジックにより無効になっています。ぜひ実験してみてください。

ここで興味深いのは rotateWalls 関数で、2 つのアニメーションを表示していることです。最初のアニメーションは、次のコードに示すように、前面の壁をフェードアウトさせ、壁の位置を近づけることによるパンダウン効果をもたらします。

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

fadeOutStartValues 変数と fadeOutEndValues 変数は、KeyValue シーケンスになります。Main.fx のソースファイルを参照すると、変数の初期化方法がわかります。フェードインアニメーションは、フェードアウトアニメーションの完了時に実行される action 関数から表示されることに注意してください。

2 番目のアニメーションは fadeIn で、前面の壁をフェードインさせ、壁をデッキ内の正しい位置に戻してパンダウン効果を解除します。

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

fadeInStartValues 変数と fadeInEndValues 変数は、再び KeyValue シーケンスになります。タイムラインの終わりのアクションで、rotateWalls が再度呼び出されることに注意してください。この呼び出しによって、指定した壁が前面に来るまで、rotateWalls アニメーションが繰り返し表示されます。

アプリケーションのビューがデッキから単一壁に変わった場合、zoomIn 関数が呼び出されます。この変更は、ユーザーがいずれかの壁のタイトルバーをクリックすると発生します。その壁が前面の壁でない場合、rotateWalls 関数が呼び出されて壁が前面に移動し、続いて zoomIn 関数が呼び出されます。

デッキビューから単一壁ビューへの拡大

ユーザーがスペースバーを押す、壁をクリックする、右下隅にあるズームアイコンをクリックするのいずれかの操作を行うと、zoomIn 関数が呼び出され、アニメーションが表示されます。

壁を拡大または縮小すると、フレームは一定のサイズで維持されますが、サムネイルは拡大または縮小されます。アプリケーションがデッキビューモードである (縮小されている) 場合、壁は少しずらした位置に重ねて配置され、サムネイルは 50% に縮小されます。アプリケーションが単一壁ビューモードである (拡大されている) 場合、壁はすべて同じ位置に配置され、フォーカスのある壁が前面に移動します。

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

縮小とローテーションのアニメーションと同様、startValuesmidValues、および endValues はすべて KeyValue シーケンスになります。アニメーションが終わる時点で action は各壁の viewMode を適切に設定し、前面の壁がフォーカスを持つようにするだけです。

単一壁ビューからデッキビューへの縮小

壁の配置は、viewMode 変数の値によって決まります。viewModeConstants.WALL_VIEW_DECK と等しい場合は、zoomOut 関数が呼び出されます。この関数によって、各壁の translateX 値と translateY 値が調整されて、壁が配置されます。zoomOut 関数も、次のコードに示すように、壁を所定の位置に移動する際のアニメーションを作成します。

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

startValuesmidValuesendValues の各変数は、KeyValue シーケンスになります。KeyValues のシーケンスが、translateX 値と translateY 値を使用して各壁に作成され、サムネイルが縮小されます。

マウスイベントの処理

壁を重ねるには、一番上の壁のマウスイベントは、後方の壁に受け取られないようにブロックする必要があります。Node API の blocksMouse を使用すると、マウスイベントが現在の Node で消費されるようになりますが、デッキビューモードと単一壁ビューモードでは、マウスイベントの扱い方が若干異なります。Wall.fx では、2 つの不透明な長方形が追加されました。1 つは、アプリケーションがデッキビューモードの場合にマウスイベントをインターセプトし、もう 1 つは単一壁ビューモードの場合にマウスイベントをインターセプトします。これらの長方形はそれぞれ、wallRectcoverRect です。

次のコードに示すように、WallRect はシーン全体をカバーしています。この長方形により blocksMouse が true に設定され、シーングラフ内の他の Node にマウスイベントが及ばないようにしています。単一壁ビューモードの使用中に、ユーザーが壁の背景をクリックすると、開いているメディアがすべて閉じます。

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

smooth 変数が false に設定されることにも注意してください。この設定は、アンチエイリアスに影響し、Rectangle がより高速に描画されますが、描画はより粗くなります。ただし、wallRect は単色なので、アンチエイリアスは使用されません。

デッキビューモードを使用中の場合、coverRect がマウスをインターセプトします。それ以外の場合、マウスイベントは通過します。デッキビューモードの場合、サムネイルに対するマウスイベントはブロックされます。アプリケーションがデッキビューの場合に coverRect をクリックすると、クリックされた壁が単一壁になります。

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

実際に必要な長方形は、これらのうち 1 つだけと言うこともできますが、これらの長方形がどのように重なっているかが重要です。次のコードでは wallRect が一番下にあり、coverRect が一番上にあることに注意してください。つまり、coverRect は、blocksMouse が true の場合に、すべてのマウスイベントをほかのノードからインターセプトします。そして、wallRect は、ほかのノードがインターセプトしていないすべてのマウスイベントをインターセプトします。長方形を 1 つ使用する場合は、アプリケーションが単一壁ビューかデッキビューモードであるかに応じて、この長方形を削除してから、シーングラフに再挿入することが必要になる場合があります。

ソースコード
package def wall : Group = Group {
   clip: wallRect
   content: [
       wallRect,
       thumbsGroupContainer,
       titleBar,
       searchTB,
       scrollCtl,
       coverRect
   ]
}

パフォーマンスの考慮事項

単一壁でパフォーマンスに最も影響するのは、Web サービスからのフィードデータの取得と、その後のサムネイルの読み込みです。「モジュール 3 タスク 3: より多くのデータの取得と読み込み」では、読み込み制限メカニズムを導入して、表示可能なサムネイルまたは隣接するサムネイルだけが表示されるようにしています。

複数の壁では、壁の全体または一部が別の壁で隠されることがあるため、どのサムネイルが表示可能であるかを決定するのがある程度複雑になります。デッキビューを使用していて壁が前面にない場合、壁の一部が隠れます。単一壁ビューでは、前面にない壁は完全に隠れます。

壁が前面にある場合、thumbs は通常どおり読み込まれます。壁が前面になく、一部分だけ表示されている場合は、表示可能なサムネイルだけが読み込まれます。それ以外の場合、壁はまったく表示されず、サムネイルは読み込まれません。

Wall.fxresetLoadLimits 関数は、このロジックを処理するためのコードブロックで、サムネイルの読み込みと読み込み解除のために ThumbnailController と連携して動作します。この関数は、壁の MetaData が最初に読み込まれたとき、ビューモードが単一壁から複数壁に変更されたとき、および Boolean 値 frontWall が変更されたときに呼び出されます。次のソースコードは、resetLoadLimits 関数の擬似コードを示しています。

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

画像読み込みの整列化には、ThumbnailController が使用されるため、壁は 1 つの ThumbnailController を共有する必要があります。Wall.fx では、次のコードスニペットに示すように、thumbnailController がスクリプトレベルで定義されています。壁間で thumbnailController へのアクセスを同期する必要はありません。thumbnailController への呼び出しはすべて、イベントディスパッチスレッドから発生します。

ソースコード
package def thumbnailController = ThumbnailController{};

試してみましょう

次の変更を加えて、画像の 4 番目の壁を追加します。

  1. Main.fx で、ビデオデータ用の Flickr Image Search Web サービスを解析する別のクラスを作成します。ソースファイルの最上部にあるインポートセクションの後に次のコードを追加します。ここには、他のすべての WebSearch クラスが定義されています。
ソースコード
/**
 * 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. 独自の WebSearch オブジェクトで別の壁を作成し、最後に表示される壁にします。3 番目の壁を定義したあとで、次のソースコードを追加します。3 番目の frontWall 変数と visible 変数は必ず false に設定してください。
ソースコード
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. 4 番目の壁を含むように wallOrder 変数を更新します。
ソースコード
/**
 * The current Z order of the walls.
 */
var wallOrder:Integer[] = [3, 2, 1, 0];
  1. flickrImages のデフォルト検索を開始するコード行を追加します。
ソースコード
flickrImages.search();
  1. プロジェクトをビルドし、実行します。

 

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