2015年11月22日日曜日

Cocos2d-xでの複数解像度ディスプレイへの対応方針 その1

過去にいくつかAndroidアプリを作って来ましたが、あまりちゃんと理解していなかった解像度についてCocos2d-xでゲームを作成するにあたり正確に理解する必要が出てきたのでその内容をまとめる。

ちなみに、以前の理解レベルは、

ん、解像度?dpで指定しとけばいいんでしょ?アイコンサイズもとりあえずxhdpiのサイズまで用意しておけばいいんでしょ?


というレベル・・・。

因みにこの話の結論(方針)としては、

  • iOS端末をベースとした低解像度から高解像度の画像をいくつか用意する
  • 動作デバイスの解像度に応じて用意したうちの最適なものを利用する
  • 選択した画像のアスペクト比とデバイス画面のアスペクト比が合わない場合はデバイスに応じて縦に合わせるか横に合わせるかを決める

になります。


さて、ここからが本題。まず解像度という言葉であるがWikipediaによると、

画面解像度(がめんかいぞうど、Display resolutionScreen resolution)とは、慣用的にコンピュータ等のディスプレイに表示される総画素数を指す。

この、総画素数は縦と横のピクセル数を掛け合わせたものになる。
また、総画素数が高くても実際に表示するディスプレイのサイズが大きければ1インチ辺りの画素数が少なくなるため粗い表示になる。
つまり、一般的に”高解像度=美しい”という理解がされているがこの高解像度の背景には今までと同じ画面サイズだけど解像度が上がったんだよという前提がある。
具体的な例で言うとAppleが出したRetinaディスプレイがわかり易いあれは今までと物理的に同じ画面サイズであるが、1インチ辺りの画素数が二倍になりましたよ、だからこれまでよりも綺麗に見えますよという話。

ほうほう、ではiOSではどんな解像度のデバイスがあるのかなと調べてみたところ
端末ピクセルデバイスディスプレイサイズ
iPhone,3G,3GS320x480320x4803.5
iPhone4,4S640x960640x9603.5
iPhone5,5s,5c640x1136640x11364
iPhone6,6s750x1334750x13344.7
iPhone6 Plus,6s Plus1242x22081080x19205.5
iPad,2768x1024768x10249.7
iPad Mini768x1024768x10247.9
iPad 3,4,Air,Air2 1536x20481536x20489.7
iPad Mini 2,3,41536x20481536x20487.9
iPad Pro2048x27322048x273212.9
と、かなり たくさんバリエーションがあることがわかった。
ゲームアプリの背景画像を綺麗に見せたい場合は上記表の2列目の解像度と同じ画像を用意すれば良いが、それはかなり大変。

ましてやAndroidの場合も全てのデバイスの解像度の画像を用意するのはほぼ不可能なので、iOSの代表的な解像度に対応を行いその他についてはこちらのブログでも紹介しているやり方で対応をすることにする。

ひとまず今回は以上。




2015年11月15日日曜日

Cocos2d-xで使うNDKはr9dにする



Cocos2d-xでアプリ開発をしようとして次のコマンドを実行してビルドしたところ

$cocos compile -p android --android-studio


エラーが!!


Android NDK: ERROR:/Users/hoge/android-ndk-r10/sources/cxx-stl/gnu-libstdc++/Android.mk:gnustl_static: LOCAL_SRC_FILES points to a missing file

Android NDK: Check that /Users/hoge/android-ndk-r10/sources/cxx-stl/gnu-libstdc++/4.8/libs/armeabi/thumb/libgnustl_static.a exists or that its path is correct

なんだよファイルはあるだろと確認したところ
r10dではgccのバージョンが4.9となっておりそのためファイルが無いとエラーになっていました。

r10dでビルドする方法を調べてみたところ、公式のフォーラムでr9dを使った方が良いとの情報がありました。

おとなしくr9dを使うことにします。

2015年11月14日土曜日

AdSizeにSMART_BANNERを設定する際の注意点

Androidのアプリに広告を乗せる場合、画面の大きなタブレットと小さな画面のスマートフォンの両方に表示することを考慮しなければいけない。

通常のスマートフォンに表示する標準のバナー(幅320x高さ50)は当然スマートフォンには丁度良いがタブレットの場合はかなり小さく見えてしまう。

