2011/11/28

Galaxy Nexus購入

twitterでちょくちょくつぶやいているように、Galaxy Nexusを購入しました。所有しているNexus OneはICSが公式にはサポートされないと言われているし、INFOBARもあそこまで作り込んでいる関係上、簡単にはアップデート提供されないだろうから、まぁ開発用端末に買ってもいいかなと。

■First impression
で、第一印象はとにかくデカイなと・・・これ、とても持ち歩く気になれないですね。いや持ち歩きますけども(笑 Nexus Oneはいまとなっては小さい端末ですが、それにしてもって感じ。


解像度が高いこともあってのこのサイズなのでしょうが、個人的にはINFOBARくらいがベストかなぁ。まぁ、大きいことは大きいけど、薄いのと軽いこともあって持った感じはそんなに悪くないです。

画面解像度は高いけど、ペンタイルということで気にされる方も多いとは思いますが、私個人にはあまり気にならないレベルでした。有機ELは色の出方が強い気がするので、見栄えはしますね。タッチパネルの精度は問題ないと思われます。

少し気になったのは、自動調整では画面が少し暗いことかなぁ?バッテリーをケチる設定にでもなっているのかしら?

あと今回、UK版を輸入したのですが、b-mobile FairのSIMは使えました。でも、電波強度のアンテナがゼロなんですよね。なので何かおかしいのかも知れませんが、とりあえず通信は無事にできてます。

■Ice Cream Sandwich
ICSは、その前にXOOMでHoneycombを触っているので、大体想像の範囲。それもあってか特に違和感なく使えてますけど、やはりデフォルトでメニューキーが無くなったのはいただけない。

実際には、アプリがサポートしている場合はメニューキーは表示されますが、アプリによって画面の上に出たり、下に出たりで一貫性が無い上に、選択項目もリスト形式で表示されるようになったので、携帯のUI的には改悪だと思ってます。

まぁ、タブレットとアプリを共通にする為の措置だとは思いますが、もうちょっと何かうまい具合に解決できないものだろうか?正直マルチタスクボタンを優先する意味はあまり無いと思います。(まぁ、iOSのホームボタンダブルクリックもひどいUIだと思いますが・・・)

■Performance
それでは、気になるパフォーマンスはどの程度でしょうか?Galaxy NexusのMPUはOMAP4460で、これはCPUこそ1.2GHzのDual Coreですが、GPUはPowerVR SGX540とiPhone4相当。画面解像度が1280x720なので、これはちょいと不安が残ります。

まず始めに、Particle Breakを試してみました。ICSにはまだFlash Playerが来てないので、ブラウザでFlashは見れませんが、AIRのランタイムは問題無いようです。動かしてみると、速い速い!画面が大きいので、必然的に指の移動量も多くなることもあって、かなり難易度が上がってしまいました。

Particle Breakは画面をCPUモードでレンダリングしているので、CPUのパフォーマンスアップがそのまま利いた感じですね。

では、3Dの表示はどうかと、以前にRenderScriptの勉強で作ったニャンコカルーセルを実行してみました。以下に動画を貼り付けますが、XOOMと比較しても、そんなに悪く無いような気がします。3Dバリバリのゲームでもしない限りはあまり問題無いのかも?


ちなみに、Honeycomb用のコードがそのまま無編集でICSでも動きました。今回、Galaxy Nexusを購入した目的のひとつにRenderScriptを駆使したUIの実験をしたいことがあったので、この結果にはちょっと勇気づけられました。

■まとめ
このサイズの大きさから常用端末には薦めづらいですが、開発用の端末としていち早くICS上でアプリを作りたい人なら買っても良いかと。でも、もしICSのアップデートまで待てるなら、Galaxy S2 LTEが現世代機では最強端末だと個人的には思ってます。

まぁ、でも来年2月のMobile World Congressでは、Tegra3や最新のSnapdragon S4を搭載したICS機が続々と発表されるでしょうから、そんなに慌てて飛びつくほどのことは無いかも知れません。ってか、Androidって陳腐化が激しいですよね。バージョンアップがもっとタイムリーになされるのなら良いのですが、メーカーの自助努力に頼ったいまの調子では買い時が非常に難しい・・・

あれ?そう言えば、XOOMの購入レポート書いてなかったな、まぁ、ええか?(笑

2011/11/27

macosxでANE

AIR Native Extensionを使ってAIRが標準でサポートしない機能を使ったモバイルアプリを作ることができるようになりました。

では、デスクトップアプリはどうでしょうか?デスクトップアプリの場合は、標準でサポートされていない機能を使いたいというよりは、既にあるコードを流用したり、処理の重いルーチンをネイティブコードに置き換えて高速化したいという要望の方が強そうです。

■サンプルのビルド
まぁ、何はともあれビルドにはまた手を焼くだろうから、簡単なサンプルから始めてみるしかありません。ということで、Adobeが公開しているサンプルを試してみました。

このサンプルがなかなかよく出来てて、順を追って各フォルダーにあるgo.shを使ってビルドして行けば、サンプルのAIRアプリが出来るようになっているのですが、恐らくネイティブコードをビルドするところで、躓くことになると思います。

02 - create platform extension/mac/TestNativeExtension以下にxcodeのプロジェクトファイルがありますので、ダブルクリックしてxcodeを起動してみましょう。

多くの人は以下のようにAdobe AIR.frameworkのところが赤くなっていることでしょう。これはこのframeworkに設定されているパスが正しくないからです。


なので、コンテクストメニューから"情報を見る"を選択して、正しいパスを設定しましょう。


次に"プロジェクト"メニューから"アクティブアーキテクチャーを設定"で、i386が選択されていない場合は、これに設定してください。ANEはx86_64だとうまく動かないようです。


後は、オリジナルのソースだと"Adobe AIR/Adobe AIR.h"という謎のインクルードファイルを指定しているのですが、これを"FlashRuntimeExtensions.h"に置き換えて、ソースと同じ場所にコピーしておきます。

[TestNativeExtension.cppファイル内]
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//#include <Adobe AIR/Adobe AIR.h>
#include "FlashRuntimeExtensions.h"
これで、xcode上でビルドしても問題が出なければ、次からはコマンドライン上でgo.shを使ってビルドしてもきっとうまく行くと思います。

さて、ようやく本題に入れます。

■サンプルを実行
とりあえず、サンプルを実行してみましょう。画面上部のラジオボタンで、ASのコードを使うかネイティブコードを使うか選択できるようになっているのですが、注目してもらいたいのは、画面下部にあるソートの機能です。

試しに、1万件くらいの配列を指定してASとネイティブコードの実行時間を比較して見ると、あれれ、ASの方が速いですね?


10万件にしても、100万件にしても同じです。やはりASの方が速いですね。以下は私の環境で実行した時の値です。
件数 ASコード Nativeコード
10000 0.003sec 0.005sec
100000 0.029sec 0.045sec
1000000 0.352sec 0.448sec

何ででしょうかね?と悩むより前に、ソースコード上のコメントにも「ネイティブコードはCの配列を作るオーバーヘッドがあるのでソートはASより遅いよ」と書いてあります(笑

ってことで、TestNativeExtension.cppのFREObject _Advanced_timeLongOp関数内で、該当のデータをやり取りしている部分を見てみると、
// Copy input array to sort internally
for ( uint32_t i = 0; i < arrayLength; i++ ) {
    result = FREGetArrayElementAt( argv[0], i, &obj );
    if ( result != FRE_OK ) {
        return (FREObject)NULL;
    }

    result = FREGetObjectAsInt32( obj, &largeArray[ i ]);
    if ( result != FRE_OK ) {
        return (FREObject)NULL;
    }
}
Array配列の要素ひとつひとつに対して、Int32型のオブジェクトを生成して確保したメモリー領域に格納しています。 

以下は、ソートした後のデータを元のArray配列に戻すコードですが、基本的にさっきと逆のことをしてます。
// Copy sorted result back
for ( uint32_t i = 0; i < arrayLength; i++ ) {
    result = FRENewObjectFromInt32( largeArray[ i ], &obj );
    if ( result != FRE_OK ) {
        return (FREObject)NULL;
    }

    result = FRESetArrayElementAt( argv[0], i, obj );
    if ( result != FRE_OK ) {
        return (FREObject)NULL;
    }   
}
なるほど、確かにこれはなかなかオーバーヘッドが大きそうなコードです。

このように、異なるプログラミングモデルを用いる場合に(Alchemyなんかもそうなのですが)、レイヤー間を跨ぐデータのやり取りのオーバーヘッドが大きくて、実際は速くならないというようなことがしばしば起こるわけです。

せっかく速くする為にネイティブコードを使いたいのに、返って遅くなるのでは、ANEって全然使えねーじゃん!となるところですが、今回の場合はデータ構造を少し工夫すればオーバーヘッドを軽減できそうです。

具体的には、Arrayでちまちまデータを渡すのでは無くて、ByteArrayにソートするデータをつっこんで、データ列をネイティブコード側で直接ソートできるようにデータを準備します。

まず、AS側のコードですが、
// データを格納するByteArrayを宣言
var byteData:ByteArray = new ByteArray();

// ソートするデータを格納するコード
for ( var i:int = 0 ; i < arraySz ; i++ ) {
    byteData.writeByte(i & 0xff);
    byteData.writeByte((i >> 8) & 0xff);
    byteData.writeByte((i >> 16) & 0xff);
    byteData.writeByte((i >> 24) & 0xff);
}
byteData.position = 0;

// ソート後のデータを取得するコード
var data:int = ((byteData.readByte() & 0xff)) | ((byteData.readByte() & 0xff) << 8) | ((byteData.readByte() & 0xff) << 16) | ((byteData.readByte() & 0xff) << 24);
ネイティブコード側の手間を省く為に、データをリトルエンディアンに並べておくのがコツです。ま、本来はエンディアンに依存しないコードにすべきでしょうけど。

AVM2もリトルエンディアンだと思ったのですが、ByteArray.write/readInt()ではうまくソート出来ませんでした。何か勘違いしてるかな?

では次にネイティブコード側です。
FREObject _Advanced_timeLongOp(FREContext ctx, void* functionData, uint32_t argc, FREObject argv[]) {
    // Byte列を格納する変数
    FREByteArray bytedata; 

    // ByteArrayクラスオブジェクトのバイトを取得
    FREResult result = FREAcquireByteArray(argv[0], &bytedata);
    if ( result != FRE_OK) {
        return (FREObject)NULL;
    }

    qsort(&bytedata.bytes[0], bytedata.length/4, sizeof(int32_t), cmpFcn);

    // ByteArrayクラスオブジェクトを解放する
    FREReleaseByteArray(argv[0]);

    return (FREObject)NULL;
}
え、こんだけ?えぇ、こんだけです。

qsortで直接ソートできる形式でデータ列を格納してあるので、余計なメモリー確保もデータコピーも不要になりました。これらのオーバーヘッドを軽減したことにより、ソートサンプルは以下のようにネイテイブコード側の方がASに比べてかなり速く処理できるようになりました。
件数 ASコード Nativeコード
最適化前
Nativeコード
最適化後
10000 0.003sec 0.005sec 0.001sec
100000 0.029sec 0.045sec 0.009sec
1000000 0.352sec 0.448sec 0.079sec

■あとがき
まぁ、今回のように極端に最適化できる例はあまり無いかも知れませんが、基本的にデータのやり取りのオーバーヘッドは大きいので、データ量が多い時は、ByteArrayにデータをつっこんで受け渡しするってのが、常套手段になるような気がします。

[Updated]
ソースコードのライセンスがよくわからないので、patchファイルだけane-labにアップして置きました。

2011/11/20

Processing + ADK

この間、Basculeさん主催のMAKEイベントに参加したのですが、ProcessingやArduinoを用いたADK(The Android Open Accessory Development Kit)の話しとか、スペースバルーンプロジェクトの話しとか聞けて、かなり面白かったんですよ。

で、前々からADKは何かやってみたいなぁとは思ってたんだけど、基板とか弄るのはちょっと個人的にハードルが高いので、前々から見つけてたここのblogを参考にNexus OneとMacで自分もちょっと遊んでみました。

と言っても、やってることはほとんどそのまんま(笑。でも、まるっきり同じだとつまらないし、せっかくProcessingに興味が沸いたところなので、Nexus Oneの傾きセンサーの値をProcessingの描画パラメータに活用する(しょぼい)サンプルを作りました。

で、前述のblogに詳しいのですが、ADKって結局アクセサリーの認証方法だけ標準化してあって、アクセサリーとAndroid側のアプリの間に流すデータはBulk転送を使って独自のおれおれフォーマットで良いんですね?

まぁ、アクセサリーによって起動するアプリが選択されるから、そういう仕様の方が自由度が大きくて、開発者にとっては作り易いですよね。

では、動作時の動画を以下に貼りつけておきます。左手前のNexus Oneを傾けると立方体の群れが同じように傾いているのがわかりますでしょうか?


オリジナルのままだと傾きセンサーのデータを更新する頻度がちょっと少なすぎて、Processing上での描画がスムーズに見えなかったので、更新頻度を5倍にしてあります。

まぁ、こんな感じでMacとADKを使えば、簡単にAndroid機器と連携した何かを作ることはできそうです(これくらいだったらソケット通信でやればええやんってのは言いっこ無し)。

今回は、どっちかと言うとNexus Oneの方がアクセサリーっぽい役割になってますけど(ADK的にはMacの方がアクセサリー)、逆に例えばMacにKinectをつないで、そこで解析したデータをAndroid側に渡すなどしたら、何か面白いアプリが作れるかも知れませんね。

と、何だか伏線っぽい締め方になってますが、気が向いたら本当にやってみるかも知れませんけど(笑。

あ、今回はコード等は前述のblogからダウンロードできるからいいかな?と思ってましたが、せっかくですのでProcessingのsketchだけ、晒しておきますか。
int angle;
BufferedReader reader;
String line;

void setup() {
  size(440, 330, P3D);
  noStroke();
  fill(255);
  angle = 0;
}

void draw() {
  lights();  
  background(0);
  
  // カメラの設定
  float camZ = (height/2.0) / tan(PI*60.0 / 360.0);
  camera(width/2.0, height/2.0 - 100, camZ, width/2.0, height/2.0, 0, 0, 1, 0);
  
  translate(width/2, height/2, -20);
  int dim = 27;
  
  // 角度の書きこまれたファイルの中身を参照
  try {
    reader = createReader("angle.txt");
    line = reader.readLine();
  } catch (IOException e) {
    e.printStackTrace();
    line = null;
  }
  
  // 角度の文字列データを数値に変換
  if (line != null) {
    angle = (line.charAt(1) - '0') * 100;
    angle += (line.charAt(2) - '0') * 10;
    angle += (line.charAt(3) - '0');
    if (line.charAt(0) == '-') {
      angle = 360 - angle;
    }
  }
  
  // 演算した角度で回転
  rotateZ(radians(angle));

  // 長方形を並べる
  for (int i = -height/2; i < height/2; i += dim*1.4) {
    for (int j = -height/2; j < height/2; j += dim*1.4) {
       pushMatrix();
       translate(i, j, 0);
       box(dim, dim, dim);
       popMatrix();
    }
  }
}

2011/11/18

コマンドラインでANE

随分と久しぶりの更新になってしまいました。タイの洪水の影響とかいろいろあって本業の方が忙しく、まとまった時間を取ることが出来なかったのでした。ようやく落ち着いたので、いろいろ気になってたことをまたぼちぼち試して行きたいと思います。

さて、今回はANEことAIR Native Extensionを取り上げたいと思います。と言っても既に世の中には沢山の事例が出てますので、何を今更という感じでしょうか?

ただ、世の中にあるほとんどのサンプルがFlash Builder4.5.1を持っていることを前提にしてますよね?いやいや、ちょっと待て、俺は持ってねーぞ、という方も中には居らっしゃることでしょう。

そこで、Flex SDKとAIR3.0 SDKとAndroid SDK(Eclipse)でAIR for Android向けにANEを使ったアプリをコマンドラインでビルドする方法を今回は解説します。

その前にANEって何?って人はAKABANAのAIR 3 Native Extension Seriesをひと通り事前に読んで内容を理解しておいてください。

今回は、ExtensionContextやAIR for Androidを拡張するJavaクラスの書き方等は特に解説しません。あくまでも、それのビルド方法にフォーカスして説明します。

ってことで、具体的なビルドの手順は以下のような感じで進めて行きます。

  • ExtensionContextのビルド
  • JARファイルの作成
  • ANEファイルの作成
  • AIRアプリのビルド
  • Android apkファイルの作成

0. AneLab-HelloWorldの入手
説明を進めるのに、題材としてane-labにあるhelloworldを使いたいと思いますので、入手してください。
$ svn checkout http://ane-lab.googlecode.com/svn/trunk/mobile/android/java/helloworld helloworld
以降、コードをcheckoutしたフォルダーを[HELLOWORLD_HOME]と記載します。

1. ExtensionContextのビルド
ExtensionContextというのは、AIRからネイティブライブラリーの中にある関数を呼び出すためのクラスですね。こいつはcompcを使ってSWCにします。ソースは、[HELLOWORLD_HOME]/AneLab-HelloWord-ane/src以下にあります。
$ cd [HELLOWORLD_HOME]/AneLab-HelloWord-ane
$ compc -source-path ./src -include-classes com.example.ane.android.helloworld.HelloWorldExtension -external-library-path [AIR3SDK_HOME]/frameworks/libs/air/airglobal.swc -output ./bin/AneLab-HelloWorld-ane.swc

2. JARファイルの作成
AIR for Androidを拡張するJavaクラスをJARファイルにまとめます。ここだけはコマンドラインでやるのでは無くて、Eclipseの機能を使います。

プロジェクトファイルは[HELLOWORLD_HOME]AneLab-HelloWorld-Java以下にあるので、これをEclipseに既存のプロジェクトとして取り込んでください。

JARファイル化はとても簡単で、まずJARファイルにするプロジェクトを選択したら、コンテクストメニューからExport...を選択します。


すると表示されるダイアログでJAVAフォルダ内のJAR fileを選択し、Next>ボタンを選択して次に進みます。


そしたら、書き出し先を選択(ここでは、[HELLOWORLD_HOME]/AneLab-HelloWorld-ane/platform/android)して、Finishボタンを選択して終了です。


3. ANEファイルの作成
ここまで進んだら、いよいよaneファイルの作成と進みますが、その前に、先程作ったSWCファイルをunzipしたら得られるlibrary.swfを[HELLOWORLD_HOME]/AneLab-HelloWorld-ane/platform/androidに置いておきます。
$ cd [HELLOWORLD_HOME]/AneLab-HelloWorld-ane/bin
$ unzip AneLab-HelloWorld-ane.swc
$ mv library.swc ../platform/android
そしたら、AIR 3 SDKのadtコマンドを使ってANEファイルにパッケージします。
$ cd [HELLOWORLD_HOME]/AneLab-HelloWorld-ane
$ adt -package -storetype pkcs12 -keystore test.p12 -target ane AneLab-HelloWorld-ane.ane extension.xml -swc ./bin/AneLab-HelloWorld-ane.swc -platform Android-ARM -C ./platform/android .
今回のサンプルで使用する認証ファイル(test.p12)に設定されているパスワードに関しては、[HELLOWORLD_HOME]/AneLab-HelloWorld-ane/readme.txtの中身をご参照ください。

4. AIRアプリのビルド
AIRアプリのビルドは通常通りamxmlcコマンドを使えば良いのですが、通常と違うのは外部ライブラリーとして、先程作成したANEファイルを指定することです。AIRアプリは[HELLOWORLD_HOME]/AneLab-HelloWorld-app以下にありますが、コンパイル前に先程作成したaneファイルをlibsフォルダーにコピーしておきます。
$ cd [HELLOWORLD_HOME]/AneLab-HelloWorld-app
$ cp ../AneLab-HelloWorld-ane/AneLab-HelloWorld-ane.ane ./libs
$ cd src
$ amxmlc +configname=airmobile -compiler.external-library-path+=../libs/AneLab-HelloWorld-ane.ane AneLabHelloWorld.mxml
+configname=airmobileを指定するのを結構忘れがちなので、ご注意ください。

5. Android apkファイルの作成
ここまで来れば、最後にadtコマンドを使ってAndroidのapkファイルにパッケージするだけです。事前に作成したアプリのSWFファイルとAIRのアプリケーション記述ファイル(AneLabHelloWorld-app.xml)を[HELLOWORLD_HOME]AneLab-HelloWorld-appにコピーして置きます。

また、コマンドラインでコンパイルする関係上、AneLabHelloWorld-app.xmlの49行目の<content>タグにアプリのswfファイルを指定しておかないとエラーになりますので、編集しておいてください。
$ cd [HELLOWORLD_HOME]/AneLab-HelloWorld-app
$ cp src/AneLabHelloWorld-app.xml src/AneLabHelloWorld.swf .
$ adt -package -target apk -storetype pkcs12 -keystore test.p12 AneLabHelloWorld.apk AneLabHelloWorld-app.xml AneLabHelloWorld.swf -extdir ./libs
普段と違うのは、-extdirにaneファイルを置いてあるフォルダーを指定するくらいですね。

これで無事にapkファイルが作成されたと思いますので、インストールしてちゃんと外部拡張が機能するか確かめてみてください。

■あとがき
今回は既存のファイルを使ったので特にトラブル無く進んだと思いますが、自分で新規に作成する場合は、AIR extension descriptor file内に記述するクラス名を間違えないように注意してください。

ここのクラス名を間違ったばかりに、私は数週間悩みました。ここのクラス名が間違ってても実行時には何もエラーが出ません。なので、何が原因でネイティブコードが呼ばれないのか、非常に悩むことになります。

SWCの作り方が悪いのか?JARの作り方が悪いのか?あるいは、ANEなのか、と。ここに解説してある手順でapkを作成したのに実行時に動作しない場合は、ここを一度疑ってみてください。

あと、いちいち上記手順を手で打ち込んで実行するのは手間なので、itozさんのblogを参考に自動化するスクリプトをantか何かで作った方がいいでしょう。

では、良いaneライフを、Good luck!

2011/08/25

Thoughts on Flash vs HTML5

世の中、何事も対立軸で見た方がわかりやすいし、面白いですよね。最近度々起こるFlash vs HTML5論争もそんな感じなのですが、この論争のきっかけ(のひとつ)を作った敬愛するSteve JobsがAppleのCEOの座を辞職することになったので、これを機会に彼のThought on Flashを真似て、Thoughts on Flash vs HTML5として、自分の考えをまとめてみました。

■FlashとHTML5はそもそも競合する技術なのか?
一部のHTML5支持層はHTML5によって今後Flashは不要になると言う。Flashの多くの支持層は、FlashとHTML5はお互いを排他する存在ではなくて、両方をうまく使ってよりよいものを作って行ければ良いと言う。双方にそれぞれの言い分があるようだ。

じゃー、私はどう思っているのか?と言いますと、私はFlashとHTML5は基本的に競合する技術だと思う。確かに両方をうまく組み合わせて使うことを否定はしないけど、HTML5がFlashのポジションを狙っているのは明らかじゃない?

いまはまだリトルリーグと大リーグくらいの力の差はあるのかも知れないけど、HTML5少年もいつかは大リーガーになることを夢見てこれからも練習に励んでいくんだろう。Flashはその頃には更に先に進んでいるんだろうけど、HTML5で多くの人が必要十分だと思う機能はいずれ実現されると思う。

但し、用語に混乱があるような気がする。HTML5と言っても何がHTML5なのか私にはよくわからない。なので、この手の話はFlash vs その他のWeb技術と捉えるようにしている。もっとぶっちゃけると、Flashを無くしたいと思っている人達と、無くしたく無いと思っている人達の論争なんだろうな、と。

■何でそんなにFlashを無くしたいの?
Flashを無くしたい人達は何でそこまでFlashを嫌うんだろうか?幾つかケースを考えてみた。

1) iOSで見れないから
これはわかる。これは他に選択肢が無いもんね。でも、みんながみんなiOSを使っているわけじゃないんだし、実はFlashを見られない人よりもHTML5を見られない人の方が多いらしいよ?いまんとこね。

2) オープンソースじゃないから
Flash Playerがオープンソースじゃないというのは、いまの時代には嫌うに充分な理由になると思う。自分は、Adobeもそのことは充分承知しているが、オープンにすることで互換性の無いPlayerが広まることを恐れているのでは無いかと勝手に想像している。

だから、Adobeはオープンにしない代わりにみんなの為にいろんなプラットフォームやブラウザ間での互換性を保つ為に血の滲むような多大な努力を払っているんだなと思うようにしている。

まぁ、オープンにしてほしいけどね。

3) プラグインを入れるのが嫌だから
自分もプラグインを入れると何となく不安定になるんじゃないかというイメージは持っているので、なるべくプラグインは入れたく無いという気持ちはよくわかりますし、実際、必要だと思う最低限のものしか入れないようにしてます。でも、常用のブラウザ(Chrome)にはデフォルトでFlash Playerのプラグインが入るようになったんですよ・・・

4) 過去に何か嫌な思いをしたから
過去に糞重いFlashサイトを開いてしまって、すごく迷惑だった?あるいはいきなり大音量が出て恥ずかしい思いをしたとか?何でもそうですけど、一度失った信用を取り戻すというのは大変なことですよね。これに関しては、私から言えることは何もありません。

ただ、Flashが流行り始めたあの頃と違って、いまはそんなにひどいサイトはあまり見かけないと思うんですけどねー。

5) ちょっとした出来心
HTML5によって以前よりもインタラクティブなサイトがFlashを使わなくても作れるようになったというのは歴然たる事実だと思う。だから、Flashイラネとか言ってみたらちょっと受けるかな?とか、ちょっとした出来心で言ってしまったってのが本当のところじゃないのかな?

