モジュール 5 タスク 2: 回転、ズーム、パン、およびスワイプ機能の追加


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

はじめに

メディアブラウザチュートリアルの「モジュール 5 タスク 1」では、メディアブラウザアプリケーションをモバイルデバイス上で実行するように調整しました。「モジュール 5 タスク 2」では、モバイルデバイスの画面に表示されているコンテンツを縦向き (垂直) 表示から横向き (水平) 表示に回転する機能を追加します。このタスクでは、サムネイルを 1 回に 1 ページずつスクロールするスワイプ機能、選択した画像をさらに大きくするズーム機能、ビューポート内でズームした画像を垂直や水平に移動するパン機能も追加します。このタスクは、「モジュール 5 タスク 1」で作成した基盤に対して行います。そのため、そのチュートリアルをレビューすることをお勧めします。

プロジェクトの実行

これらの機能はモバイルデバイスでのみ使用可能であるため、このタスクでは、module05-task02-mobile プロジェクトのみが必要です。このタスクで使用する JavaFX モバイルエミュレータは、現在 Windows プラットフォームでのみ使用可能であるため、module05-task02-mobile プロジェクトは Windows 上でのみ構築と実行が可能であることに注意してください。

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

    module05-task02_nb.zip ファイルには、モジュール 5 タスク 1 で説明した 3 つの NetBeans プロジェクトを含む tutorials フォルダとそのサブフォルダが含まれています。ただし、このタスクでは、プロジェクト名は module05-task02-commonmodule05-task02-desktop、および module05-task02-mobile になります。

  2. 図 1 のモバイルエミュレータに示されているように、NetBeans IDE を開始し、IDE で module05-task02-mobile プロジェクトを開いて、アプリケーションのモバイルバージョンを実行します。

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

  3. 新しい画像を検索する手順については、「モジュール 5 タスク 1」の「プロジェクトの実行」のセクションを参照してください。

  4. モバイルエミュレータの画面を縦向きから横向きに回転するには、図 2 に示されているように、モバイルエミュレータのメインメニューから、「View」>「Orientation」>「90°」を選択して回転します。また、横向きから縦向きに回転するには、「View」>「Orientation」>「0°」を選択します。

    注: このリリースでは、0 度または 90 度に回転するオプションのみ実装されています。

    モバイルエミュレータで画面を回転する 図 2

  5. 図 3 に示されているように、モバイルエミュレータが 90 度回転します。

    回転したモバイルエミュレータの画面 図 3

  6. スワイプ機能を使用するには、画面にカーソルを置き、次にクリックして押したまま、カーソルを画面上で横に移動します。この操作により、サムネイルの壁が 1 回に 1 ページ、スクロールします。

    注: モバイルエミュレータでスワイプ機能を使用するとき、カーソルはモバイルデバイスの画面の境界の内側に配置したままにする必要があります。カーソルを画面の端を越えて移動すると、マウスボタンを離したことがアプリケーションによって認識されず、モバイルデバイスの画面をクリックするまでサムネイルの画像が再度読み込まれない場合があります。

  7. ズーム機能を使用するには、拡大する画像を選択します。虫めがねアイコンの下にある右ソフトキーをクリックしてズームします。再度、画像をタップまたはクリックして、元のサムネイルのサイズに画像を戻します。

  8. パン機能を使用するには、最初に画像をズームする手順に従い、次にキーボードまたはモバイルエミュレータの矢印キーを使用して画像をパンします。

    注: 指でドラッグやタップしてスワイプおよびズームなどの機能を使用するには、モバイルデバイスがタッチスクリーン機能を備えている必要があります。

アーキテクチャー

このタスクでは、Globals.fx クラスを追加します。このクラスには、アプリケーションの表示の向きを設定するために使用する変数が含まれています。また、図 4 に薄い青色のテキストで示されているように、MobileWall.fx クラスの更新も行います。

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

モバイルデバイス上での表示の回転

