モジュール 5 タスク 2: 回転、ズーム、パン、およびスワイプ機能の追加
- 概要
-
- モバイルデバイスの画面上の画像の回転、ズーム、パン、およびスワイプ機能を実装する
はじめに
メディアブラウザチュートリアルの「モジュール 5 タスク 1」では、メディアブラウザアプリケーションをモバイルデバイス上で実行するように調整しました。「モジュール 5 タスク 2」では、モバイルデバイスの画面に表示されているコンテンツを縦向き (垂直) 表示から横向き (水平) 表示に回転する機能を追加します。このタスクでは、サムネイルを 1 回に 1 ページずつスクロールするスワイプ機能、選択した画像をさらに大きくするズーム機能、ビューポート内でズームした画像を垂直や水平に移動するパン機能も追加します。このタスクは、「モジュール 5 タスク 1」で作成した基盤に対して行います。そのため、そのチュートリアルをレビューすることをお勧めします。
プロジェクトの実行
これらの機能はモバイルデバイスでのみ使用可能であるため、このタスクでは、module05-task02-mobile プロジェクトのみが必要です。このタスクで使用する JavaFX モバイルエミュレータは、現在 Windows プラットフォームでのみ使用可能であるため、module05-task02-mobile プロジェクトは Windows 上でのみ構築と実行が可能であることに注意してください。
- モジュール 5 タスク 2 NetBeans プロジェクトの圧縮ファイルをダウンロードして、解凍します。
module05-task02_nb.zipファイルには、モジュール 5 タスク 1 で説明した 3 つの NetBeans プロジェクトを含むtutorialsフォルダとそのサブフォルダが含まれています。ただし、このタスクでは、プロジェクト名はmodule05-task02-common、module05-task02-desktop、およびmodule05-task02-mobileになります。 - 図 1 のモバイルエミュレータに示されているように、NetBeans IDE を開始し、IDE で
module05-task02-mobileプロジェクトを開いて、アプリケーションのモバイルバージョンを実行します。
図 1 - 新しい画像を検索する手順については、「モジュール 5 タスク 1」の「プロジェクトの実行」のセクションを参照してください。
- モバイルエミュレータの画面を縦向きから横向きに回転するには、図 2 に示されているように、モバイルエミュレータのメインメニューから、「View」>「Orientation」>「90°」を選択して回転します。また、横向きから縦向きに回転するには、「View」>「Orientation」>「0°」を選択します。
注: このリリースでは、0 度または 90 度に回転するオプションのみ実装されています。
図 2 - 図 3 に示されているように、モバイルエミュレータが 90 度回転します。
図 3 - スワイプ機能を使用するには、画面にカーソルを置き、次にクリックして押したまま、カーソルを画面上で横に移動します。この操作により、サムネイルの壁が 1 回に 1 ページ、スクロールします。
注: モバイルエミュレータでスワイプ機能を使用するとき、カーソルはモバイルデバイスの画面の境界の内側に配置したままにする必要があります。カーソルを画面の端を越えて移動すると、マウスボタンを離したことがアプリケーションによって認識されず、モバイルデバイスの画面をクリックするまでサムネイルの画像が再度読み込まれない場合があります。 - ズーム機能を使用するには、拡大する画像を選択します。虫めがねアイコンの下にある右ソフトキーをクリックしてズームします。再度、画像をタップまたはクリックして、元のサムネイルのサイズに画像を戻します。
- パン機能を使用するには、最初に画像をズームする手順に従い、次にキーボードまたはモバイルエミュレータの矢印キーを使用して画像をパンします。
注: 指でドラッグやタップしてスワイプおよびズームなどの機能を使用するには、モバイルデバイスがタッチスクリーン機能を備えている必要があります。
アーキテクチャー
このタスクでは、Globals.fx クラスを追加します。このクラスには、アプリケーションの表示の向きを設定するために使用する変数が含まれています。また、図 4 に薄い青色のテキストで示されているように、MobileWall.fx クラスの更新も行います。
図 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 の値は定数になっています。
ステージの height と width の変更に対応するには、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 を使用することを示しています。アプリケーションが縦向きまたは横向きのモードのいずれであるかを決定するには、width と height を決定する必要があります。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_HEIGHT と Globals.STAGE_WIDTH の値が交換されます。別の見方をすれば、エミュレータは縦向き表示であり、アプリケーションは横向き表示になっています。ソースコードでは、次の 2 つの変数を使用して、ステージの向きとアプリケーションの回転値を追跡します。
Globals.STAGE_HEIGHTがGlobals.STAGE_WIDTHより大きい場合は、Globals.PORTRAIT_ORIENTATIONがtrueに設定されます。- ユーザーがエミュレータの左ソフトキーを押すか、回転ボタンをクリックすると、
Globals.ROTATE_MODEはtrueに設定されます。このアクションを処理するコードはMobileWall.fxにあります。
ステージの向きは回転アクションでは変更されないため、x および y 座標系はアプリケーションによって認識される高さと幅とは一致しません。アプリケーションを回転するとき、シーングラフの遠近の座標はコードでは変更されないため、この不一致は紛らわしい場合があります。つまり、コード自体が x 座標を y 座標に、またはその逆に変換しなければなりません。ただし、このことは、Globals.ROTATE_MODE が使用されている数か所でのみ問題となるだけです。この問題のいくつかについて、次に詳しく説明します。
MobileWall.fx では、回転ボタン
および検索ボタン
は、常に対応するソフトキーと隣接するように配置されます。通常 (回転しないで、エミュレータの向きが 0°)、回転ボタンのアイコンは右から 3 ピクセル、下から 3 ピクセルに配置されます。translateY の計算方法は次のとおりです。
Globals.STAGE_HEIGHT - Constants.ROTATE_BUTTON_HEIGHT - 3
アプリケーションを回転すると、アイコンは同じ場所に配置されますが、アプリケーションでは、y 軸に沿って幅が認識されます。以前の width は height となり、以前の height は width となります。したがって、ROTATE_MODE が true である場合の、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」ボタンは、エミュレータが横向きのときに回転する必要があります。特に、ノードに対する変換シーケンスに注意してください。変換は次の順番で適用されます。effect、opacity、clip、transforms、scaleX と scaleY、rotate、translateX と translateY。次の MobileSearchTextBox.fx コードでは、左ソフトキーの変数 lsk を、最初は元の位置から約 90°回転し、その後 translateX と translateY 変換のアプリケーションによって所定の位置に移動します。右ソフトキーのアフォーダンスである 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 クラスとキーボードフォーカスを持つ Node は mediabrowser.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 軸上のパンオフセットを追跡し、画像を中央に置くよう初期設定されます。ズームは、viewport を ImageView に追加することにより実行されます。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 関数が呼び出されて、変数 panX と panY の値を再計算し、新しい viewport を ImageView 用に作成します。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 変数の ImageView の bind コードブロック内で使用して、検索アイコンまたはズームアイコンを表示するかどうかを決定できます。
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();
}
}
次のように、変数 toggleZoom は MobileWall.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 のインスタンスのいずれか、または isMediaShowing が false に設定されている場合は、null にすることができます。理由は、「モジュール 1 タスク 4」で説明したとおり、Photo と Video はともに Media から派生しており、変数 toggleZoom は Media.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 を拡大縮小するために使用されます。変換における拡大縮小率は x と y 軸の両方に適用され、サムネイルは比率に合わせて拡大縮小されることに注意してください。また、transforms のシーケンスには、thumbs Group を中央で垂直に配置する translate 変換が含まれることにも注意します。同じ効果は、直接 TranslateX と TranslateY を使用することによっても得られます。
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.widthとb 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関数内で、panXとpanYの境界チェックをコメントアウトしてみます。次のサンプルコードを参照してください。
ソースコード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; }; }
