2012年5月7日月曜日

SDK 2.0.1でも描画問題への対処は必要(3)

Titanium SDK 2.0.1GA2で、描画関係のバグが解消されているか調べる話の続きです。iPhone用アプリの開発で経験した、画面を回転させたとき、ImageViewやLabelが変な位置に描画される問題を取り上げます。前の投稿で直っていると書きましたが、対処方法を実施しても消えなかったバグが出なくなっただけで、対処方法を不要するレベルで直っているかは不明でした。そこを調べたので報告します。

 

まずはバグの内容を。画面を回転させたときにアニメーションで表示されますが、縦横表示で同じViewを使い、View上のUI部品の位置を回転時に変更すると、設定したtopやleftの値とは全然違う位置に表示される問題です。発生する条件ですが、複数のImageViewの位置を変更させると起こるようです。単に位置が変になるだけではなく、本来なら下に隠れているImgeViewの一部も表示されてしまいます。対処方法は、次のような形でした。見えているUI部品だけの位置を変更し、下に隠れているUI部品はsetTimeoutで遅延させて変更する方法です。この対処方法でほとんど解消したのですが、たまに実機でのみ、一部のLabelが変な位置に表示されます。

以上にような状態のまま2.0.1GA2で再ビルドすると、実機でも変な位置の表示が解消されました。その点では、問題が解消されたといえます。しかし、本来なら特別な対処方法(一部の設定を遅延させる)を使わなくても、プロパティで指定した位置に表示すべきものです。それが直っているかどうか、シミュレータと実機の両方で確認してみました。

 

まず、対処方法を加えたコードです。画面が回転したときに呼び出される関数として作ってあります。表示中のUI部品だけは位置プロパティを変更し、残りのプロパティ変更は遅延した別関数として作りました。このような形で作ると、シミュレータ上ではバグが完全に消えました。ただし実機でのみ、少しバグが出ます。

// 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;
        ...
    }
} 

このコードを、本来の形に戻します。遅延する関数として2つに分けるのではなく、すべてのプロパティ変更を一緒にして、1つの関数として作ります。具体的なコードは、次のようになります。

// すべての変更を1つにまとめる
function changeOrientF() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        imgView1.height = 240;
        imgView1.width = 320;
        ...
        imgView2.height = 240;
        imgView2.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        imgView1.height = 320;
        imgView1.width = 427;
        ...
        imgView2.height = 320;
        imgView2.width = 427;
        ...
    }
}

ご覧のように、難しい変更ではありません。バグが解消していれば、一部のプロパティ変更を遅延させなくても、正常な位置に表示されるはずです。

 

さて実際に動かした結果ですが、シミュレータでも実機でも、変な位置に表示されるバグは出ませんでした。ImageViewもLabelも、プロパティで設定した位置に表示されます。動作中のいろいろなタイミングでiPhoneを回転しましたが、途中の状態はさておいて、最後には正常な位置で表示しました。アプリのアニメーション中に回転アニメーションが加わっても、いつも正常な位置に収まります。2.0.1GA2では、バグが解消されているようです。

画面回転での表示バグは、いろいろな対処方法を試しましたが、どうしても解決しなかったものでした。SDKのバージョンアップで解消され、本当に良かったです。バグが消えたことで、特別な対処方法を用いる必要がなくなりました。

 

ここまで3回の投稿を整理すると、2.0.1GA2で対処が必要な描画問題は、フラッシュバック症状だけになりました。これは前から対処方法を見付けていますから、ぜんぜん大丈夫です。2.0.1GA2では対処方法をpostlayoutイベント処理で実現しますが、非常に簡単な変更でした。この1つだけで大丈夫になったということは、Titanium SDKのレベルアップではないでしょうか。まさに意味のあるバージョンアップですね。

2.0.1が安定したバージョンになれば、もう安心して公開できます。画像がメインのアプリなので、あとは画像とテキストの制作待ちですが、SDKの安定バージョンが出る頃には、制作も終わっているでしょう。めでたし、めでたし。

2012年5月3日木曜日

SDK 2.0.1でも描画問題への対処は必要(2)