■FlasherはFlashが無くなったら困るの?
じゃー仮にFlashの無い世界がやって来たとして、Flasherは困るのだろうか?実はそんなに困らないんじゃないのかなぁ?と思ってます。むしろFlashを毛嫌いして、インタラクティブなUIやコンテンツをあまり作ってこなかった人達の方が、HTML5の時代になって、そういうコンテンツを求められた時に困るのでは無かろうか?

AdobeもHTML5対応のツールを今後積極的に出していくと思うし、Edgeを見てみても、それは現在のFlash IDEの使い勝手をある程度踏襲したものになるだろう。であれば、極論すれば、Flasherに取っては出力フォーマットがFlashかHTML5の違いでしか無いのだ。

だからHTML5の時代が来ても、インタラクティブなUIや演出に長けたFlasherの仕事は減ることは無く、むしろ増える方向に行くのでは無いだろうか?HTML5にはまた違ったKnowHowが当然求められるだろうが、Flashで一回乗り越えた人達の方が、いろいろと鼻は効くような気がする。

それに、最終的にAdobeはFlash IDEでFlashコンテンツを作っても、プラグインが無いとHTML5にフォールバックするようなコンテンツを作れるようにするでしょ?それは絶対間違いないでしょ?だからHTML5派の人もFlashを使えるようになってても損はしないと思うけどな。