JavaFX モバイルエミュレータのメインメニューから 「View」>「Orientation」>「90°」を選択すると、物理的にモバイルフォンを横向きにしたように見えます。図 3 に示されているように、メディアブラウザアプリケーションの表示が回転します。アプリケーションがエミュレータの表示画面に合うように、サムネイルの数が 3 列から 2 列に変更されます。また、画面上には、回転ボタンのアイコンと検索ボタンのアイコンが適切な位置にあり、左右のソフトキーとの適切なアフォーダンスが引き続き提供される点に注意してください。想定したとおりに、スクロールバーが動作し、画像はクリックすると拡大します。「View」>「Orientation」>「0°」を使用して向きを 0 度に戻すと、表示は以前の状態に戻ります。

アプリケーションの表示を回転すると、Stage の高さと幅に対するハードウェア駆動型の変更をエミュレートすることができます。タイトルバーとスクロールバーの位置とサイズは、Constants.STAGE_HEIGHT および Constants.STAGE_WIDTH の値に基づいて、Wall.fx で定義されます。Constants.STAGE_HEIGHT および Constants.STAGE_WIDTH が Stage の高さと幅にバインドされている場合は、モバイルエミュレータの表示の向きが変更されるときにアプリケーションの表示は大きさが変更され、正しい位置に再び配置される必要があります。ただし、「モジュール 1 タスク 1」で導入された時点から、STAGE_HEIGHT および STAGE_WIDTH の値は定数になっています。

ステージの heightwidth の変更に対応するには、STAGE_HEIGHT および STAGE_WIDTH は定数としてではなく、変数として扱われる必要があり、グローバルでアクセス可能にする必要があります。したがって、新しい Globals.fx ソースファイルは、これらの変数と、アプリケーションの表示を回転する際のサムネイルの表示方法を決定するのに使用するその他の変数を保持する場所として導入されます。Constants.STAGE_HEIGHT および Constants.STAGE_WIDTH は、Globals.STAGE_HEIGHT および Globals.STAGE_WIDTH 変数となります。次に示されるように、Globals.fx に定義される変数の値は Main.fx で初期化されます。

ソースコード
Globals.THUMB_HEIGHT = 69;
Globals.THUMB_WIDTH = 92;
Globals.THUMB_VERTICAL_SPACING = 12;
Globals.THUMB_HORIZONTAL_SPACING = 16; 

Main.fx で表示の向きに対するハードウェア駆動の変更の検出が行われます。Main.fx では、局所変数 mobile_stage_height および mobile_stage_width が、ステージの height および width 変数にバインドされています。ステージの height および width が変更されるときに Globals.STAGE_HEIGHT および Globals.STAGE_WIDTH の値を設定するコードのブロックである、これらの変数の replace トリガーに注意してください。このセクションで後述するように、これらのトリガーは Globals.ROTATE_MODE もリセットします。

次の Main.fx からのコードスニペットは、stage.height を使用した mobile_stage_height と、stage.width を使用した mobile_stage_width という 2 つの変数間の直接の関係を作成するキーワード bind を使用することを示しています。アプリケーションが縦向きまたは横向きのモードのいずれであるかを決定するには、widthheight を決定する必要があります。isInitialized への呼び出しによって、width および height が両方とも確実に初期化されます。

ソースコード
var mobile_stage_height: Number = bind stage.height on replace {
    Globals.STAGE_HEIGHT = mobile_stage_height;
    if (isInitialized(Globals.STAGE_WIDTH)) {
        Globals.ROTATE_MODE = false;
        if (Globals.STAGE_WIDTH > Globals.STAGE_HEIGHT ) {
            // landscape
            Globals.PORTRAIT_ORIENTATION = false;
        }
        else {
            // portrait
            Globals.PORTRAIT_ORIENTATION = true;
        }
    }
};