そんなときに嬉しいのがスマートバナーである。
スマートバナーではAdView側で現在動作している端末の画面サイズを見て適切なバナーを自動で切り替えてくれる。
そんな素晴らしい設定ではあるが一つ注意がある。

公式の注意書きにもある通り、

AdViewは表示できる十分なスペースが無い場合は何も表示されない。
従って、実装は次のようにxml上でAdViewを表示する領域のサイズを自動で切り替えられるようにすると良い。


参考URL: 超簡単!xmlファイルの変更だけでできるAndroidのタブレット対応

2015年9月5日土曜日

PostgreSQL 無かったらINSERT文について

SQLはSELECT * FROM xxxくらいしか叩けない自分ですが、
DBを構築する必要が出てきたので色々と勉強中です。

早速ですが、DBにINSERTしたいレコードと同じものが無い場合のみINSERTしたい場合どうするか悩みました。

一度レコードの存在をSELECTで調べてその結果を一時的に保持して、
無ければ次にINSERTをすることも考えましたが、一度にそれをしたい!

早速ネットを調べてみると同じような質問が当然のごとくありました。
Postgres: INSERT if does not exist already

そして、この質問に対して次の様な回答がありました。
INSERT INTO example_table
    (id, name)
SELECT 1, 'John'
WHERE
    NOT EXISTS (
        SELECT id FROM example_table WHERE id = 1
    );

ほんのわずかなレースコンディションがあるようですが、これでやりたいことはできるようです。
しかし、なぜこれがうまく動くのかが理解できない!

で、INSERT、SELECT、NOT EXISTSをそれぞれ調べて次のことが分かり無事に理解できました。

  • NOT EXISTは続くSELECTの結果が無い場合trueを返す
  • SELECT 1, 'John'は1, 'John'を表示する
  • INSERT は()の後にSELECT文を書いてその結果を代入することができる
つまり、
  1. SELECT id FROM example_table WHERE id = 1で入れたいレコードがあるかを調べる
  2. WHERE NOT EXISTSで無かった場合実行するという条件を付ける
  3. 条件がtrueであった場合、SELECT 1, 'John'で代入したい値を指定する
  4. その値をINSERTする
ということだと理解しました。



2015年8月19日水曜日

Pythonの変数スコープ

Pythonのサンプルプログラムを見ていて気になることがあった。
例外処理でtry-catch-finallyの外側で変数宣言をしていないのに、tryとfinallyそれぞれで同じ変数を参照していた。 
 
try:
    instance = SomeClass()
    instance.getSomething()

except Exception, e:
    print 'Error %s' % e

finally:
    if instance is not None:
        instance.doSomething()

JavaやC++ではこのような書き方はエラーになるので非常に違和感があった。
そこで調べてみたところ、 Pythonでは変数のスコープは基本的に

  • クラス
  • 関数

この二つしかなくif分や例外等の制御構文ではスコープは存在しないということだった。
参考:Python の名前空間とスコープ

2015年8月18日火曜日

PythonとMacPorts

Pythonを勉強中で環境を構築中。

基本的に必要なモジュールはMacPortsを使ってインストールをしているが、MacPortsでPythonをインストールするとデフォルトでMacに存在するPythonとバッティングする。

virtualenvとかを使えばうまく切り分けられるのだろうが、まだあまり理解できていない。

ひとまず、MacPorts側でどのPythonを使うのかは次のコマンドで設定ができる。
 $ sudo port select python

すると次のように選択肢を表示してくれるので使いたいものを選ぶ
 
Available versions for python:
 none
 python25-apple
 python26-apple
 python27 (active)
 python27-apple

$ sudo port select python

2015年6月21日日曜日

Fragmentでバックキーイベントを拾う際の注意点

Fragmentでバックキーが押されたことを検知したくてこちらのサイトに紹介してある方法と同じように実装をしてみた。
しかし、バックキーが押されたイベントを拾うことができなかった。

 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.myfragment, container, false);

    view.setOnKeyListener(new View.OnKeyListener() {
        @Override
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN)
                Log.w(TAG, “on pressed back key”);
            return false;
        }

    });

    view.setFocusableInTouchMode(true);

    return view;
}