■まとめ
個人的にはHTML5がFlashを駆逐すると言うにはまだまだ時期尚早だと思うし、現実解としてのFlashは今後もしばらく、というかずっと使われることになると思う。だからと言って安泰というわけでも無く、ある領域に置いてはHTML5にその座を譲ることになるのであろう。

だけど、そんなことはどうでもいいことじゃないだろうか?正直、HTML5とFlashのどちらが明日生き残ろうが、大した問題では無い。そんなことより、大好きなあの子に今日フラれるかも知れないことの方がよっぽど重要な問題だよね。

2011/08/21

RenderScriptでカルーセルエフェクト

前回のエントリーでRenderScriptを使ってページめくりの効果を実装してみました。となれば、次はもちろんYouTubeアプリのカルーセル表示ですよね。

既に、3D空間にテクスチャーを貼りつけた矩形を表示する方法はわかりました。なので、これも比較的簡単に実装できそうですね。

■カルーセル表示の実装
今回は、円周上に10面の矩形を並べてグルグル回して遊んでみましょう。前回のエントリーと基本的なところは変わらないので、RenderScriptの中身だけ見ていきます。

1. 矩形の座標を求める(carousel.rs)
10面の矩形を円周上に並べるということは、円に内接する正十角形の座標を求めれば良いですね。ということで、公式通り下図のように求めます。


 コードは以下の部分が該当しますが、これは演算済みのものを初期値として持たせておいても構わないですね。まぁ、このコードも初期化時の1回しか呼ばれないので、大した負荷にはならないとは思いますが。
static void initBitmaps() {
    // ビットマップのアドレスを格納
    Bitmaps_t *b = bitmap;
    b->data = gTex_00; b++;
    b->data = gTex_01; b++;
    b->data = gTex_02; b++;
    b->data = gTex_03; b++;
    b->data = gTex_04; b++;
    b->data = gTex_05; b++;
    b->data = gTex_06; b++;
    b->data = gTex_07; b++;
    b->data = gTex_08; b++;
    b->data = gTex_09; b++;

    // 正十角形の一辺の長さを得る
    len = RADIUS * 2 * sin(M_PI/NUM_ITEMS);

    // 矩形の座標を求める
    float angle;
    for (int i = 0; i < NUM_ITEMS; i++) {
        angle = i * 360 / NUM_ITEMS;
        vertices[i*3] = sin(angle * M_PI / 180) * RADIUS;
        vertices[i*3 + 1] = 0;
        vertices[i*3 + 2] = -cos(angle * M_PI / 180) * RADIUS;
    }
}
2. カメラを配置(carousel.rs) 矩形を空間に配置したら、後はカメラを配置すれば良いです。これはモデルビューの座標を設定するところでやればいいですね。
static void displayCarousel() {    
 
    ※コードは該当部分だけ抜粋してます

    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f); // カメラ位置
    rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f);
    rsgProgramVertexLoadModelMatrix(&matrix);

}
この辺りの値は、並べる矩形の数や円周の半径の大きさにもよるので、トライ&エラーで合わせ込む他は無いんでしょうかね?本当は演算で求められたらすっきりするのでしょうが。

3. 矩形の回転(carousel.rs)
矩形の回転は矩形自体を円周に沿って移動させるのでは無くて、単純にカメラをY軸方向に回転させれば同じ効果を得られます。

ので、こちらもモデルビューの座標を設定する部分でカメラの向きを指定するようにします。
static void displayCarousel() {    

    ※コードは該当部分だけ抜粋してます
    
    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f);
    rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f); // カメラを回転
    rsgProgramVertexLoadModelMatrix(&matrix);

}
ユーザーがフリックした場合、はじめは勢い良く回って徐々に減速して止めたいので、描画毎に回転する角度を徐々に小さくするような演算を行ってます。
static void displayCarousel() {    

    ※コードは該当部分だけ抜粋してます

    // カメラの回転角度を演算
    // gVxがフリックの強さ
    if (gVx != 0) {
        rot = rot + gVx;
        gVx = gVx * 0.95;
        if (fabs(gVx) < 0.1) {
            gVx = 0;
        }
    }
    
    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f); // カメラ位置
    rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f); // カメラを回転
    rsgProgramVertexLoadModelMatrix(&matrix);

}
また、カメラは正面を向いた状態では矩形の方を向いていませんので、下図のように18°(18 = 360÷10÷2)だけ回転させたものを初期値として持たせます。


最後に、矩形描画に関する部分のすべてのコードを以下に載せて置きます。
#define NUM_ITEMS 10
static int rot = 360/NUM_ITEMS/2; // カメラの角度の初期値
static void displayCarousel() {
    // Vertexシェーダーの設定
    rsgBindProgramVertex(gProgVertex);

    // 透視変換の設定(透視投影)
    rs_matrix4x4 proj;    
    float aspect = (float)rsgGetWidth() / (float)rsgGetHeight();
    rsMatrixLoadPerspective(&proj, 30.0f, aspect, 0.1f, 1500.0f);    
    rsgProgramVertexLoadProjectionMatrix(&proj);
    
    // Fragmentシェーダーの設定
    rsgBindProgramStore(gProgStoreBlendNone);
    rsgBindProgramFragment(gProgFragmentTexture);
    rsgBindProgramRaster(gCullNone);
    rsgBindSampler(gProgFragmentTexture, 0, gLinearClamp);

    // カメラの回転角度を演算
    // gVxがフリックの強さ
    if (gVx != 0) {
        rot = rot + gVx;
        gVx = gVx * 0.95;
        if (fabs(gVx) < 0.1) {
            gVx = 0;
        }
    }
    
    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f); // カメラ位置
    rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f); // カメラの回転
    rsgProgramVertexLoadModelMatrix(&matrix);

    // 矩形の描画
    Bitmaps_t *b = bitmap;
    for (int i = 0; i < 10; i++) {
        rsgBindTexture(gProgFragmentTexture, 0, b->data);
        rsgDrawQuadTexCoords(
            vertices[i*3],
            -(len/2),
            vertices[i*3+2],
            0,1,
            vertices[i*3],
            len/2,
            vertices[i*3+2],
            0,0,
            vertices[i == 9 ? 0 : (i+1)*3],
            len/2,
            vertices[i == 9 ? 0 + 2 : (i+1)*3 + 2],
            1,0,
            vertices[i == 9 ? 0 : (i+1)*3],
            -(len/2),
            vertices[i == 9 ? 0 + 2 : (i+1)*3 + 2],
            1,1
        );
        b++;
    }
}
■まとめ
上記の通り、比較的簡単にAndroidタブレットで使われているカルーセル表示を実装することが出来ました。今回、実装するにあたっては、clockmakerさんのPapervision3D本を参考にさせていただいてます。

Flashには昔からこのような3Dのエフェクトを多用したテクニックが確立されてますので、RenderScriptでクールなエフェクトを用いたUIを作る際に参考にできる情報が沢山あると思います。

また、はじめからRenderScriptでゴリゴリ実装して思った動作にならなかった場合は、デバッグが非常にやりずらいので、予めPapervision3Dとかを使って試作するなどして勘所を掴んでおけば、デバッグもスムーズに進むかも知れません。

では、いつものようにコードはここに置いておきます。また、実行時の動画も以下に貼りつけておきます。


2011/08/15

RenderScriptでページめくりエフェクト

RenderScriptを使ったアプリで代表的なのがGoogle謹製のBooksとYouTubeアプリらしい。例えば、Booksに関して言うと、恐らくページめくりの部分に使っているんだろうなぁということは容易に想像できますよね。

だが、これを実際にRenderScriptで実装するのはどうやるんだろう?簡単に思いつくのは、Mesh.TriangleMeshBuilder()でテクスチャーを貼り付けるMeshを作成して、rsgDrawMesh()で描画する方法だ。

こいつのやり方はサンプルコード(RsRenderStates)にもあるが、以下のような手順になる。

1. Meshデータの作成(RsRenderStatesRS.java)
private Mesh getMbyNMesh(float width, float height,
                         int wResolution, int hResolution) {
    Mesh.TriangleMeshBuilder tmb = new Mesh.TriangleMeshBuilder(
        mRS, 2, Mesh.TriangleMeshBuilder.TEXTURE_0
    );

    for (int y = 0; y <= hResolution; y++) {
        final float normalizedY = (float)y / hResolution;
        final float yOffset = (normalizedY - 0.5f) * height;
        for (int x = 0; x <= wResolution; x++) {
            float normalizedX = (float)x / wResolution;
            float xOffset = (normalizedX - 0.5f) * width;
            tmb.setTexture(normalizedX, normalizedY);
            tmb.addVertex(xOffset, yOffset);
        }
    }

    for (int y = 0; y < hResolution; y++) {
        final int curY = y * (wResolution + 1);
        final int belowY = (y + 1) * (wResolution + 1);
        for (int x = 0; x < wResolution; x++) {
            int curV = curY + x;
            int belowV = belowY + x;
            tmb.addTriangle(curV, belowV, curV + 1);
            tmb.addTriangle(belowV, belowV + 1, curV + 1);
        }
    }

    return tmb.create(true);
}
Mesh.TriangleMeshBuilder()を用いて、width x heightの領域を
wResolution x hResolutionに分割するMeshデータ(三角形)を生成します。

はじめにテクスチャーの分割設定をsetTexture()で行ない、次に分割したそれぞれの座標に対してaddVertex()で座標を追加してます。

座標は分割したテクスチャーの頂点座標を指定しますが、テクスチャーの中心が(0, 0)に来るような設定にします。

例えば、512x512のテクスチャーを4x4の領域に均等に分割したとすると、テクスチャーと座標の関係は下図のようになります。


次にaddTriangle()でMeshを構成する三角形を追加しますが、三角形の各頂点は座標を指定するのでは無くて、先程設定した頂点座標のインデックスを指定します。その際にインデックスの並びは下図のように、なぜか反時計回りになって居ないとダメなようです。