var mobile_stage_width: Number = bind stage.width on replace {
    Globals.STAGE_WIDTH = mobile_stage_width;
    if (isInitialized(Globals.STAGE_HEIGHT)) {
        Globals.ROTATE_MODE = false;
        if (Globals.STAGE_WIDTH > Globals.STAGE_HEIGHT ) {
            // landscape
            Globals.PORTRAIT_ORIENTATION = false;
        }
        else {
            // portrait
            Globals.PORTRAIT_ORIENTATION = true;
        }
    }
}; 

このチュートリアルの内容では、回転は、アプリケーションの height および width に対するソフトウェア駆動型の変更です。エミュレータで左のソフトキーを押したり、回転アイコン回転アイコンをクリックしても、ステージの height および width は変わりません。つまり、ステージの x および y 座標の位置は変わりません。代わりに、Globals.STAGE_HEIGHTGlobals.STAGE_WIDTH の値が交換されます。別の見方をすれば、エミュレータは縦向き表示であり、アプリケーションは横向き表示になっています。ソースコードでは、次の 2 つの変数を使用して、ステージの向きとアプリケーションの回転値を追跡します。

  • Globals.STAGE_HEIGHTGlobals.STAGE_WIDTH より大きい場合は、Globals.PORTRAIT_ORIENTATIONtrue に設定されます。

  • ユーザーがエミュレータの左ソフトキーを押すか、回転ボタンをクリックすると、Globals.ROTATE_MODEtrue に設定されます。このアクションを処理するコードは MobileWall.fx にあります。

ステージの向きは回転アクションでは変更されないため、x および y 座標系はアプリケーションによって認識される高さと幅とは一致しません。アプリケーションを回転するとき、シーングラフの遠近の座標はコードでは変更されないため、この不一致は紛らわしい場合があります。つまり、コード自体が x 座標を y 座標に、またはその逆に変換しなければなりません。ただし、このことは、Globals.ROTATE_MODE が使用されている数か所でのみ問題となるだけです。この問題のいくつかについて、次に詳しく説明します。

MobileWall.fx では、回転ボタン 回転アイコン および検索ボタン 検索アイコン は、常に対応するソフトキーと隣接するように配置されます。通常 (回転しないで、エミュレータの向きが 0°)、回転ボタンのアイコンは右から 3 ピクセル、下から 3 ピクセルに配置されます。translateY の計算方法は次のとおりです。

ソースコード
Globals.STAGE_HEIGHT - Constants.ROTATE_BUTTON_HEIGHT - 3 

アプリケーションを回転すると、アイコンは同じ場所に配置されますが、アプリケーションでは、y 軸に沿って幅が認識されます。以前の widthheight となり、以前の height は width となります。したがって、ROTATE_MODEtrue である場合の、translateY の計算方法は次のとおりです。

ソースコード
Globals.STAGE_WIDTH - Constants.ROTATE_BUTTON_HEIGHT - 3

シーングラフに関して言えば、これらは同じ変換です。同じロジックが検索ボタンに適用されます。

Wall.fx では、キーボードナビゲーションを処理するコードは、回転ともバランスを取る必要があります。アプリケーションを回転する前に、右ナビゲーションキーを押すと、現在の選択が同じ列の次のアイコンに移動します。上ナビゲーションキーを押すと、現在の選択は同じ列の以前のアイコンに移動します。アプリケーションを回転すると、行は y 軸に沿って配置され、列は x 軸に沿って配置されます。右ナビゲーションキーが上ナビゲーションキーとなり、上ナビゲーションキーが右ナビゲーションキーとなります。この変更は ROTATE_MODE にのみ適用されることに注意してください。