もっとよく調べてみると、バックキー等のハードウェアキーのイベントを拾うためにはそのViewにフォーカスが当たっている必要があることがわかった。
テキストエディット等はタッチすればフォーカスが当たるがFragmentは別の方法でフォーカスを当てる必要がある。

  • 方法1:レイアウトファイルに書いたFragmentの属性にandroid:focusable = "true"を入れる
  • 方法2:javaのコード上でrequestForcus()を呼ぶ

したがって、先ほどのコードは下記のように書く必要がある。

 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.myfragment, container, false);

    view.setOnKeyListener(new View.OnKeyListener() {
        @Override
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN)
                Log.w(TAG, “on pressed back key”);
            return false;
        }

    });

    view.setFocusableInTouchMode(true);
    view.requestFocus();

    return view;
}

2015年6月14日日曜日

Copy as cURL に感動

ウェブページの内容をスクリプトで解析したいということがよくある。
当然結果はcurl等のコマンドラインツールで実行したテキストの形式で受け取りたい。
なので、ブラウザで送るリクエストと同等のものをcurlのオプションや引数の設定をして作らなければならない。

いままではchromeの開発ツールにあるNetworkタグのリクエスト内容を見て位置からコマンドを作っていた。

ふと、リクエストを右クリックしてみると"Copy as cURL"なるものがあった。


まさかと思い試してみると、期待通りcurlのコマンドが!
curl 'http://seica.info/search/hinmoku.aspx' -H 'Cookie: ASP.NET_SessionId=hu2sjzkjdvf2ll3emwmw4xyv; __utmt=1; __utma=19271098.1453152246.1434261901.1434261901.1434261901.1; __utmb=19271098.16.10.1434261901; __utmc=19271098; __utmz=19271098.1434261901.1.1.utmcsr=naro.affrc.go.jp|utmccn=(referral)|utmcmd=referral|utmcct=/nfri/contens/open-db/' -H 'Origin: http://seica.info' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: ja,en-US;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H 'Cache-Control: max-age=0' -H 'Referer: http://seica.info/search/hinmoku.aspx' -H 'Connection: keep-alive' --data '__EVENTTARGET=&__EVENTARGUMENT=&__LASTFOCUS=&__VIEWSTATE=%2FwEPDwUKLTE2NzE0NTU5O...(略)

おそらくweb開発をやっている方からすると当たり前の機能なのでしょうか、
私は・・・感動しました!

2015年6月13日土曜日

AndroidアプリWhat's This公開

以前に作ったアプリの知見を生かしつつ、新しい技術を取り入れることを目標としてWhat's Thisというアプリを作りました。

【ざっくりとした使い方と機能】
・ユーザが事前に任意の画像に対してアプリより対象物の指定と名前、よみがなの設定を行う
・設定後の画像の対象物をユーザがタッチすると、名前が表示され、よみがなが音声として流れる


子供がものの名前を覚えるときに使ったり、人の名前を覚えるときに使って貰えればと思っています。
ご興味がありましたら是非使ってみてください。

https://play.google.com/store/apps/details?id=com.nsunrize.whatsthis&hl=ja






2015年5月9日土曜日

メモリを節約しながら画面サイズに合わせたBitmapの表示方法

CanvasにBitmap(画像ファイル)を表示させるときに、縮小もしくは拡大して表示したいということがある。 このときよく使われる方法はBitmap.createBitmapを呼び出してサイズを変更したBitmapを新しく作る方法であるが、 createという言葉の通り新しくBitmapオブジェクトが生成されることになるためメモリを食ってしまう。

 もっと良い方法が無いか試してみたところ、drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)を 使えばメモリを消費せずに描画できることがわかった。 
第二引数のRectはnullで良いらしいので、第三引数のRectに表示したい画像の座標を入れてやればOKで無事に メモリを節約した状態で拡大縮小表示ができた。

2015年5月7日木曜日

Gralleryから選択した画像のMD5値を取得する

GralleryにIntentを投げて選択した画像が過去に選択したものかを判断するためにMD5値を計算してそれを比べることにした。

まず、GralleryにIntentを投げる。
 @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_GET_CONTENT);
        startActivityForResult(intent, 100);
    }