2. テクスチャーとなるBitmapの読み込み(RsRenderStatesRS.java)
private Allocation loadTextureRGB(int id) {
    return Allocation.createFromBitmapResource(
               mRS, mRes, id,
               Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE,
               Allocation.USAGE_GRAPHICS_TEXTURE
    );
}

private void loadImages() {
    mTexOpaque = loadTextureRGB(R.drawable.data);
    mScript.set_gTexOpaque(mTexOpaque);
}
Allocation.createFromBitmapResource()を用いて、ResourceからBitmapデータのAllocationを作成し、RenderScript側のrs_allocationにバインドします。

3. 描画(renderstates.rs)
static void bindProgramVertexOrtho() {
    // Default vertex shader
    rsgBindProgramVertex(gProgVertex); // Vertex Programをバインド

    // Setup the projection matrix
    rs_matrix4x4 proj;
    
    // 平行投影領域の設定
    rsMatrixLoadOrtho(
        &proj, 0, rsgGetWidth(), rsgGetHeight(), 0, -500, 500
    );

    // 投影領域をロード
    rsgProgramVertexLoadProjectionMatrix(&proj);
}

static void displayMeshSamples() {
    bindProgramVertexOrtho();

    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 128, 128, 0); // 表示位置の設定
    rsgProgramVertexLoadModelMatrix(&matrix);

    // Fragment shader with texture
    rsgBindProgramStore(gProgStoreBlendNone);
    rsgBindProgramFragment(gProgFragmentTexture);
    rsgBindSampler(gProgFragmentTexture, 0, gLinearClamp);
    rsgBindTexture(gProgFragmentTexture, 0, gTexOpaque);

    rsgDrawMesh(gMbyNMesh); // Meshの描画

    rsgFontColor(1.0f, 1.0f, 1.0f, 1.0f);
    rsgBindFont(gFontMono);
    rsgDrawText("User gen 10 by 10 grid mesh", 10, 250);
}
ポイントとなるところだけ抜粋して解説すると、bindProgramVertexOrtho()で透視変換の設定を行ってますが、ここでは平行投影に設定してます。従って、Z軸の座標位置に限らず、オブジェクトはオブジェクトの大きさで描画されます。

後は、rsgDrawMesh()でテクスチャーをバインドしたMeshを描画しているのですが、例によってRenderScript関連の関数を以下にまとめておきます。
rsgBindProgramVertex()Vertexシェーダーをバインド
rsMatrixLoadOrtho()平行投影領域の設定
rsgProgramVertexLoadProjectionMatrix()座標行列を透視変換行列としてロード
rsMatrixLoadTranslate()移動行列をロード
rsgProgramVertexLoadModelMatrix()座標行列をモデルビューの行列としてロード
rsgBindProgramStore()ProgramStore(ハードウェアがフレームバッファにどのように描画を行うかを司る)をバインド
rsgBindProgramFragment()Fragmentシェーダーをバインド
rsgBindSampler()Sampler(データをどのようにテクスチャーバッファーに展開するかを司る)をバインド
rsgBindTexture()テクスチャーをバインド
rsgFontColor()文字の色を設定
rsgBindFont()フォントをバインド
rsgDrawText()文字列を描画

ただ、このやり方の問題点は、Meshの頂点をRenderScript側からいじるためのAPIが(多分)無いので、RenderScript側でページめくりの座標演算が出来ないことだ。Meshのデータ構造がわかればRenderScriptからいじれるのかも知れないが、そんな情報はググッてもどこにも出てきやしないのさ。

だからと言って、頂点座標の演算をJavaコード側で行うのは何か違う気がする。なので、この方法は却下する。

■ページめくりエフェクトの実装
じゃー、他にやりようは無いのだろうか?実は、rsgDrawQuadTexCoords()という関数を使えばRenderScriptから頂点座標をいじれてテクスチャーを貼り付けられる矩形を描画することができる。ヘッダーファイルのコメント中には、パフォーマンスが低いから大量の描画には向かない旨が書かれてあるが、現状ではこれしか使えそうなAPIが無いので、今回はこれを使って実装してみた。

原理的にはすごく単純で、ページの画像Bitmapデータを複数の矩形に分割し、それぞれの矩形座標に対して、ページめくりの動きをつけて、分割したBitmapをテクスチャーとして矩形に貼りつけるだけだ。

1. Bitmapの分割(PageCurlRS.java)
private void loadImages(int id) {
    // もとのbitmapデータを作成
    Bitmap b = BitmapFactory.decodeResource(mRes, id, mOptionsARGB);
    	
    // bitmapを5つの領域に分割
    mScript.set_gTex_00(Allocation.createFromBitmap(mRS,
                    Bitmap.createBitmap(b,   0,   0, 198, 512),
                    Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE,
                    Allocation.USAGE_GRAPHICS_TEXTURE
    ));

    mScript.set_gTex_01(Allocation.createFromBitmap(mRS,
                    Bitmap.createBitmap(b, 198,   0, 116, 512),
                    Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE,
                    Allocation.USAGE_GRAPHICS_TEXTURE
    ));
    	
    mScript.set_gTex_02(Allocation.createFromBitmap(mRS,
                    Bitmap.createBitmap(b, 314,   0,  82, 512),
                    Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE,
                    Allocation.USAGE_GRAPHICS_TEXTURE
    ));
    	
    mScript.set_gTex_03(Allocation.createFromBitmap(mRS,
                    Bitmap.createBitmap(b, 396,   0,  64, 512),
                    Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE,
                    Allocation.USAGE_GRAPHICS_TEXTURE
    ));
    	
    mScript.set_gTex_04(Allocation.createFromBitmap(mRS,
                    Bitmap.createBitmap(b, 460,   0,  52, 512),
                    Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE,
                    Allocation.USAGE_GRAPHICS_TEXTURE
    ));

    // 分割したbitmapのAllocationを管理する為のstructure配列の領域作成
    ScriptField_Bitmaps bitmap = new ScriptField_Bitmaps(mRS, 5);
    mScript.bind_bitmap(bitmap);
}
ページはめくる時に端っこの方が折れ具合が大きくなるので、均等に分割するんじゃなくて、端に行くほど細かく分割してやる(下図)。


2. 座標の演算(pagecurl.rs)
static void calcVertices(float degRot) {
    if (degRot<0) degRot=0;
    if (degRot>MAX_ROTATION)
        degRot=MAX_ROTATION;

    float r = -degRot * DEGREE;				
    float rMod;
						
    // Calculate rotation amounts for each rectangle
    // and store in vStripRots

    // [A] Applies to all degrees
    rMod = boundary1Mod;
			
    // [B] Applies to all degrees > boundary1
    if (degRot > boundary1) {
        // range: 0 to MAX_ROTATION - B1
        float a = degRot - boundary1;

        // range: 0 to B2MOD
        a = a / (boundary2 - boundary1) * boundary2Mod;

        // range: B1MOD to B1MOD-B2MOD
        rMod -= a;
    }	

    float vStripRots[SEGMENT_W + 1];
    // Recursively multiply vStripRots elements by rMod
    for (int i = 0; i < (SEGMENT_W + 1); i++) {
        vStripRots[i] = r;
        r *= rMod;
    }

    // [C] Applies to degrees > boundary2. 
    //    Grow vStripRots proportionally to MAX_ROT.
    //    (Note the 'additive' nature of these 3 steps).
    if (degRot >= boundary2) {
        for (int j = 0; j < (SEGMENT_W + 1); j++) {
            float diff = MAX_ROTATION*DEGREE - fabs(vStripRots[j]);

            // range: 0 to 30
            float rotMult = degRot - boundary2;

            // range: 0 to 1
            rotMult = rotMult / (MAX_ROTATION - boundary2);

            // range: __ to MAX_ROTATION
            vStripRots[j] -= diff * rotMult;
        }
    }
			
    // [2] Create myVertices[]
    for (int k = 0;
         k < (SEGMENT_W + 1) * (SEGMENT_H + 1) * 3;
         k = k + 3) 
    {
        int idx = floor((float)((k/3) / (SEGMENT_H + 1))) - 1;
        myVertices[k]
            = (idx >= 0) ? vStripWidths[idx] : base_vertices[k];
        myVertices[k + 1] = base_vertices[k + 1];
        myVertices[k + 2] = base_vertices[k + 2];
    }

    // [3] Apply rotation to myVerts[]
    for (int l = (SEGMENT_H + 1) * 3;
         l < (SEGMENT_W + 1) * (SEGMENT_H + 1) * 3;
         l = l + 3)
    {
        int idx2 = floor((float)((l/3) / (SEGMENT_H + 1))) - 1;
        myVertices[l] = cos(vStripRots[idx2]) * myVertices[l]
                      - sin(vStripRots[idx2]) * myVertices[l + 2];
        myVertices[l + 1] = myVertices[l + 1];
	myVertices[l + 2] = cos(vStripRots[idx2]) * myVertices[l + 2]
                          + sin(vStripRots[idx2]) * myVertices[l];
    }

    // [4] 'connect' the rectangles
    for (int m = (SEGMENT_H + 1) * 2 * 3;
         m < (SEGMENT_W + 1) * (SEGMENT_H + 1) * 3;
         m = m + 3)
    { // (first 2 edges are fine)
        myVertices[m]     += myVertices[m - (SEGMENT_H + 1) * 3];
        myVertices[m + 2] += myVertices[m - (SEGMENT_H + 1) * 3 + 2];
    }
		
    int i = 0;
    for (int x = 0; x < (SEGMENT_W + 1); x++) {
        vertices[x * 3    ] = myVertices[i++];
        vertices[x * 3 + 1] = myVertices[i++];
        vertices[x * 3 + 2] = myVertices[i++];
	
        int pos = (SEGMENT_W + 1 + x) * 3;
        vertices[pos]     = myVertices[i++];
        vertices[pos + 1] = myVertices[i++];
        vertices[pos + 2] = myVertices[i++];
    }
}
ページめくりの座標演算に関しては、ここのblogにあるコードを参考にした。これはPapervision3Dでページめくりを実装した例だが、Flashにはこのような参考になるコードが山ほどあるので、非常にありがたいですよね。

3. 画面への描画(pagecurl.rs)
static void displayPageCurl() {    
    // Vertexシェーダーの設定
    rsgBindProgramVertex(gProgVertex);

    // 透視変換の設定(透視投影)
    rs_matrix4x4 proj;    
    float aspect = (float)rsgGetWidth() / (float)rsgGetHeight();
    rsMatrixLoadPerspective(&proj, 30.0f, aspect, 0.1f, 1500.0f);    
    rsgProgramVertexLoadProjectionMatrix(&proj);

    // Fragmentシェーダーの設定
    rsgBindProgramStore(gProgStoreBlendNone);
    rsgBindProgramFragment(gProgFragmentTexture);
    rsgBindProgramRaster(gCullNone);
    rsgBindSampler(gProgFragmentTexture, 0, gLinearClamp);
    
    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -1480.0f);
    rsMatrixRotate(&matrix, 0, 1.0f, 0.0f, 0.0f); // 回転しない場合でも
                                                  // 指定しないとダメ 
    rsgProgramVertexLoadModelMatrix(&matrix);
    
    // ページめくりの座標演算
    calcVertices(gVx);
	  
    // 分割した矩形の描画
    Bitmaps_t *b = bitmap;
    for (int j = 0; j < SEGMENT_H; j++) {
        for (int i = 0; i < SEGMENT_W; i++) {
	    rsgBindTexture(gProgFragmentTexture, 0, b->data);
	    b++;
    	    rsgDrawQuadTexCoords(
    	        offset_x + vertices[i*3+j*(SEGMENT_W+1)*3],
                offset_y + vertices[i*3+j*(SEGMENT_W+1)*3+1],
                fabs(vertices[i*3+j*(SEGMENT_W+1)*3+2]),
                0, 1, // テクスチャー左下
                offset_x + vertices[i*3+(j+1)*(SEGMENT_W+1)*3],
                offset_y + vertices[i*3+(j+1)*(SEGMENT_W+1)*3+1],
                fabs(vertices[i*3+(j+1)*(SEGMENT_W+1)*3+2]),
                0, 0, // テクスチャー左上
    	        offset_x + vertices[(i+1)*3+(j+1)*(SEGMENT_W+1)*3],
                offset_y + vertices[(i+1)*3+(j+1)*(SEGMENT_W+1)*3+1],
                fabs(vertices[(i+1)*3+(j+1)*(SEGMENT_W+1)*3+2]),
                1, 0, // テクスチャー右上
    	        offset_x + vertices[(i+1)*3+j*(SEGMENT_W+1)*3],
                offset_y + vertices[(i+1)*3+j*(SEGMENT_W+1)*3+1],
                fabs(vertices[(i+1)*3+j*(SEGMENT_W+1)*3+2]),
                1, 1 // テクスチャー右下
            );
        }
    }
}
ページめくりの効果を立体的に見せたいので、先程の例と違い、今回は透視変換は透視投影に設定しています。ここでの注意点は、モデルを回転させない場合でも、rsMatrixRotate()で回転の指定をしないとモデルが画面に描画されないことです(実際には回転させないので、角度0で設定します)。