ソースコード
if ( not Globals.ROTATE_MODE ) {
    if (evt.code == KeyCode.VK_LEFT) {
        newThumbIndex -= Globals.THUMB_ROWS;
    } else if (evt.code == KeyCode.VK_RIGHT) {
        newThumbIndex += Globals.THUMB_ROWS;
    } else if (evt.code == KeyCode.VK_UP) {
        newThumbIndex -= 1;
    } else if (evt.code == KeyCode.VK_DOWN) {
        newThumbIndex += 1;
    } else {
        return;
    }
} else {
    if (evt.code == KeyCode.VK_DOWN) {
        newThumbIndex -= Globals.THUMB_ROWS;
    } else if (evt.code == KeyCode.VK_UP) {
        newThumbIndex += Globals.THUMB_ROWS;
    } else if (evt.code == KeyCode.VK_LEFT) {
        newThumbIndex -= 1;
    } else if (evt.code == KeyCode.VK_RIGHT) {
        newThumbIndex += 1;
    } else {
        return;
    }
}

おそらく、コードでもっとも複雑な部分は、新しく選択したサムネイルをコードによって表示可能にするキーボードナビゲーション処理の直後であると考えられます。コードでは、初めはサムネイルの getBoundsInScene() 関数の minX および maxX を使用して、サムネイルを完全に表示可能にするかどうかを決定していました。しかし、アプリケーションを回転すると、サムネイルの幅は y 軸に沿って配置されるようになります。そのため、次に示すように、maxY の値を minX の代わりに、minY の値を maxX の代わりに使用する必要があります。

ソースコード
var newMinPos = if ( not Globals.ROTATE_MODE )
                    newThumbWithFocus.getBoundsInScene().minX 
                else 
                    newThumbWithFocus.getBoundsInScene().maxY; 

var newMaxPos = if ( not Globals.ROTATE_MODE ) 
                    newThumbWithFocus.getBoundsInScene().maxX 
                else 
                    newThumbWithFocus.getBoundsInScene().minY; 

var oldMinPos = if ( not Globals.ROTATE_MODE ) 
                    oldThumbWithFocus.getBoundsInScene().minX
                else 
                    oldThumbWithFocus.getBoundsInScene().maxY;
					

アプリケーションが ROTATE_MODE になると、スクロールの方向は反対になります。ドラッグイベントの方向が反対になっている ScrollControl.fx で、この変更を確認できます。

ソースコード
var dragDistance = if ( not Globals.ROTATE_MODE ) evt.dragX else -evt.dragY;

回転アイコンおよび検索アイコンと同様に、検索ダイアログボックスの「Cancel」ボタンと「Done」ボタンは、それぞれ左ソフトキーと右ソフトキーと揃える必要があるため、これらのボタンには特別な処理が必要です。回転アイコンおよび検索アイコンとは異なり、「Cancel」ボタンと「Done」ボタンは、エミュレータが横向きのときに回転する必要があります。特に、ノードに対する変換シーケンスに注意してください。変換は次の順番で適用されます。effectopacitycliptransformsscaleXscaleYrotatetranslateXtranslateY。次の MobileSearchTextBox.fx コードでは、左ソフトキーの変数 lsk を、最初は元の位置から約 90°回転し、その後 translateXtranslateY 変換のアプリケーションによって所定の位置に移動します。右ソフトキーのアフォーダンスである rsk も同じ方法で扱われます。

ソースコード
def lsk: Group = Group {
   translateX: bind
      if (Globals.PORTRAIT_ORIENTATION or Globals.ROTATE_MODE ) 1
      else lskButton.height + 1
		
   translateY: bind
      if (Globals.PORTRAIT_ORIENTATION) Globals.STAGE_HEIGHT - lskButton.height - 1
      else if (Globals.ROTATE_MODE) Globals.STAGE_WIDTH - lskButton.height - 1
      else 1
		
   transforms: bind
      if (Globals.ROTATE_MODE or Globals.PORTRAIT_ORIENTATION)  null
      else Transform.rotate(90, 0, 0)
		
   content: [ lskButton lskLabel ]
	
   onMousePressed: function(me: MouseEvent) : Void {
      if ( onClose != null ) { 
          onClose();
      }
   }
}

画像のズームとパンを行う