Titanium SDK 2.0.1GA2を使って、描画関係の問題が解消しているか調べる話の続きです。その問題とは、hide中のViewで、ViewにaddしたUI部品の位置や内容を変更してから、Viewをshowしたとき、変更前の状態が一瞬表示されることです。フラッシュバック症状と呼んでいます。前回は、UI部品の変更を箇所を何も変えず、setTimeoutで処理していた箇所を、postlayoutイベント処理に変更した話でした。この簡単な変更だけでも、問題なく動きました。

 

いよいよ今回は、UI部品を変更する箇所の改良です。Titanium SDK 2.0.1に追加された、UI部品の複数プロパティ変更を、1つの変更のように扱う処理を利用します。単独で使うupdateLayout関数と、ペアで使うstartLayout関数とfinishLayout関数の、2種類が用意されています。どちらも、ViewまたはUI部品で使う関数なので、単独のViewまたは単独のUI部品が対象となります。

今回のアプリでは、View上の複数部品を一緒に変更するため、すべての変更が終わってから、変更が完了したと知らせる必要があります。ペアで使う関数しか役に立ちません。実際のコードは次のようにしました。

// 変更の開始を知らせる
imgView.startLayout();
label1.startLayout();
label2.startLayout();
view.startLayout();

// 各UI部品を変更
imgView.top = 120;
imgView.left = 200;
imgView.image = photoName[i];
...

// 変更の終了を知らせる
imgView.finishLayout();
label1.finishLayout();
label2.finishLayout();
view.finishLayout);

// viewを表示
view.show();

シミュレータ上で動かしてみると、まったく変わりません。変更前から正常に動いているため、当然でしょう。フラッシュバック症状が出るかどうか確認するために、postlayoutイベント処理をsetTimeout処理に戻し、遅延時間を1ミリ秒に設定して動かしました。しっかりとフラッシュバック症状が出ます。症状を消すためには、はやりpostlayoutイベント処理が必要でした。

この後、viewをshowする処理の位置などを変更しながら動きを観察していて、大事なことに気付きました。Viewをhideしている状態ですから、描画機能は動いてません。その状態でView上のUI部品を変更しても、プロパティの値が変更されるだけです。そしてViewがshowされたときに、設定されたプロパティで描画内容を生成します。そのshowの最初に、フラッシュバック症状が発生するというわけです。変更前のフラッシュバック症状がshowしたときに出るということこそ、showした時点から描き始めている証拠です。

整理すると、startLayout関数とfinishLayout関数は、showされてる状態で有効なのであって、hideされているときに使っても意味がないのです。もちろん、ここで試さなかったupdateLayout関数も同様です。今回のアプリのようにhide中の変更では、単に無駄に処理を加えているだけとなります。意味なしです。というわけで、startLayout関数とfinishLayout関数を削除しました。

 

新しい関数の使用条件を理解していなかったので、予想外の結果となりました。結局、フラッシュバック症状への対応は、前回の投稿と同じまま、setTimeout処理をpostlayoutイベント処理に変更するだけで完了です。回り道をしましたが、startLayout関数とfinishLayout関数などの役割を理解できたので、良しとしましょう。この結果を知っていたら、前の投稿と一緒に書いて構わなかったですね。まあ、こんなこともあります。

2012年5月1日火曜日

SDK 2.0.1でも描画問題への対処は必要(1)

このブログでは、Titanium Mobileを使った際の画面表示の問題と格闘してきました。少し前の投稿では、リリースされたSDK 2.0.1GA2を使うことで、最大の問題だったImageViewやLabelが変な位置に表示される症状が、直ったとの速報を書きました。あれから使い続けていますが、症状は1回も発生していません。直っているのは確実なようです。

描画関係では他にも問題があり、その1つ1つが直っているのか、直っていないなら同じ対処方法で大丈夫なのか、1つずつ見直したいと思います。まずは、Viewのレイアウト変更への対処を取り上げます。

 

以前の投稿「Viewはshowしたときに描き直されるもの?」で、レイアウトを変更したときの対処方法を書きました。おさらいすると、問題は、hideしていたView上にあるImageViewやLabelの画像やテキストや位置を変えた後、showすると変更前の状態が一瞬だけ表示されることでした。フラッシュバック症状と呼んでいます。対処方法は、Viewの透明度を限りなく透明に設定してからshowし、setTimeoutで遅れて透明度を不透明に戻す方法でした。

