2012年4月17日火曜日

画面回転で画像だけ変な位置に

Titanium Mobileを使った2つめのアプリも、必要な機能はすべて作り終わりました。画像をじっくり見せるアプリなので、あとは中身の画像を用意するだけです。これは別途に作業するため、開発は一旦終了。このアプリはiPad専用なのですが、ミニ版としてiPhoneに移植することになりました。iPad版は横長での使用に限定しましたが、iPhone版は縦長でも使用するという話に。というわけで、画面回転に対応させる必要が生じました。そこで新たな問題と遭遇。その顛末、解決までの道のりを書きます。

 

画面の回転に対応させるのは、とても簡単でした。「orientationchange」イベントの処理を追加して、そこでWindow内のUI部品のレイアウトを変更します。複数のWindowがあり、それぞれにレイアウト変更の処理を加えました。メモリー消費を考慮して、ウィンドウは閉じたらメモリー解放する形にしています。画面回転のイベント処理もクリアーする必要から、addEventListenerで追加した処理を、removeEventListenerで削除しなければなりません。削除しやすいように、イベント処理は名前付きの関数を利用します。次のような形で。

// 画面回転の処理
function changeOrientF() {
    var orient = Ti.Gesture.orientation; // 関数で引数eを使わないように、このプロパティで縦横を判断
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        view1.top = 100;
        view1.left = 0;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        view1.top = 0;
        view1.left = 200;
        ...
    }
}

考え方としては、UI部品をグループ分けして、それぞれ別なViewに貼り付けます。画面の縦横回転の際に、View単位で位置を調整してレイアウトを変更するようにしました。この考え方で、まったく問題ないはずでした。

 

いろいろなWindowで縦横回転を試すと、画像を入れたImageViewだけが、回転後に変な位置に表示されます。設定する値が間違ってるのかと思い、画面上にデバッグ用ボタンを追加し、回転アニメーションが終わってから、ImageViewのtopとleftの値を表示させました。すると、両方の値は正常に設定されていて、それとは関係ない位置にImageViewが表示されているのです。当然、明らかなバグです。しかし、どこのバグでしょうか。iOSか、Titanium Mobileか。

解決しなければならないので、症状を調べてみました。何十回と回転させると、表示させる位置にはパターンがあるようです。iPhoneの回転位置は4つあり、それぞれで決まった位置にImageViewが表示されました。また、何十回も続けて回転させると、ImageViewに表示される画像が少しずつ劣化していきます。縦位置と横位置ではImageViewの大きさが違い、一旦小さくなってから大きくなるとき、小さくなった画像を拡大して表示している様子でした。つまり、もとの画像ファイルを毎回読み込んでいるのではなくて、画面上に表示した画像を再利用して、回転したり拡大縮小しているというわけです。これらはすべて、アニメーション機能が動いていて、画質重視ではなく効率重視で設計されているのでしょう。

 

いろいろなWindowで観察していたら、ImageViewを1つしか使っていないと、正常な位置に表示されると分かりました。しかし、複数使っているWindowでも、1つ以外は下に隠れていて、画面上には表示されていません。そんな使い方のWindowでも、ImageViewが複数あると、すべてのImageViewが変な位置に表示されます。下に隠れているはずのImageViewまで、一部が表示されてしまうのです。そう、とんでもない症状です。

そこで、次のように考えました。回転し始めたときは、一番手前のImageViewだけ大きさと位置を変更し、他の下に隠れているImageViewは、時間差攻撃で大きさと位置を変更したら良いかも知れないと。JavaScriptは、次のようにしました。

// 1つだけImageViewを変更する
function changeOrientF() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        imgView1.height = 240;
        imgView1.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        imgView1.height = 320;
        imgView1.width = 427;
        ...
    }
    setTimeout(changeOrient2F, 200); // 0.2秒後に動かす
}
// 残りのImageViewを、時間差を付けて変更する
function changeOrient2F() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        imgView2.height = 240;
        imgView2.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        imgView2.height = 320;
        imgView2.width = 427;
        ...
    }
}

この0.2秒という時間差は、回転アニメーションの時間を見て決めました。最初は0.3秒にしたのですが、少し余裕がありそうに見え、0.2秒に変更して問題なかったので0.2秒に落ち着きました。実際のアニメーション時間より、少し短いかも知れません。結果は、見事に成功でした。ImageViewが正常な位置に表示されます。

しかし、ここで安心してはいけません。あくまで、シミュレータ上での成功だけです。実機に転送して、最終確認をしなければ。さっそく転送した試したところ、見事に失敗でした。最初にシミュレータ上で乱れた位置とは、また別な位置にImageViewが表示されます。振り出しに戻りました。この時点で、少し凹みましたね。いったん成功したと思ったわけですから。

 

こんなときは、上手い珈琲でも飲んで一休みです。珈琲を飲みながら、別な対処方法を思い浮かべます。ImageViewだけ乱れるなら、ImageViewとまったく同じ大きさのViewを用意して、ImageViewを囲むようにして使ったらどうだろうか。さっそく、次のようなJavaScriptを作って試しました。

// ImageViewを定義する部分
var view1i = bbb.createViewF(view1, 240, 320, 120, 0); // 入れ物のView
var imgView1 = bbb.createImgViewF(view1i, strPhotoName, 240, 320, 0, 0);

// 1つだけImageViewを変更する、画面回転の処理
function changeOrientF() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        view1i.height = 240;   // 入れ物のViewを変更
        view1i.width = 320;
        view1i.top = 120;
        view1i.left = 0;
        imgView1.height = 240; // ImageViewは、大きさだけ変更
        imgView1.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        view1i.height = 320;   // 入れ物のViewを変更
        view1i.width = 427;
        view1i.top = 0;
        view1i.left = 53;
        imgView1.height = 320; // ImageViewは、大きさだけ変更
        imgView1.width = 427;
        ...
    }
    setTimeout(changeOrient2F, 200); // 0.2秒後に動かす
}

入れ物のViewと、入れるImageViewは、まったく同じ大きさにします。同じ大きさなので、ImageViewのtopとleftはゼロのまま固定。表示位置の変更は、入れ物となるViewのtopとleftの値で設定するわけです。

さて結果ですが、まずはシミュレータ上で試すと成功。ここで安心してはいけません。次に、実機へ転送して動作確認。やりました。大成功です。変な位置に表示される症状が出なくなり、本来の位置に表示されました。とりあえず、良かったです。

 

Titanium Mobileで開発を始めて、まだ3つめのアプリですが、いろいろと問題に遭遇しました。これからも遭遇しそうです。最初に作ったアプリでは、WebViewで苦労させられました。最近は、ImageViewで苦労しています。ここで以前に書いた、変更前の状態が一瞬だけ表示される症状も、ボタンやラベルで発生することはなく、WebViewとImageViewではほぼ確実に発生します。WebViewも、HTMLの評価結果を画像として表示していると捉えられ、画像を表示するViewが、Titanium Mobileの鬼門かも知れません。

開発効率を重視してTitanium Mobile/Desktopを選びましたが、意外に苦労が多くて困惑気味です。前からJavaやCを使っているので、Objective-Cでも苦労しないと思います。Xcodeに移行したら、今回のような問題が発生しないのであれば、移行する価値はありますね。というわけで、ちょっと悩み中です。

 

追記:表示する位置が変になる症状は、ImageViewだけではありませんでした。Labelでも発生することを発見しました。同じ対策が有効かどうか、あとで確認する予定です。

0 件のコメント:

コメントを投稿