セレクトキーまたは画像のサムネイルをクリックすると、画像が拡大します。画像を拡大した後は、画像をズームしたり、ナビゲーションキーを使用して画像をパンしたり、画像を縦横に移動したりすることがます。この機能をサポートするコードは、Photo.fx にあります。

押されたキーの処理が、ズームおよびパン機能の中心となります。モバイルデバイスで画像が表示されると、ImageView クラスとキーボードフォーカスを持つ Nodemediabrowser.Photo になります。モバイルエミュレータで右ソフトキーをクリックするか、コンピュータのキーボードで F2 キーを押すと、ズームモードを開始または終了することができます。画像をズームした後は、ナビゲーションキーを使用して、画像を上下左右に移動またはパンすることができます。これらのアクションはすべて handleKeyPressed 関数で処理されます。この関数は、mediabrowser.Media クラスファイルにある handleKeyPressed 関数をオーバーライドします。この関数は、onKeyPressed 関数のプロキシとして、Media クラスに追加されました。

ソースコード
override protected function handleKeyPressed(ke: KeyEvent): Void {
    if (ke.code == KeyCode.VK_SOFTKEY_1) {
        toggleZoom();
    } else if (ke.code == KeyCode.VK_UP) {
        pan(0, -DY);
    } else if (ke.code == KeyCode.VK_DOWN) {
        pan(0, DY);
    } else if (ke.code == KeyCode.VK_LEFT) {
        pan(-DX, 0);
    } else if (ke.code == KeyCode.VK_RIGHT) {
        pan(DX, 0);
    } else {
        super.handleKeyPressed(ke);
    }
}

toggleZoom 関数は、変数 panX および panY を初期化します。これらの変数は、画像の左上隅からの x 軸と y 軸上のパンオフセットを追跡し、画像を中央に置くよう初期設定されます。ズームは、viewportImageView に追加することにより実行されます。viewport に入る画像の部分のみ表示されます。

ソースコード
override var toggleZoom = function() {
    if (zoomFactor == 1) {
        zoomFactor = 2;
        def bounds = imageView.getBoundsInScene();
        panX = bounds.width / zoomFactor / 2;
        panY = bounds.height / zoomFactor / 2;
        var rect = Rectangle2D {
            minX: panX;
            minY: panY;
            width: bounds.width / zoomFactor;
            height: bounds.height / zoomFactor;
        }
        imageView.scaleX = zoomFactor;
        imageView.scaleY = zoomFactor;
        imageView.x = rect.minX;
        imageView.y = rect.minY;
        imageView.viewport = rect;
    } else {
        zoomFactor = 1;
        imageView.scaleX = 1;
        imageView.scaleY = 1;
        imageView.x = 0;
        imageView.y = 0;
        imageView.viewport = null;
    }
}

ユーザーがナビゲーションキーを押すと、pan 関数が呼び出されて、変数 panXpanY の値を再計算し、新しい viewportImageView 用に作成します。function (dx, dy) の引数は、現在のパン位置からのデルタとして表されます。パンは、画像の viewport が実際の画像の外側に決してはみ出さないよう制限されます。Rectangle2D オブジェクトは不変で、viewport は新しく構築される Rectangle2D オブジェクトと置き換えられます。

ソースコード
function pan(dx: Number, dy: Number): Void {
    if (zoomFactor == 1) {
        return;
    }

    def viewW = imageView.localToScene(imageView.boundsInLocal).width; 
    def viewH = imageView.localToScene(imageView.boundsInLocal).height;
    def zoomW = viewW / zoomFactor;
    def zoomH = viewH / zoomFactor;

    panX += dx;
    panY += dy;

    if (panX < 0) {
        panX = 0;
    }

    if (panY < 0) {
        panY = 0;
    }

    if (panX + zoomW > viewW) {
        panX = viewW - zoomW;
    }

    if (panY + zoomH > viewH) {
        panY = viewH - zoomH;
    }

    imageView.viewport = Rectangle2D {
        minX: panX
        minY: panY
        width: zoomW;
        height: zoomH;
    };
}