始め、このことに気がつかなかったので、二週間くらい無駄にしましたよ・・・

矩形を描画するrsgDrawQuadTexCoords()の引数と実際の矩形、テクスチャーの関係は下図になります。


透視変換が平行投影と透視投影ではテクスチャーのマッピング方法が違う(透視投影の場合はテクスチャーの上下がひっくり返る)ような気がするが、気のせいかも知れない。あと、座標の指定する順番も何か描画に影響があるような気も?


■まとめ
と、まぁ、1つのページを複数の矩形に分割するという、かなりブルートフォースなやり方で無理やりRenderScriptでページめくりを実装してみたが、やはり王道は1つのMeshにページをテクスチャーとして貼りつけ、RenderScriptの中でページめくりの座標演算を行う方法だと思われる。

もし、このようなやり方、あるいは他にもっと良いやり方をご存知の方がいらっしゃれば、是非ご教授くださいまし。例によって、サンプルコードはここに置いてます。

 最後に、動作させた時の動画以下に貼り付けておきます。

2011/07/09

renderscript-examples開設

Google Projectにrenderscript-examplesというプロジェクトを開設しました。RenderScriptに関する情報を少しづつまとめて置いて行くつもりです。

まだ、コンテンツはほとんど何も無いのですが、この間blogに書いたwonderflの重力マウスをRenderScriptに移植したもののソースコードも置いてありますので、興味のある方は見てみてください。