まず調べたのは、SDK 2.0.1GA2でも、UI部品を変更する前の状態が一瞬表示されるフラッシュバック症状が出るかどうかです。setTimeoutの遅延時間を極端に短い1に設定して、シミュレータ上の動作を見てみました。結果は、前と同じです。SDK 2.0.1GA2でも、同様のフラッシュバック症状が出ました。つまり、この部分の動きは変わっていないということです。

ただし、何も対処していないわけではありません。SDK 2.0からは、プログラムの作り方で対応するように、新しい機能が追加されています。the UI Layout Systemが更新され、UI部品の大きさに関するデフォルト値が変わりました。同時に、描画での作業終了を考慮した機能が追加されています。それぞれのUI部品ごとに、複数プロパティを変更するときの処理を1つとして扱い、全部が終ったら描画する形も可能になりました。具体的な方法が2つ用意されていて、まずupdateLayout関数では、複数のプロパティを一度に指定できます。もう1つのstartLayout関数とfinishLayout関数はペアで使い、この間にプロパティの変更処理を入れます。

さらに、描画内容を生成し終わるまでの待つ機能が加わっています。それがpostlayoutイベントで、変更するViewやUI部品にイベント処理を加えれば、setTimeoutで処理を遅らせる必要はありません。setTimeoutで遅らせる方法では、少し余裕を持った待ち時間を設定するため、全体として処理が遅くなります。ところがpostlayoutイベントで知らせる方法だと、描画内容を生成し終わったら始められますから、無駄な待ち時間は生じないはずです。

SDK 2.0.1での変更点は、Appceleratorの開発者向けドキュメントに記述してあります。興味のある方は「Transitioning to the New UI Layout System」を読んでみてください。

 

開発中のアプリで、SDK 2.0.1GA2での改良を反映させてみました。フラッシュバック症状は前と同様に出ますから、時間を遅らせて表示させる処理は必要のままです。ただし、実現方法としては、setTimeoutで遅延させる方式から、postlayoutイベントで処理を開始する方式へと切り替えます。これで無駄な待ちが少しは減るでしょう。まずは、これまで実施していたsetTimeoutによるコードです。

// 修正前(setTimeoutを使用)
view1.hide();          // view1を非表示にします
view1.opacity = 0.001; // view1を限りなく透明にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();                  // view1を再表示します
setTimeout(resetOpacityF, 50); // 50ms後に、不透明に戻すfunctionを起動させます
// この関数は、ここで終了

function resetOpacityF(){      // 時間差攻撃で、view1を不透明に戻します
    view1.opacity = 1;
}

これを、処理内容は同じまま、postlayoutイベントで処理するコードに切り替えます。具体的には、次のように作ります。

// 修正後(postlayoutイベントを使用)
view1.hide();          // view1を非表示にします
view1.opacity = 0.001; // view1を限りなく透明にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();                  // view1を再表示します
view1.addEventListener('postlayout', resetOpacityF); // イベント処理を設定します
// この関数は、ここで終了

function resetOpacityF(){      // postlayoutイベントで、view1を不透明に戻します
    view1.removeEventListener('postlayout', resetOpacityF); // イベント処理をクリアします
    view1.opacity = 1;
} 

setTimeoutの代わりとして、postlayoutイベント処理関数をaddEventListenerでViewに加えています。これで描画内容の生成終了待ちとなります。postlayoutイベントが発生すると、設定したイベント処理関数の実行が始まり、まず最初にremoveEventListenerでイベント処理関数を削除し、本来の処理を開始します。以上のように、ほとんど前と同じままで、変更が完了してしまいました。

 

view1をshowした直後にpostlayoutイベント処理を追加し、そのまま待ちます。本来ならshowする前にイベント処理を追加すべきなのですが、描くのに時間がかかるためでしょう、これでも問題なく動きました。さらには、UI部品の変更を1つにまとめる変更もしなければならないのですが、それをする前に試しに動かしたら、正常に動いてしまいました。すぐにremoveEventListenerを実行しているためでしょうね。アプリの動作としては、フラッシュバック症状が発生せず、とくに副作用もありません。こんなに簡単に動いて良いのでしょうか、と疑問に思うぐらい簡単に動きました。