サムネイルを表示するとき、右ソフトキーを押すと検索ダイアログボックスが表示されます。右ソフトキーのアフォーダンスは検索アイコンで、ボタンとしても動作します。ただし、選択した画像を拡大すると、右ソフトキーの意味は "検索" から "ズーム" に変わります。右ソフトキーを押すと、ズームモードが開始されます。キーの意味が変わるため、アフォーダンスはズームアイコンズームアイコンに変更されます。

MobileWall.fx には、すでに検索アイコンを処理するコードとロジックが含まれており、少しの必要な変更を加えるだけで、このコードによってズームアイコンを表示することができます。Wall.fx は、拡大された画像が表示されている場合に true に設定される isMediaShowing という変数を持っています。次に示すように、この変数を searchButton 変数の ImageViewbind コードブロック内で使用して、検索アイコンまたはズームアイコンを表示するかどうかを決定できます。

ソースコード
content: [
    ImageView {
        image: bind 
            if ( not isMediaShowing ) then 
                Constants.SEARCH_ICON
            else
                Constants.ZOOM_ICON
        fitWidth: Constants.SEARCH_BUTTON_WIDTH
        fitHeight: Constants.SEARCH_BUTTON_HEIGHT
    }
] 

以前のコードは、検索アイコンをズームアイコンに変更するのには十分ですが、検索アイコンはボタンとしても動作します。検索アイコンが押されると、showDialog 変数が true に設定され、検索ダイアログボックスが表示されます。ただし、isMediaShowing 変数が true に設定されているとき、アイコンはズームアイコンとなり、Photo.fx 内の toggleZoom 関数を起動することができます。

ソースコード
onMouseClicked: function(me:MouseEvent) : Void {
    if ( not isMediaShowing ) {
        if ( not showDialog ) {
            showDialog = true;
        }
    } else if ( toggleZoom != null ) {
        toggleZoom();
    }
}

次のように、変数 toggleZoomMobileWall.fx に定義されています。

ソースコード
package var toggleZoom: function() : Void;

次に示すように、toggleZoom は、MobileMediaBrowser.fx から初期化可能となるように package アクセスとともに宣言されています。

ソースコード
override var wall = MobileWall {
     webSearch: bind yahoo
     fullView: showMedia
     hideMedia: hideMedia
     isMediaShowing: bind ( media != null )


     toggleZoom: bind media.toggleZoom
};

変数 media は、Photo のインスタンスまたは Video のインスタンスのいずれか、または isMediaShowingfalse に設定されている場合は、null にすることができます。理由は、「モジュール 1 タスク 4」で説明したとおり、PhotoVideo はともに Media から派生しており、変数 toggleZoomMedia.fx に追加されたためです。

ソースコード
public-read protected var toggleZoom: function() : Void;

toggleZoom 変数のアクセスモードとして public-read protected を使用することにより、派生したクラスがこの変数をオーバーライドして、その他のすべてのクラスがこの変数を読み取ることができます。変数がオーバーライドされない場合は、変数は null の値を持ち、以前説明したonMouseClicked 関数で無操作として扱われます。

スワイプ

スワイプとは、モバイルデバイスの画面上でサムネイルの壁を 1 回に 1 ページ、前後に移動するドラッグ操作で、マウスの左ボタンをクリックし押したまま画面上でカーソルを動かします。スワイプの実装は、「モジュール 2 タスク 2」で紹介したスクロールコントロールに基づきます。スワイプと、「モジュール 4 タスク 1」で紹介したフリックスクロール機能はともに、Wall の translation 変換に結び付け れた ScrollControl の position 変数を操作するという点で似ています。

スワイプ機能を実装するには、フリックをオーバーライドする必要があり、そのためには、ScrollControl のマウス操作をオーバーライドする必要があります。MobileWall.fx では、Wall の scrollCtl 変数がオーバーライドされ、別のマウスハンドラを参照することができます。