onActivityResultにて帰ってきたIntentからMD5値を計算する。
 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 100 && resultCode == RESULT_OK) {
            try {
                ContentResolver contentResolver = getContentResolver();

                // Intentより画像ファイルのパスを取得する
                String[] columns = { ImageColumns.DATA };
                Cursor cursor = contentResolver.query(data.getData(), columns, null, null, null);
                if(cursor==null) return;
                cursor.moveToFirst();
                String path = cursor.getString(cursor.getColumnIndex(ImageColumns.DATA));
                
                // ファイルを読んでMD5値を計算する
                FileInputStream fis = new FileInputStream(path);

                MessageDigest md = MessageDigest.getInstance("MD5");
                DigestInputStream dis = new DigestInputStream(fis, md);

                byte [] buffer = new byte[1024];
                while(dis.read(buffer) == 1024) { }

                byte[] digest = md.digest();
                StringBuilder md5 = new StringBuilder();
                for (int i = 0;i < digest.length;i++) {
                    md5.append(Integer.toHexString(digest[i] >> 4 & 0xf));
                    md5.append(Integer.toHexString(digest[i]& 0xf));
                }
                Log.v(TAG, md5.toString());

                fis.close();
                cursor.close();
            } catch (Exception e) {

            }
        }
    }

2015年3月29日日曜日

Bundleに頼らないBitmapの扱い方

必要になった背景

  • Nudiaという画像編集アプリを作ったが、!!! FAILED BINDER TRANSACTION !!!が頻発した
  • 画像を利用するActivityやFragmentそれぞれでメンバ変数にBitmapを保持しており、それら全てでBundleにBitmapを格納していたことが原因
Bundleに保存していると、アプリがシステムから落とされた場合でも、データをリストアすることできるのでとても便利ではある。しかし、Bungleに格納できるデータ容量は有限のため、Bundleに頼らないBitmapの保持方法を考えなければいけない。

考えた案

  1. 編集都度ファイルに保存し、必要な度ファイルからBitmapを読み込む
  2. Bitmapをホストするシングルトンクラスを作りそこを通してアクセスするようにする
1については頻繁に更新読み込みをするため現実的ではないので不採用。消去法的に2になるが一つ問題が出てきた。
その問題とはシングルトン自体がシステムから解放されてしまった場合、ホストされているBitmapも一緒にメモリから消えてしまうことだった。


この問題に対して次の図のように対応をした。
シングルトンとホストされるBitmapは変わらないが、メモリが少なくなってきたこと等をトリガとしてアプリのデータ領域にBitmapのキャッシュを持つようにした。
また、アプリのシステムからの終了後の復帰時にはホストするBitmapがnullであった場合にキャッシュからデータを読み込む様にした。

いまのところ差し当たり問題はないが、今後他のユースケースで問題が出てくるかもしれないので注意が必要。


2015年3月25日水曜日

著作権違反によるアプリリジェクト

そういえば、アプリを公開した後いつまで経ってもGooglePlayの検索結果に現れないためおかしいと思ってデベロッパーコンソールを見てみたところ・・・


リジェクトされていましたwww

以下Googleからの通告内容


下記の問題を解決し、修正したバージョンを公開してください。
削除の理由: コンテンツ ポリシーの知的財産権条項となりすましまたは虚偽の振る舞い条項への違反。詳しくは、知的財産権の侵害なりすましに関するポリシーについてのヘルプ記事をご覧ください。
  • アプリ、または Google Play ストアに掲載されている情報(タイトル、説明、ロゴ、プロモーション用スクリーンショットなど)で、第三者に帰属する保護対象の著作物を不正に使用してはなりません
  • アプリのアイコンやプロモーション用スクリーンショットに、既存のサービスに類似した紛らわしい画像を使用してはなりません