本来であれば、UI部品の箇所も一緒に変更して公開すべきでしょう。でも、簡単な変更でも正常に動いたので、これも面白い情報だと思って公開しました。完全ではない変更でどのように動くかも、意外に貴重な情報となるからです。期待どおりに動かなかったケースで、こういう完全でない変更での動きが、解決方法を見付けるヒントになったりしますので。

 

動いたのを確認しただけでは、ちょっと満足感が不足です。postlayoutイベントが発生するまでの時間はどの程度なのか、やはり気になりますよね。そこで、イベント発生までの時間を計測してみました。addEventListenerの直前に時刻を計り、removeEventListenerの直後にも時刻を計って、差を求めるだけです。イベント処理の追加と削除を含めたのは、これらの処理も含めた経過時間を知りたかったからです。計算した時間差を表示する機能を加えて、実際に実行してみました。

描画内容を生成する時間は、UI部品の種類や数や変更内容によって左右されます。計測結果は、あくまで今回のアプリの場合です。postlayoutイベントを使った箇所は2つで、両方とも測定しました。1番目の箇所は、変更するUI部品の数が6つで、4つがLabel、2つがImageViewです。6つとも位置を変更し、それぞれの値であるテキストと画像も毎回変更します。たまにですが、一部のUI部品で位置だけ変更しない場合もあります。こうした条件のアプリをシミュレータ上で計測したところ、最低では0、最高で41の値となりました。数値の単位はミリ秒で、マシンは現行の13インチMacBook Air(Core i5 1.7GHz Dual)です。発生頻度が一番高いのは0で、全体の3割ぐらいを占めていました。0を含めた一桁台が全体の半分程度ありました。数値が極端に大きいときは、画像をメモリーに読み込んでいるとか、ガベージコレクタが動いているとか、特別な条件なのでしょうか。原因は不明です。

2番目の箇所は、変更するUI部品が半分の3つで、2つがLabel、1つがImageViewです。これらへの変更内容は1番目と同じですが、透明度を変更する処理が加わっています。上記のサンプル・コードは、この2番目の箇所のものでした。計測すると、50〜52と値はほぼ一定でした。このようにバラツキがほとんどないのが普通だと思います。1番目の箇所でバラツキが生じた理由が分かりません。

同じ計測を、実機でも試してみました。初代iPadで実行すると、1箇所目は最低が6で、最高が62でした。全体的に値が大きくなっています。頻度としては小さな値の比率が大きく、とくに一桁が半分程度を占めるという変な結果となりました。バラツキの傾向も非常に似ていて、シミュレータが正常に機能していることを証明した感じです。まあ、当たり前の結果でしょう。2番目の箇所もシミュレータと似ていて、値は56〜67とバラツキは小さいです。1番目の箇所と同様に、シミュレータよりも少し遅くなっています。

余談ですが、setTimeoutでは50に設定していたので、最高が67という結果では、修正前の50という値が小さすぎたのかも知れません。しかし、実機でかなり使いましたが、描画の問題は出ませんでした。postlayoutイベントが発生するタイミングに多少の余裕があるのか、描画内容としてギリギリだと目立たないのか、その辺は分かりません。修正前の50という値は、単なる偶然ですが、一番遅い初代iPadによる実機での動作としては絶妙な値だったのでしょう。

 

計測結果では、片方の描画は非常に短い時間が多くなりましたが、実機を触っている限り体感できません。もともと短い時間なので、少しぐらい減っても体感できないのでしょう。今回の修正により、固定した時間だけ遅らせるのではなく、実際の描画内容生成が終わるまで待つ形になりました。処理としては、より良い形になっています。あまりにも簡単な修正でしたが、最適化された形となりました。今後も、似たような状況では、postlayoutイベント処理を使うでしょう。

UI部品の変更は、次の投稿で書きます。また別な症状の確認も残っていますから、それは後でということで。