ソースコード
override var scrollCtl = ScrollControl {
        translateX: 0
        translateY: Constants.TITLE_BAR_FACADE_HEIGHT
        contentWidth: bind thumbsGroupWidth + Globals.THUMB_HORIZONTAL_SPACING
        windowWidth: bind Globals.STAGE_WIDTH
        windowHeight: bind Globals.STAGE_HEIGHT - Constants.TITLE_BAR_FACADE_HEIGHT
        position: bind scrollPosition with inverse;
        fractionDisplayed: bind fractionDisplayed;
        scrollOffset: bind scrollOffset;
        columns: bind wallColumns()
        visible: bind fractionDisplayed < 1.0
        // ScrollControl overrides the onMouseXXX functions. Here, we
        // initialize them to functions in MobileWall. This trumps the
        // override.
        onMousePressed: handleMousePressed
        onMouseDragged: handleMouseDragged
        onMouseReleased: handleMouseReleased
    }; 
 

handleMousePressed 関数では、ScrollControl の position 変数の現在の値が posAnchor に保存されます。変数 swiped は、ドラッグジェスチャーがスワイプの定義を十分に満たすほどマウスがドラッグされたときに、true に設定されるフラグです。

ソースコード
    function handleMousePressed(me : MouseEvent ) : Void {
        posAnchor = scrollCtl.position;
        swiped = false;
    }

handleMouseDragged 関数は、ドラッグジェスチャーがスワイプであるかどうかを決定します。マウスを動かした距離が Globals.STAGE_WIDTH の 3/5 に満たない場合、壁はドラッグされたと判断されます。マウスを動かした距離が Globals.STAGE_WIDTH の 3/5 を超える場合、そのジェスチャーはスワイプとして定義され、アニメーションによって 1 画面の幅だけ壁が前後に移動します。

ソースコード
function handleMouseDragged(me : MouseEvent ) : Void {
    var dragDistance = if ( not Globals.ROTATE_MODE ) me.dragX else -me.dragY;
    if ( not swiped ) {
        // ignore small movements of the mouse
        if (java.lang.Math.abs(dragDistance) > Constants.FLICK_DRAG_THRESHOLD) {
            // see ScrollControl for treatement of the dragging flag.
            scrollCtl.dragging = true;
            // scroll the wall only so far...
            if (java.lang.Math.abs(dragDistance) < Globals.STAGE_WIDTH * 3 / 5) {
                scrollCtl.position =
                posAnchor - dragDistance /
                (scrollCtl.contentWidth - scrollCtl.windowWidth);
            } else {
                //  the wall has be scrolled far enough to be a swipe...
                // setting swiped to true causes subsequent mouse drag
                // events to be ignored.
                swiped = true;

                // Move the scroll position forward by one "page"
                var deltaPos = (numVisibleColumns() * columnWidth) /
                (thumbsGroupWidth - maxVisibleWidth);

                if ( dragDistance < 0 ) { 
                    deltaPos *= -1
                }

                // notice that the new position is based on where the
                // position was before the wall was dragged.
                var newPos = posAnchor - deltaPos;
                if ( newPos > 1.0 ) {
                    newPos = 1.0
                }
                else if ( newPos < 0.0 ) {
                    newPos = 0.0
                }

                // This animation moves the wall from the current
                // scroll position to the new scroll position. The
                // Interpolator.EASEIN causes the animation to accelerate
                // over time.
                Timeline {
                    keyFrames: [
                        at(500ms) {
                        scrollCtl.position => newPos tween Interpolator.EASEIN
                        }
                    ]
                }.play();
            }
        }
    }
}

handleMouseReleased 関数は、ScrollControl の position 変数を posAnchor に格納された元の値に戻すアニメーションの再生のみ行います。