保護対象の著作物には一般に、商品名、ブランド、画像、ロゴ、音楽などの作品が含まれます。
ポリシーに準拠するように、アプリやその掲載情報を変更してください。そのためには、アプリやその掲載情報から、保護対象の著作物に該当する可能性のあるすべてのコンテンツを削除します。
こうしたコンテンツを利用する許可を得ている場合は、Google Play ヘルプセンターからご連絡いただき、許可内容が確認できる正式な証明書をお送りください。
削除はすべて追跡の対象となり、削除が繰り返されるとアプリの公開停止につながります。アプリの停止はデベロッパー アカウントのステータスに影響を与える要因となり、Google Play を利用できなくなる可能性がある点にご注意ください。
この通知は、ご自身が公開中の他のアプリに対する事前通知でもあります。カタログ内の他のアプリ全てが上記のポリシー(ただしこれに限定されません)に違反していないことを直ちに確認することで、今後他のアプリで同様のアップロードの不承認やアプリの停止が生じることを回避できます。アプリを公開する前に、アプリがデベロッパー販売 / 配布契約コンテンツ ポリシーに準拠していることをご確認ください。
今回の判断が誤りであると思われる場合は、こちらの Google Play ヘルプセンターの記事をご利用ください。

どうやらアプリを開いたときに出るトップ画面に操作説明のための入れていた画像が著作権違反となったようでした。
別の著作権が寛容な画像に差し替えたところ無事に公開されました。

Appleのように人手では審査はしていないと思われますので、おそらくアップしたApkを展開して中の画像をGoogleお得意の画像検索をして著作権違反を自動的に調べているのでしょうか?

まあひとまず公開ができたので良かったですが、著作権のあたりもしっかりケアしないといけないという良い教訓になりました。





AndroidアプリNudia公開

ようやく以前より作っていたアプリが完成しました。

Android上で画像を編集するアプリです。
主な編集用途は水玉コラを作るものなのですが、水玉コラを作る前に画像に好きなように書き込みをできるようにしています。

そのため、水着を肌色に塗ってぼかしを入れて水玉コラにすると裸のように見える!?・・・と思います。

是非インストールしてみてください。
https://play.google.com/store/apps/details?id=com.nsunrise.nudia




"201306251500" by Don Le is licensed under CC BY 2.0

2015年3月21日土曜日

FragmentのonCreateOptionsMenuでMenuを追加するときの注意点

あるFragmentを持つActivityがあるとする。
Menuの追加処理は必ずしもActivityだけではなくてFragment側でもonCreateOptionsMenuをオーバライドしてやればできる。
これを利用すると、Fragmentの切り替え時にそのFragmentに合ったMenuを表示することができる。

そのときの注意点。

ダメな例
 
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    super.onCreateOptionsMenu(menu, inflater);
    inflater.inflate(R.menu.main, menu);
}

正しい例
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    super.onCreateOptionsMenu(menu, inflater);
    if(!menu.hasVisibleItems())
        inflater.inflate(R.menu.main, menu);
}
menu.hasVisibleItems()で既にmenuがあることを確認しないと、FragmentがaddされたときにMenuがどんどん増えてしまう。
addする前にremoveしていれば既に追加したMenuは消えてくれるが、removeせずにaddを呼ぶ場合などは注意が必要。


2015年3月1日日曜日

マルチタッチではMotionEvent.ACTION_MASKでマスクする

ScrollViewでマルチタッチを検出したくてScrollViewを継承したカスタムビューを作り、 onInterceptTouchEventにてMotionEvent.ACTION_POINTER_DOWNを待ち構えたが 何故かイベントがキャッチされない。 
調べてみたところ、マルチタッチの場合はMotionEventのactionの値をMotionEvent.ACTION_MASKで マスクしてやらなければいけないらしい。

ダメな書き方
 public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_POINTER_DOWN:
            Log.v(TAG, "Action_pointer_down");
            break;

        case MotionEvent.ACTION_POINTER_UP:
            Log.v(TAG, "Action_pointer_up");
            break;
    }
    return false;
}

正しい書き方
 public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN:
            Log.v(TAG, "Action_pointer_down");
            break;

        case MotionEvent.ACTION_POINTER_UP:
            Log.v(TAG, "Action_pointer_up");
            break;
    }
    return false;
}

なぜこんなことをしなければいけないかはAndroid Developerにちゃんと書いてあった。
It returns the masked action being performed, without including the pointer index bits.

つまり、actionの値にはポインターのインデックスを示すbitがあり、マスクしないとそこの値も含めた値になってしまうためそれぞれのアクションの条件値と異なってしまうということらしい。