しかし、無数にあるGoogle ProjectにもRenderScriptに関するプロジェクトはまだ無かったみたいで、もしかして世界初?RenderScriptって誰も触ってない?(汗

プロジェクト名をまんまrenderscriptにしようかとも一瞬思ったのですが、やっぱりびびって後ろにexamplesって付けちゃった、てへ

2011/07/02

INFOBAR購入

INFOBARを購入しました。色は、最初はおじさんらしくKUROにしようかと思ったのだが、悩んで末にCHOCOMINTに。

 購入の動機はもちろん、iida UI。Androidなのに、こんなにオサレでええのんか?と思うくらいデザインやエフェクトがかっこいいし、なにより動きが気持ち良い。何と、ローンチに合わせて、こんなサイトも公開されたので、まだ実物を触ったこと無い人は見てみるといいと思います。

まんま、こんな感じ。iida UIじゃなかったら多分Galaxy S2を買ってたかな?

端末は丸っこいから、手に持ったホールド感はいいですね、軽いし。iPhone4は確かにかっこいいんだけど、何となく角が手に痛そう・・・。長時間触ったことないから、見た目の印象でだけだけど。

あと、ハードキーはMENU, HOME, BACKの並び。個人的な好みは、ソニエリのBACK, HOME, MENUの順。NexusOneともまた違うので、ちょい慣れるのに時間がかかりそうです。でも、国内メーカーは、ソニエリ以外はMENU, HOME, BACKの順に統一されているっぽい感じなので、これに慣れれば、次に機種変する時は困らないかも?

個人的に購入前に気になってたのが、画面の解像度がqHD(960x540)と高いので、パフォーマンス的にどうなのか?というところ。ただ、GPUが第2世代のAdreno205だし、CPUも1.4GHzだからか、 Particle Breakは何なく動きましたので、AIR for Android開発者としてはひと安心か?

で、twitterでINFOBARに関する質問を募集したところ、以下のご質問をいただいたので、個人的な見解で回答します。

・電池の持ちはどうですか?
バッテリーが1020mAhなので、不安っちゃー不安ですが、もうちょっと使い込んでみないとわからないですね。私の使い方では2日ぎりぎり持つか?という感じでしたが、NexusOneと比較して明らかに電池の消費が激しいという感じはいまのところしません。

・ 30代女子派遣社員に使いこなせますか?
ど、どうなんでしょう(笑 ただ、iida UIのかっこいいホーム画面に目を奪われがちなのですが、アプリを起動した瞬間に現実?に戻されます。あー、そういえばお前はAndroidだったよなと(笑。

現時点でiida UIの完成度を個々のアプリにまで求めてはいけません(汗・・・なので、スマホ経験者じゃないと、特に初期設定時には手厚いサポートが必要な気がします。

逆に、そこを乗り切れば、後は慣れで徐々に使いこなせて行くのでは無いでしょうか?IMEはATOKをケチらずに購入することを個人的にはお薦めします。

2011/06/24

Titanium Meetup Tokyo #5 参加

6/21に開催されたTitanium Meetup Tokyo #5に参加してきました。

基本的に、各々がモクモクとコーディングをする(だけの)会なのですが、@masuidriveさんを捕まえてTitaniumの深いところまで質問できるのが、個人的には魅力的だと思いました。

ちょうどいま、Titaniumを使って初めて本格的なアプリを作ってて、幾つか質問したいことも溜まってたし、今回は開催地がオフィスの近くということもあって、いろいろ好都合でした。

ってことで、恥を忍んで、質問したことをここに晒そうと思います。

1. Toolbarのあるウィンドウから無いウィンドウに遷移すると黒背景が見える
app.jsでbackgroundColorを黒に設定してました・・・orz お恥ずかしい

2. 階層の深いウィンドウから元のウィンドウに一気に戻る
下図のように、どんどん階層の深いウィンドウに潜って行って、一気に一番はじめのウィンドウに戻りたい時ってありますよね?でも、どうやればいいんでしょう?


これは、増井さんも良いやり方がわからないとのことでした。でも、必要性はわかっていただけたと思うので、そのうちFrameworkでサポートされるかも知れませんね?こういったリクエストを直接なかの人に伝えられるというのも、こういう会合に参加するメリットだと自分は思いますよ。

で、自分は、一番はじめのウィンドウにカスタムイベントを飛ばして、そのハンドラーの中で直下の子ウィンドウをクローズすることで、ムリヤリ実現していたのですが、増井さんもいまはそれしかなかろうということでした。

まぁ、これでやってやれないことは無いのですが、やってみるとわかりますが、元に戻るのに結構時間がかかります。一瞬ハングったかと思うくらい・・・多分、内部ではいろいろ後始末に追われているんでしょうね(汗

3. アプリケーションがバックグラウンドに回った/戻った時のイベント
アプリケーションが動作中に電話がかかってきたなどで、バックグラウンドに回ることがあるじゃないですか?あるいは、ユーザーがホームボタンを押したとか。そういう時、あるいは逆にバックグラウンドからまた復帰した時のイベントは無いのか?ということを最後にお尋ねしました。

これに関しては、ドキュメント化されていないけど、Ti.App.addEventListener()で以下のイベント設定すれば取れるということでした。

pauseアプリがバックグラウンドに回った
resumeアプリがバックグラウンドから復帰

流石にこれは、ドキュメントに書いておかなアカンやろ(笑

Code strong,

2011/06/20

Flasherの為のRenderScript入門

■RenderScriptとは?
RenderScriptはAndroid OS3.0から導入された、ハイパフォーマンスな3Dグラフィックスのレンダリングや演算をC言語(C99の構文)で記述できるAPIです。

これまでも、ハイパフォーマンスなレンダリングを行う手段として、OpenGLをNDKで直接叩くという方法がありましたが、RenderScriptはLLVMで一旦中間コードにコンパイルされたものがアプリケーションに付随され、実行時にそれぞれの環境向けに最適化されたマシンコードに更にコンパイルして実行するという仕組みになっております。

その結果、使用しているCPUやGPUの種類によらず、同一のソースコードでそれぞれの環境に置いて効率の良いパフォーマンスを発揮しますが、当然のこととして、特定の環境向けにカリカリにチューニングされたOpenGLのパフォーマンスには及びません。

ただ、iPhoneと違って、様々なプラットフォーム上に展開しているAndroid OSのデバイスとしては、環境依存を吸収する手段として、なかなか面白い仕組みでは無いでしょうか?

■RenderScriptシステムのレイヤー構造
RenderScriptのシステムは、以下の3つのレイヤーから構成されます。

・Native RenderScript layer
このレイヤーはレンダリングや演算の為のRenderScriptそのものです。上に書いたように、C言語の構文で記述しますが、 RenderScriptは汎用的なCPUのみならず、GPU上でも実行する場合もあり得るので、標準的なCのライブラリーがすべて使えるわけではありません。

Native RenderScriptライブラリーで提供されている主な機能は下記。
  1.  膨大な数の演算関数
  2. ベクターや行列等の基本データ型に変換するルーチン
  3. ログ取得の為の関数
  4. グラフィックスのレンダリング関数
  5. メモリー領域確保の為の関数
  6. 2元、3元、4元ベクトル等の、RenderScriptシステムをサポートするデータ型や構造
・Reflected layer
Reflected layerは、AndroidVMからnative RenderScriptコードにアクセスする為の レイヤーで、Androidのビルドツールが自動生成します。その為、Reflected layerは、Android frameworkからRenderScript内の関数や変数にアクセスする為のエントリーポイントを提供します。

・Android framework layer
Android framework layerは、RenderScript APIや通常のAndroid framework APIから構成され、あなたのアプリケーションのActivityのライフサイクルやメモリーの管理などをします。また、このレイヤーで、Reflected layerを介して、ユーザーの操作をnative RenderScriptのコードに関連づけたりもします。

と、かなり前置きが長くなりましたが、詳しくはここのドキュメントを読んでみてください。Flasher的にはC/C++で記述し、同様にLLVMでActionScriptに変換するAlchemyを使うのと、ノリは似ているかも知れませんね。

今回、RenderScriptを使ってFlashでよく使われるParticleのデモを実装してみたいと思いますが、題材としてWonderflの重力マウス(さらに軽量化してみた)を使用したいと思います。

■重力マウスをRenderScriptに移植
重力マウスを移植するにあたって、以下の4つのファイルを記述します。
Gravity.javaGravityアプリのActivityを記述
GravityView.javaRenderScript用のSurfaceViewを記述
GravityRS.javaRenderScript用のエントリーポイントクラスを記述
gravity.rsRenderScriptのコードを記述

Gravity.java
package com.android.sample;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class Gravity extends Activity {
    private GravityView mView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);        
        mView = new GravityView(this); // RSSurfaceViewの作成
        setContentView(mView);
    }
}
特に何も難しいことはしていません。Activityの中で、アプリケーションで使用するViewを設定していますが、今回はRSSurfaceViewを継承したGravityView(後述)を作成してます。

GravityView.java
package com.android.sample;

import android.renderscript.RSSurfaceView;
import android.renderscript.RenderScript;
import android.renderscript.RenderScriptGL;

import android.content.Context;
import android.view.SurfaceHolder;
import android.view.MotionEvent;

public class GravityView extends RSSurfaceView {

    public GravityView(Context context) {
        super(context);
    }

    private RenderScriptGL mRS; // グラフィックス出力の為にスクリプト等
                                // を付与するコンテクスト

    private GravityRS mRender;  // エントリーポイントクラス

    // Surfaceの構成が変わると呼ばれるハンドラー
    public void surfaceChanged(SurfaceHolder holder, int format,
                               int w, int h) {
        super.surfaceChanged(holder, format, w, h);
        if (mRS == null) {
            // グラフィックスのピクセルフォーマットを指定する為のクラス
            RenderScriptGL.SurfaceConfig sc
                = new RenderScriptGL.SurfaceConfig();
            mRS = createRenderScriptGL(sc);
            mRS.setSurface(holder, w, h); //osのsurfaceをバインド

            // エントリーポイントクラスを作成
            mRender = new GravityRS();
            mRender.init(mRS, getResources(), w, h); // 初期化
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        // 後始末
        if (mRS != null) {
            mRS = null;
            destroyRenderScriptGL();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev)
    {
        // エントリーポイントクラスにタッチした座標、指圧と
        // IDの情報を渡す
        // IDはマルチタッチがサポートされている場合に、
        // タッチした指を識別する為のID
        mRender.newTouchPosition(ev.getX(0), ev.getY(0),
            ev.getPressure(0), ev.getPointerId(0));        
        return true;
    }
}
RenderScriptでレンダリングした内容を表示する為のSurfaceViewクラスです。

surfaceChanged()メソッド内でRenderScriptのコンテキストを生成し、Surfaceをバインドしています。スクリプトのバインディングは、後述するエントリーポイントクラス(GravityRs)の方で行なっています。

また、タッチイベントが発生した際も、エントリーポイントクラスに座標等のタッチに関連する情報を渡します。このように、ユーザーの操作によって発生したイベントは基本的にエントリーポイントクラスに情報を渡して、エントリーポイントクラス側で処理をしてもらいます。

GravityRS.java
package com.android.sample;

import android.content.res.Resources;
import android.renderscript.*;

public class GravityRS {
    public static final int PART_COUNT = 50000; // 総パーティクル数

    public GravityRS() {
    }

    private Resources mRes;
    private RenderScriptGL mRS;      // レンダリングのコンテクスト
    private ScriptC_gravity mScript; // スクリプトファイルへの
                                     // エントリークラス

    // 初期化関数
    public void init(RenderScriptGL rs, Resources res,
                     int width, int height) {
        mRS = rs;
        mRes = res;

        // GLSLコードを用いないシンプルなフラグメントシェーダーを作成
        ProgramFragmentFixedFunction.Builder pfb
            = new ProgramFragmentFixedFunction.Builder(rs);

    // バーテックスプログラムから指定された色を使用する
        pfb.setVaryingColor(true);

        // フラグメントシェーダーをレンダリングコンテクストにバインド
        rs.bindProgramFragment(pfb.create());
        
        // スクリプト内のPoint構造体×パーティクル数の分だけ
        // メモリー領域を確保する
        ScriptField_Point points = new ScriptField_Point(mRS,
                                                         PART_COUNT);
        
        // RenderScriptで表示する幾何学データのコンテナを作成する
        // オブジェクト
        Mesh.AllocationBuilder smb = new Mesh.AllocationBuilder(mRS);

        // pointsを頂点データとしてオブジェクトに追加
        smb.addVertexAllocation(points.getAllocation());

        // データの種類を点座標に設定
        smb.addIndexSetType(Mesh.Primitive.POINT);

        // コンテナを作成
        Mesh sm = smb.create();

        // スクリプトファイルのエントリークラスを生成
        mScript = new ScriptC_gravity(mRS, mRes, R.raw.gravity);

        // スクリプト内のpartMeshにMeshをセット
        mScript.set_partMesh(sm);

        // pointsとスクリプト内のpointをバインド
        mScript.bind_point(points);

        // レンダリングコンテクストにスクリプトをバインド
        mRS.bindRootScript(mScript); 
    }

    // タッチ情報のアップデート
    public void newTouchPosition(float x, float y, float pressure,
                                 int id) {
        mScript.set_gTouchX(x); // スクリプト内のgTouchXにX座標をセット
        mScript.set_gTouchY(y); // スクリプト内のgTouchYにY座標をセット
    }
}
一見、複雑なことをやっているようですが、まずフラグメントシェーダーの設定し、それから座標データを格納するメモリー領域の確保をしてます。その後、ビルドツールで自動生成されるエントリーポイントのクラスをインスタンス化して、スクリプト内の変数やポインターと確保したメモリー領域の関連付けを行ってます。

そして最後に、レンダリングコンテクストにスクリプトをバインドして実行、という感じでしょうか?

gravity.rs
#pragma version(1)
#pragma rs java_package_name(com.android.sample)
#pragma stateFragment(parent)

#include "rs_graphics.rsh"

// staticな変数はエントリーポイントが生成されない
static int initialized = 0;

rs_mesh partMesh; // 描画するデータのコンテナ

// タッチ座標
float gTouchX = 0.f; // X座標
float gTouchY = 0.f; // Y座標

// パーティクルの構造体
typedef struct __attribute__((packed, aligned(4))) Point {
    float2 delta;
    float2 position;
    uchar4 color;
} Point_t;
Point_t *point;

// パーティクルの初期化
void initParticles()
{
    // データサイズを取得
    int size = rsAllocationGetDimX(rsGetAllocation(point));

    float width = rsgGetWidth();   // サーフェイスの幅
    float height = rsgGetHeight(); // サーフェイスの高さ

    uchar4 c = rsPackColorTo8888(0.9f, 0.0f, 0.9f); // 色

    Point_t *p = point;
    for (int i = 0; i < size; i++) {
        p->position.x = rsRand(width);  // 乱数でX座標をセット
        p->position.y = rsRand(height); // 乱数でY座標をセット
        p->delta.x = 0;
        p->delta.y = 0;
        p->color = c;
        p++;
    }
    initialized = 1; // 初期化完了
}

/*
 * root()は描画する毎に呼ばれる
 */
int root() {
    // 初回実行時に初期化
    if (!initialized) {
        initParticles();
    }

    float width = rsgGetWidth(); 
    float height = rsgGetHeight();

    // 指定色でレンダリングサーフェイスをクリア
    rsgClearColor(0.f, 0.f, 0.f, 1.f);
    
    int size = rsAllocationGetDimX(rsGetAllocation(point));

    Point_t *p = point;
    for (int i = 0; i < size; i++) {
        float diff_x = gTouchX - p->position.x;
        float diff_y = gTouchY - p->position.y;
        float acc = 50.f/(diff_x * diff_x + diff_y * diff_y);
        float acc_x = acc * diff_x;
        float acc_y = acc * diff_y;
        p->delta.x += acc_x;
        p->delta.y += acc_y;
        p->position.x += p->delta.x;
        p->position.y += p->delta.y;
        p->delta.x *= 0.96;
        p->delta.y *= 0.96;
        if (p->position.x > width) {
            p->position.x = 0;
        } else if (p->position.x < 0) {
            p->position.x = width;
        }
        if (p->position.y > height) {
            p->position.y = 0;
        } else if (p->position.y < 0) {
            p->position.y = height;
        }
        p++;
    }    
 
    rsgDrawMesh(partMesh); // 座標データ列の描画
    
    return 1; // おおよそ1ms後に再描画
}
最後にいよいよRenderScriptの本体です。

まず、Point構造体がパーティクルのデータ構造を構成しており、初回実行時だけ、initParticles()関数内で、パーティクルの座標や色のデータの初期化を行います。

そして、root()関数が描画毎に呼ばれる関数で、Flasher的にはenterFrameのイベントハンドラーのようなものと思えば、理解がしやすいでしょうか?この中のfor文で各パーティクルの座標データをアップデートしております。ここのコードは重力マウスのコードをほぼ、そのまま丸パ◯リ(笑)

ActionScriptでは、動作スピードを上げる為に、リンクリストを形成しておりましたが、ここではリンクリストは使用しないで、単純に構造体のポインターをfor文で要素の数だけ回すという処理になってます。

パーティクルの座標データの更新が終わったら、rsgDrawMesh()という関数で、コンテナ内のデータを元に描画を行います。

最後に、このファイル中に出てきたRenderScript関連の関数をまとめておきます。
rsGetAllocation()VM側で確保したメモリー領域を返す
rsAllocationGetDimX()メモリー領域のX方向の大きさを返す。今回、データは1次元の配列なので、要素数を返すことと同じか?
rsgGetWidth()サーフェースの幅を返す
rsgGetHeight()サーフェースの高さを返す
rsPackColorTo8888()R, G, B値から色コード(8888形式)を生成
rsRand()乱数値を生成
rsgDrawMesh()コンテナのデータを元に描画

■RenderScriptのパフォーマンス
RenderScriptを使うことで、どれくらいのパフォーマンスが出たのでしょうか?PC上では重力マウスは、楽に60fps出ていますが、手持ちのXOOMのブラウザ経由では、20fpsほどしか出ません。

今回、RenderScriptにはfpsを計測する機能を実装していないので、厳密にはどれくらいのフレームレートが出ているのかはわかりませんが、 フルスクリーン(解像度で4倍以上)で、尚且つパーティクルの数を50000に増やしても、目視で30fpsはゆうに出ているように感じました。

なので、少なく見積もっても5倍以上の性能差はありそうです。モバイル版のFlash Playerに早くStage3Dが搭載されることが待たれますね。

実際のアプリケーションの動作の動画を以下に貼りつけておきます。


■まとめ
難しいことは全て理解しなくとも(私も半分くらいしか理解できてない)、上記ファイルをテンプレートに、RenderScriptファイル中のfor文の中身(パーティクルの動作をつけている部分)を適当に書き換えれば、簡単に遊べると思いますので、腕に覚えのあるFlasherの方に是非お試しいただければと思います。

まぁ、遊ぶにはAndroid 3.xの実機が必要にはなりますがね・・・(^_^;

2011/06/12

書評:Titanium Mobileで開発するiPhone/Androidアプリ


6/10に世界初のTitanium Mobile本こと「Titanium Mobileで開発するiPhone/Androidアプリ」が発売されました。

レビューをほんの少しお手伝いさせていただいた関係で、ご献本いただきましたので、軽く中身をご紹介させていただきます。

第1章はTitanium Mobileを開発したAppcelerator社の紹介やツールの将来展望、そして気になる価格体系の情報など。そしてそして、最後に例のSection3.3問題に関して。昨年のいま頃は、もうこれからどうなるの?って感じで、個人的にTitaniumに対するモチベーションもかなり削がれた時期だったのですが、まさか1年経って開発環境としてこんなに注目を集め、入門書まで刊行されることになるとは、感慨もひとしおですね。

第2章は開発環境のインストールから、Hello worldまで。Titanium Mobileは開発環境もターゲットもマルチプラットフォームに対応しているので、すべての環境を網羅するのは大変だったと思うのですが、非常に丁寧に解説されてます。

第3〜5章は、具体的なアプリの開発を通じて、より詳細にTitanium Mobileの使い方を学べます。Twitterクライアントを題材にしているので、多くの方に非常に参考になる内容では無いでしょうか?また3-4章でスクリプトファイルの分割に関しての解説があるのですが、個人的にはTitanium Mobileでアプリを作る際にわりと肝になる部分ではないかと思いますよ。

第6章は、Titanium Mobile APIリファレンスとなってます。本家Appceleratorのサイトよりも詳細で、豊富なコードサンプルも掲載されており、本書の実に1/3ほどのボリュームをこのAPIリファレンスに費やしています。簡易リファレンスと書いてあるのは単なる著者のご謙遜でしょうね(笑)。このAPIリファレンスの為だけに本書を購入しても良いくらいです。

そして、充実のAppendixへと本書は続きますが、これより先は実際に手に取ってみて皆さんの方で是非ご確認してみてください。拙作のXib2Jsも紹介されてて、うれしい限りです(何でも、Appcelerator社の研修テキストブックにも紹介されてるとか?)。

ってことで、これから始める方にも、既にTitanium Mobileを開発に使っている方にもお薦めの一冊だと思います。

Code strong,

2011/05/14

AIRとTitanium MobileがBonjour

モバイル端末上のアプリケーションデータを、PC上の別アプリケーションに送りたいような場合に、HTTPサーバーを立ててやる方法もあるにはあるんですが、事前にIPアドレスを知っていないといけないとか、もひとつエレガントさに欠けると思うんですよ。

で、PC側がMacintoshでモバイル側がiPhoneならば、Bonjour(リンク)を使ったらネットワークの面倒な設定をしなくても簡単にやり取りを出来るんじゃないかと思い、試してみました。

iPhone側はTitanium MobileがBonjourをサポートしているので、これを利用すれば良さそうです。今回は、Kitchen SinkにBonjourを使ったサンプルがあるので、せっかくだから、これをそのまま使用します。

Mac側はAIRでアプリを作ることに決めているんですが、残念ながらAIRではBonjourをサポートしておりません。仕方ないので、NativeProcessで外部アプリを利用しましょう。

1. dns-sdコマンドラインツール
 Macにはdns-sdというコマンドラインツールがあって、これでBonjourサービスの検索や登録ができます。このツールを使って、接続したい機器のアドレスとポートの情報を取得するまでの手順を確認しましょう。

まず事前に、Kitchen SinkのPlatformタブにBonjourってのがあるので、これを選択しておきます。これは、Bonjourのサービスを起動すると共に、そのサービスとのやり取りを兼ねたサンプルとなっております。

ソースコード(リンク)を見てみると、サービスタイプが _utest._tcp になっているので、以下のコマンドを実行すると、このサービスタイプを提供している機器のインスタンス名を取得できます。
$ dns-sd -B _utest._tcp
インスタンス名が取得できれば、以下のコマンドでアドレスとポートを知ることができます。
$ dns-sd -L <インスタンス名> _utest._tcp
ここまでわかれば、後はAIRで、このアドレスとポートに接続するSocketを作成してあげれば、それを通じてやり取りが出来るようになります。

実際に上記コマンドを実行して、出力されるログを確認してみてください。

2. AIRアプリの作成
AIRで外部のプログラムを起動するには、NativeProcessを使用します。以下のようにして、まずインスタンス名を取得するコマンドを実行します。
var process:NativeProcess = new NativeProcess();

var args:Vector.<string> = new Vector.<string>();
args.push("-B");
args.push("_utest._tcp"); // 今回のサンプルのサービスタイプ

var info:NativeProcessStartupInfo = new NativeProcessStartupInfo();
info.executable = new File("/usr/bin/dns-sd");
info.arguments = args;

process.start(info);
process.addEventListener(ProgressEvent.STANDARD_OUTPUT_DATA,
    onBrowseService);
コマンドの実行結果は標準出力に出ますので、ハンドラー関数の中で以下のようにして、結果を取得し、インスタンス名に当たる部分を抜き出します。
private function onBrowseService(event:ProgressEvent):void{
  var stdo:IDataInput = process.standardOutput;
  var result:Array = stdo.readUTFBytes(stdo.bytesAvailable).split('\n');
  var column:String = String(result[result.length - 3]);
  var index:int = column.search("Instance Name");
  if (index != -1) {
    var instanceName = result[result.length - 2];
    instanceName = instanceName.slice(index); // インスタンス名
  }
}
ここでは、直前の行の"Instance Name"文字列の位置を手がかりにインスタンス名を抜き出してます。また、同名のサービスタイプを提供する機器が複数存在する場合を考慮してません。

インスタンス名がわかれば、以下でアドレスとポートの情報を取得します。
var process = new NativeProcess();

var args:Vector.<string> = new Vector.<string>();
args.push("-L");
args.push(インスタンス名);
args.push("_utest._tcp");
    
var info:NativeProcessStartupInfo = new NativeProcessStartupInfo();
info.executable = new File("/usr/bin/dns-sd");
info.arguments = args;
process.start(info);
process.addEventListener(ProgressEvent.STANDARD_OUTPUT_DATA,
    onResolveService);
同様にコマンドの実行結果は標準出力に出ますので、ハンドラー関数の中で以下のようにして、結果を取得し、アドレスとポートに当たる部分を抜き出します。
private function onResolveService(event:ProgressEvent):void {
  var stdo:IDataInput = process.standardOutput;
  var result:Array = stdo.readUTFBytes(stdo.bytesAvailable)
                                      .split("can be reached at ");
  if (result.length == 2) {
    process.removeEventListener(ProgressEvent.STANDARD_OUTPUT_DATA,
        onResolveService); 
    result = result[1].split(" (");
    var iNamePort:Array = String(result[0]).split(":");
    var address:String = iNamePort[0];     // アドレス
    var port:int = parseInt(iNamePort[1]); // ポート
  }
}
アドレスとポートがわかれば、それとやり取りする為のSocketを作成してあげます。
var socket = new Socket();
socket.connect(address, port);
socket.addEventListener(Event.CONNECT, onConnect);
無事に接続すると、ハンドラー関数が呼ばれます。Kitchen Sinkのサンプルでは、ターゲットに"req"という文字列を送ると、機器情報を返信してきますので、これを受け取れるように受信ハンドラーを設定しておきます。
private function onConnect(e:Event):void {
  socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
  socket.writeUTFBytes("req"); // ターゲットにデータ送信
}

// ターゲットからデータを受信
private function onSocketData(e:ProgressEvent):void {
  var reply:String = socket.readUTFBytes(socket.bytesAvailable);
}
ご参考までに、以下に実際に動作するAIRアプリのソースコードを置いておきます。
http://homepage.mac.com/daoki2/lab/Bonjour/BonjourTest.tar.gz (4KB)

以上のようにすれば、Bonjourを介して、MacとiPhoneで簡単にデータのやり取りを行うアプリを作ることが出来そうです。ただ、Mac以外やAndroidでは、どうすれば良いのでしょうか?

実は、ここ(リンク)にBonjourのJava実装のソースコードがあるので、これを使えばもしかしたらその他のプラットフォームでも同じようなことが出来るかも知れません。

もしうまく動かせたら、是非教えてください (^_^;

2011/04/19

b-mobile Fairレポートその2

昨日、b-mobile Fairの簡単なレポートを報告しましたが、自分の普段のスマフォの使い方を詳しく書かなかったので、あまり参考にならなかったと思います。

今日、平日の典型的なパターン通りに使ってみて、データ通信量がどの程度だったか、確かめてみました。以下、普段の利用パターンです。

・まず、自宅では事情が無い限りは機内モードに設定し、Wi-Fiを有効にしてある
・家を出る時にWi-Fiと機内モードを無効に設定(つまり3Gを有効にする)
・通勤電車内で、twitterとwebサーフィンを少々
・帰宅したら、また機内モードに設定

これで、今日確かめてみたら3MBしか消費してませんでした(笑)。
結局のところ、自分は通勤電車内(往復約1時間)で少しネットをやるくらいなので、ほとんどデータの通信は発生しないみたいですね。もしかして、スマフォ要らん?

後は、稀に休日に出先で調べ物をしたりとか、今後、勉強会でノートPCのテザリングに使った時にどれだけ消費するか?何だけど、目安の250MB/月を大きく超えることはないような気がしてます。詳細は、その機会があった時にまた調べてみようと思います。

あと、スピードに関してですが、AndroidのSpeedTestがようやく東京のサーバーを捕捉してくれたので、比較してみました。

Download/Uploadスピード
NexusOne 2.78/0.38Mbps 
iPhone3G 1.21/0.22Mbps 

計測する場所や、その時のネットワークの状況にもかなり左右されるとは思いますが、自分の体感的にもNexusOneの方が明らかに速く感じるので、やはりドコモ回線はそれなりのスピードが出るようですね。

が、まぁご参考程度にお考えください。

2011/04/18

b-mobile Fair購入レポート

b-mobile Fairを購入して、手持ちのNexus Oneに挿して使ってみました。

実はau EVOのWiMAX&テザリングに魅力を感じてたので、XOOMとのセット販売がされれば購入しようかとも考えてたのですが、セット販売はWi-Fi WALKERのみというナゾ仕様なので、当面手持ちのNexus Oneを活用して、夏モデル以降に期待という戦略に切り替えました。

だって、EVOって海外だと1年前のモデルでっせ?それが日本では最新の携帯として売られるこの悲しさ・・・

それに、WiMAXより3Gの方が受信が安定して速度も出るケースが多そう?なので、自分の使い方とデータ通信量しだいでは、今後もb-mobile Fair + SIMフリー端末で行く可能性もあるかな。

ちなみに、今日、普段の使い方に近いイメージで使ってみて、大体6MBくらいを消費。これだと、250MB/月を下回るので、4ヶ月持つ計算に一応はなるね。まぁ日によってだいぶんと前後するんだろうけど、2500円/月で通信費を運用できれば、かなりお得だ。

で、気になる速度をSpeedTest使って計測してみたけど、Android版がなぜか何回やっても東京のサーバーを捕捉してくれないので、参考程度に海外のサーバーで比較すると、概ねソフトバンク回線の1.5倍ほどスピードが出ている。上りは3倍くらい出ることも珍しくはない感じ。

ここら辺はさすがはドコモ回線というところか?でも端末のCPUの速度も結構違うので、その影響も多少あるのやも知れん。

と言うことで、すっかり古くなってしまったiPhone 3Gは塩漬けにして、しばらくAndroidを常用してみようと考えているんだけど、Nexus Oneさんのバッテリー消費が結構激しいのねん・・・

2011/04/03

AIR for iOSやってみた

AIR2.6 SDKからiOS用のパッケージを作成できるようになりました。ただ、所有しているiPhoneが3Gの為にiOS3.xからアップデートを行っておらず(AIR2.6 SDKで作成できるのはiOS4.x以降のパッケージ)、いままで試す機会がございませんでした。

今回、iPod 4thを手に入れましたので、早速以前にAndroid用に作成したアプリをiOS向けに書きだして、どんなもんか確かめてみました。尚、AIR2.6 SDKの詳細に関しては上条さんのblogに詳しいので、そちらをご参照いただければと思います(リンク)。

1. 開発証明書ファイルとプロビジョニングファイル
iOS用AIRアプリケーションの作成手順に関しては、ここのドキュメント(リンク)が参考になりますので、目を通しておいてください。

iOS用にipaインストーラーファイルを作成するには、Android同様にadtコマンドを使用しますが、その際に、P12ファイル形式のApple開発用証明書ファイルとApple開発用プロビジョニングファイルが必要になります。

既に、iOS用にアプリ開発する環境がセットアップされていれば、開発証明書ファイルはキーチェーンアクセスから書きだすことが出来ます。また、プロビジョニングファイルは、iOS Provisioning Portalからダウンロードできます。

具体的なadtコマンドは以下のようになります。
adt -package -target ipa-debug 
    -keystore iosPrivateKey.p12 -storetype pkcs12 -storepass qwerty12 
    -provisioning-profile ios.mobileprovision 
    HelloWorld.ipa 
    HelloWorld-app.xml 
    HelloWorld.swf icons Default.png

2. アプリケーション記述ファイル
iOS用のアプリケーション記述ファイルで指定できる内容に関しては、ここのドキュメントをご参照ください(リンク)。Androidとは指定する内容や、記述方法がだいぶん違うことにはじめは戸惑いますよね。

まぁ、何はともあれ、これでipaファイルを書きだせば、後はそれをダブルクリックして、iTunes経由で実機に転送というPackager for iPhoneと同様の流れになります。ちなみに、ipaファイルの書きだし時間はPackager for iPhoneと比べて、だいぶん短縮されてます。

で、動かしてみたのですが・・・「お、遅い」。えぇ、Android(NexusOne)と比べて明らかに遅いのです。まぁ、全く同じハードでは無いので、厳密な比較はできないのですが、iPhone4相当のiPodとNexusOneで、そんなにハードウェアの処理速度に違いがあるとは思えません。

比較したアプリが、Bitmapの描画とフィルターを酷使するもの(Particle Break)だったので、恐らく高解像度なRetinaディスプレイが不利に働いているものと思われます。

また、iOSではNativeApplication.nativeApplication.exit()が効かないらしく、アプリ側からアプリを終了させることが出来ません。ホームボタンを押して、アプリがバックグラウンドにまわった時には、一応実行は止まります(アプリを選択することで、動作が再開します)が、ちょいと不便ですわね。

まだ触りはじめたばかりなので、何か勘違いとかしているかも知れませんが、個人的にはややがっかり・・・でも、まだ出たばかりだし、Androidもはじめは遅かったから、今後ランタイムがチューニングされて行くことに期待しましょう。

2011/04/02

てら子14 GoogleAPI

ちょっと遅くなりましたが、東京てら子14 GoogleAPIで発表した内容を公開します。

当日は時間が足りなくて、かなり駆け足でほとんど何も説明の無い状態でしたので、じっくり資料をご参照いただき、ご不明な点がございましたら、twitterにでも質問を投げてください。

・東京てら子14発表資料 Google URL短縮(リンク)

また、ソースコードはSpark Projectの方にアップしておきました(リンク)。
 
コミットしたの1年振りくらいかな(汗

2011/03/21

StageWebViewでGoogle OAuth2.0認証

Google APIを使用してユーザー固有のデータにアクセスする際に、ユーザー認証が必要となります。StageWebViewを使用すると、簡単にGoogleのOAuth2.0認証を行えますので、その方法を紹介します。

前準備として、Google APIを使用するアプリケーションの登録と、認証時のリダイレクト先を用意しておく必要があります。

1. Google APIを使用するアプリの登録
ここの手順(リンク)に従って、Google APIを使用するアプリを登録します。

2. リダイレクト先の用意
OAuth2.0認証を行う際に必要となる、リダイレクト先を用意します。実際にアクセスできるURIならどこでも構わないと思います。

3. OAuth2.0認証を行うAIRアプリの作成
では、OAuth2.0認証を行うStageWebViewを用いたAIRアプリを作成します。

まず、OAuth2.0認証で使用する定数の定義。
private const OAUTH_URL:String =
  "https://accounts.google.com/o/oauth2/auth?response_type=token";
private const CLIENT_ID:String = "xxxx.apps.googleusercontent.com"; 
private const REDIRECT_URI:String = "http://your.redirect.uri";
private const SCOPE:String = "https://www.google.com/m8/feeds/";
OAUTH_URLOAuth2.0認証を行うURL
CLIENT_ID1.で設定したGoogle APIを使用するアプリのID
REDIRECT_URI1.で設定したリダイレクト先のURI
SCOPEOAuth2.0認証を使ってアクセスするGoogleのサービス

次にStageWebViewを作成してOAuth2.0認証を行なうコードです。
var web:StageWebView = new StageWebView();
web.stage = this.stage;
web.viewPort = new Rectangle(0, 0, stage.stageWidth, stage.stageHeight);
web.loadURL(OAUTH_URL + "&scope=" + SCOPE
  + "&redirect_uri=" + REDIRECT_URI
  + "&client_id=" + CLIENT_ID);
web.addEventListener(LocationChangeEvent.LOCATION_CHANGE,
  web_LocationChangeHandler);
StageWebViewはDisplayObjectよりも前面のレイヤーに表示されますが、今回はアプリケーションの全面に表示されるように設定しています。

web.loadURL()で、OAuth2.0認証を行うURLを指定しています。

最後にweb.addEventListener()でLocationChange.LOCATION_CHANGEイベントのイベントハンドラーを設定していまので、GoogleのOAuth2.0認証を行った後に、指定のリダイレクト先に飛ぶと、このイベントハンドラーが呼ばれることになります。

では、イベントハンドラーのコードを見てみましょう。
private function web_LocationChangeHandler(e:LocationChangeEvent):void {
  var url:Array = web.location.split("#");

  // もしリダイレクト先が自分の指定したURLだったら
  // OAUTHトークンをデコードする
  if (url[0] == REDIRECT_URI) {
    var params:Array = String(url[1]).split("&");
    for each (var param:String in params) {
      var vars:Array = param.split("=");
      switch(vars[0]) {
      case "access_token":
        OAUTH_TOKEN = vars[1];
        break;
      default:
        // do nothing
        break;
      }
    }
    web.removeEventListener(LocationChangeEvent.LOCATION_CHANGE,
      web_LocationChangeHandler);
    web.stage = null;
    web.dispose();
  }
}
web.locationで飛んだ先のURLを取得します。ちゃんとOAuth2.0認証に成功して飛んで来た場合には、

http://your.redirect.uri#access_token=$$$$&expires_in=$$$$

という文字列が取得できるはずですので、飛び先のURLが自分が指定したURLの場合は、以降のパラメータをデコードして、OAuthのトークンを取得します。

最後に、OAuthトークンを取得する為に使用したStageWebViewは不要となりましたので、 後始末を行います。

以降、Google APIでユーザー認証が必要となるデータにアクセスする際には、ここで指定したOAuthトークンを使用します。

2011/02/20

Xib2JsでTitaniumアプリ作成入門

前回のエントリーで、Titaniumでアプリ開発をする際の面倒な点として、GUIのRADツールがまだ無いので、拙作のxib2jsというツールを紹介させていただきました。

今回は、もう一歩進んで、xib2jsで具体的にアプリを作成する手順をご紹介させていただこうかと思います。

1. Titaniumアプリの新規プロジェクトを作成
まず、Titanium Developerで新規プロジェクトを作成してください。


2. アプリケーションの画面を設計
次にInterfaceBuilderでアプリの画面を作成します。InterfaceBuilderを立ち上げてiPhoneアプリの作成を選択します(iPadアプリの場合はiPadを選んでください)。


次にTab Bar Controllerを画面構成ウィンドウにドラッグ&ドロップして作成します。


新規作成したTab Bar Controllerにインスペクターウィンドウから名称を設定します。名前は何でも構いませんが、スペース等を入れると後のJavaScriptのコンパイルでエラーが出ますので、ご注意ください。


 それぞれのTabにUIViewを配置しまして、以降は通常のやり方でUIComponentを画面上にドラッグ&ドロップで並べて行きます。



今回は、あまり見栄えはしませんが、Titanium Developerで作成されるデフォルトのアプリケーションと同じようなものにしてみましたが、適宜、いろんなUIComponentを使って画面を作成してみてください。ひと通りのもの使えるようにしてあるつもりです。



使用するUIComponentに名称が設定されてあると、後にxib2jsがJavaScriptのコードに変換する際に、その名称を使用しますので、適宜名称を設定しておきます。名称が未設定の場合はxib2jsの方で、コンポーネントの種類と通し番号で適当な名称に変換します。


[注意]
xib2jsはTab Bar Itemに設定されているラベルの名称でTitanium.UI.Tabを作成するコードを生成します。InterfaceBuilderはデフォルトで"Item 1"のようにスペースの入ったラベルを作成するので、このままでは後ほど生成されたJavaScriptをコンパイルする際に、エラーとなってしまいます。これを避ける為、ここではラベルを"Item1"のようにスペースを削除してます。

画面設計をひと通り終えたら、ファイルを保存しておきます。

3. JavaScriptコードへの変換

InterfaceBuilderで作成したファイルをJavaScriptへ変換するのにxib2jsを使用します。ここから(link)アプリをダウンロードしてください。zipで圧縮してあるので解凍してできた.airファイルをダブルクリックしてインストールします。

xib2jsアプリが起動したら、先程InterfaceBuilderで作成した.xibファイルをアプリにドラッグ&ドロップすると、JavaScriptに変換されます。



画面左上の[Save]ボタンを押して、変換されたコードを保管するディレクトリーを選択します。ここでは、最初に作成したTitianiumアプリのResourceフォルダー以下を指定します。

[注意]
xib2jsはファイルを保存する際に、同名のファイルが存在しても無断で上書きしますので、必要なファイルを誤って上書きしないようにご注意ください

また、xib2js上で作成されたコードは選択してコピー出来ますので、必要な部分のみを選択して、自分のコードにペーストして使用するという方法もあるかと思います。

4.コンパイル&実行

作成されたコードをTitanium Developer上のRun EmulatorでiOSシミュレータを起動します。JavaScriptのコンパイルに成功すると、シミュレータが立ち上がって先程作成した画面がアプリに反映されていることが確認できると思います。


ここで、Tabの順序がInterfaceBuilderとは逆になっているのは、ご愛嬌(笑)。JavaScriptを編集して、Tabの設定順序を変更してください。

このようにxib2jsは、まだInterfaceBuilderで作成した画面を100%完璧に再現するJavaScriptのコードに変換できるわけではありませんので、変換に失敗した場合は寛大な気持ちでコードを適宜修正して使用してくださいまし。

Code Strong,

2011/02/15

MogSnap

iPhoneユーザーの皆様、是非MogSnapという無料アプリをAppStoreからダウンロードして触ってみてください。

これ、最近注目度の上がってきているTitanium Mobileという開発環境を使って作られているのですが、恐らく言わなければネイティブのObjective-Cで作られたアプリだと思われることでしょう。それくらい素晴らしい出来なんです。

自分は少し前からTitaniumを触ってますが、AIRなどと比べてUIはネイティブのCocoa Touchを使いますので、他のアプリとのルックアンドフィールの統一感や、何よりパフォーマンスの面で有利です。

Packager for iPhoneは、現状自分の持っているiPhone 3Gでは使い物にならないくらいの速度しか出ないのに比べて、上述のMogSnapなんかもボタンの表示が(いま現在は)おかしくなるようですが、何とか動かせられます。

まぁ、AIRにはAIRの良い面もあって、例えば、Particle BreakやRealCircleのようなゲームをTitaniumで作るのは、逆にちょっと難しいような気もします。

ただ、Titaniumの場合は、Titaniumが提供する環境で解決できない問題が発生した場合は、ネイティブコードで作成したモジュールを呼び出すという技が使えるので、(もちろんそれ用に別途用意する必要があるので)労力を別にすれば、作れないアプリなど無いのかも知れませんが。

とまぁ、ここまで読んで試しにTitaniumを触ってみようと思われた方、Titaniumの現在の泣き所は、開発ツールがまだ充実していない点です。アプリは基本的にJavaScriptで記述するのですが、GUIのRADツールが現状用意されていないので、いまどきGUIをコードで記述して構築しないと行けません(FlexでいうところのMXMLのような仕組みも無い)。

なので、そこんところが面倒だという方は、拙作のxib2jsというアプリを試してみてください。これは、Interface Builderで設計したUIデータをTitanium Mobile用のJavascriptコードに変換するAIRアプリです(変換精度は要改善)。

そう、AIRはこういうちょっとしたツールを作るのにも便利ですよね。

Titaniumに関する情報は以下のblogに詳しくまとめられておりますので、ご参考にしてくださいまし。

http://d.hatena.ne.jp/donayama/

とにかく、いま日本のTitanium開発者の間ではMogSnapに追いつけ追い越せって感じで盛り上がっていますので、今後も面白いアプリで出てくると良いなぁと思ってます。

Code Strong,

2011/02/07

WebGL Lesson1


初めてのWebGL

やっていることはここに書いてあるのと全く同じです。Chrome9など、WebGLに対応したブラウザなら以下に表示されているはずです。



とりあえず、触ってみましたよレベル。

2011/02/05

De MonsterDebuggerを使ったデバッグ

第1回AIR for Android勉強会で、自分のお薦めの開発スタイルとして、なるべくデスクトップ上で開発するという話をさせていただきました。

AIRの利点はマルチプラットフォーム環境ということです。マルチプラットフォームには2つの意味があります。1つは、MacやLinux、WinなどのOSを超えるという意味でのマルチプラットフォーム。

もうひとつは、デスクトップやモバイル、TVなどの機器を超えるという意味でのマルチプラットフォーム。要するに、AIR for Androidだろうが、所詮AIRアプリなので、それなら開発効率を最優先して、画面の小さなデスクトップアプリとしてあらかた開発してしまい、デバイス固有な部分やパフォーマンスなどの合わせこみは、大体アプリが動き始めてから行えば良いではないか?という発想です。

そういう開発スタイルをとった場合に、デバッグ用のツールとして重宝したのがDe MonsterDebuggerです。

1. インストール/実行方法
De MonsterDebuggerのホームページからAIRファイルをダウンロードして、インストールします。

De MonsterDebuggerを起動すると、Fileメニューに"Export Client Class"というアイテムがあるので、これを選択し、De MonterDebuggerと接続する為のクライアントクラスを自分のプロジェクトディレクトリーを指定して作成します。


最後に、自分のアプリケーション内でMonsterDebuggerのインスタンスを作成しておけば、アプリケーションを起動した際にDe MonsterDebugerと接続します。
import nl.demonsters.debugger.MonsterDebugger;
var debugger:MonsterDebugger = new MonsterDebugger(this);


2. 便利な使い方
De MonsterDebuggerを使う利点は、簡単にログをトレースしたりメモリーリークのチェックができたりすることもあるのですが、個人的にはLive editing機能によってDe MonsterDebugger側から実行時にオブジェクトのプロパティを編集できるのが非常に便利だと感じました。


表示オブジェクトのちょっとした座標やサイズの調整に、コンパイル⇒実行というサイクルを繰り返さないでも済むのは、ありがたいです。

3. 注意点
De MonsterDebuggerをAIR for Androidアプリのデバッグに使う際に、1つだけ注意が必要です。

それは、アプリケーション記述ファイルのsupportedProfilesタグ(下記)を無効、もしくは"desktop"等にしないといけません。
<supportedProfiles>mobileDevice</supportedProfiles>
これは、AIRアプリとDe MonsterDebuggerはLocalConnectionで接続されるのですが、この機能はmobileDeviceプロファイルではサポートされていないからです。

4. ハードウェアキーの割り当て
同様に、adlはmobileDeviceプロファイルが指定されていると、メニューからAndroidのBack/Menu/Searchキーをエミュレーションできるのですが、これが使えなくなりますので、別途Back/Menu/Searchの機能を矢印キー等に割り当てておくこともお薦めします。
private function keyHandler(e:KeyboardEvent):void {
    switch(e.keyCode) {
    case Keyboard.MENU:
    case Keyboard.DOWN:
        //Menu選択時の処理
        break;
    case Keyboard.LEFT:
    case Keyboard.BACK:
        //Back選択時の処理
        break;
    }
}