ソースコード
function handleMouseReleased(me : MouseEvent) : Void {
        // see ScrollControl for treatment of the dragging flag.
        scrollCtl.dragging = false;

        // If the wall wasn't dragged far enough to be a swipe, then
        // play an animation that repositions the wall back to where it was.
        if ( not swiped ) {
            Timeline {
                keyFrames: [
                    at(750ms) {
                    scrollCtl.position => posAnchor tween Interpolator.EASEIN
                    }
                ]
            }.play();
        }

    }

壁を移動するとき、表示可能な列の数を使用して、ScrollControl の position 変数の増分または減分を決定します。このコードを機能させるには、表示可能な列の合計幅を画面の幅と同じにする必要があります。列を画面の幅に合わせる最も容易な方法は、サムネイルを収めるように拡大縮小することです。変数 thumbsScaleFactor を使用すると、表示可能な列に対する画面の幅の比率を求めることができます。

ソースコード
def thumbsScaleFactor: Number =
        bind Globals.STAGE_WIDTH / (numVisibleColumns() * columnWidth);

この拡大縮小率は、thumbs Group の倍率を変換することにより、thumbs Group を拡大縮小するために使用されます。変換における拡大縮小率は xy 軸の両方に適用され、サムネイルは比率に合わせて拡大縮小されることに注意してください。また、transforms のシーケンスには、thumbs Group を中央で垂直に配置する translate 変換が含まれることにも注意します。同じ効果は、直接 TranslateXTranslateY を使用することによっても得られます。

ソースコード
protected def thumbs: Group = Group {
    content: bind thumbnails
    transforms: bind [
        javafx.scene.transform.Transform.scale(thumbsScaleFactor, thumbsScaleFactor)
        javafx.scene.transform.Transform.translate(scrollOffset, thumbsTranslateY)
    ]
}

試してみましょう

ファイル Photo.fx にはズーム機能とパン機能を有効にするコードが含まれています。選択したグラフィックの拡大はここで定義します。

  • Rectangle2D 関数内と toggleZoom 関数内で bounds.widthb unds.height の区切りを zoomFactor によってコメントアウトします。次のサンプルコードを参照してください。
    ソースコード
     
    function toggleZoom() {
            if (zoomFactor == 1) {
                zoomFactor = 2;
                def bounds = imageView.getBoundsInScene();
                panX = bounds.width / zoomFactor / 2;
                panY = bounds.height / zoomFactor / 2;
                var rect = Rectangle2D {
                    minX: panX;
                    minY: panY;
                    width: bounds.width; /* / zoomFactor; */
                    height: bounds.height; /* / zoomFactor; */
                }
                imageView.scaleX = zoomFactor;
                imageView.scaleY = zoomFactor;
                imageView.x = rect.minX;
                imageView.y = rect.minY;
                imageView.viewport = rect;
            } else {
                zoomFactor = 1;
                imageView.scaleX = 1;
                imageView.scaleY = 1;
                imageView.x = 0;
                imageView.y = 0;
                imageView.viewport = null;
            }
        }
    
     
  • pan 関数内で、panXpanY の境界チェックをコメントアウトしてみます。次のサンプルコードを参照してください。
    ソースコード
    function pan(dx: Number, dy: Number): Void {
            if (zoomFactor == 1) {
                return;
            }
    
            def viewW = imageView.getBoundsInScene().width;
            def viewH = imageView.getBoundsInScene().height;
            def zoomW = viewW / zoomFactor;
            def zoomH = viewH / zoomFactor;
    
            panX += dx;
            panY += dy;
    
          /* 
    	   if (panX < 0) {
                panX = 0;
            }
    
            if (panY < 0) {
                panY = 0;
            }
    
            if (panX + zoomW > viewW) {
                panX = viewW - zoomW;
            }
    
            if (panY + zoomH > viewH) {
                panY = viewH - zoomH;
            } 
    		
    	 */
    
            imageView.viewport = Rectangle2D {
                minX: panX
                minY: panY
                width: zoomW;
                height: zoomH;
            };
        }
    
    

 

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