世界平和とAndroid
エンジニアの草苅です。
スマートフォンを扱うエンジニアの皆さんは、日々Android のバグに悩まされているのではないかと思います。弊社も類に漏れず様々な Android のバグと戦っています。
特にあんさんぶるガールズ!ではアニメーションはすべて Canvas を利用していることもあり、Android の Canvas 絡みのバグに、頭を痛めています。
Android のバッドノウハウは悩んでいる人みんなで共有した方が、世のため人のためになるのではと思い立ったので、世界平和を願っていくつかまとめてみたいと思います。
1. GPUレンダリングの設定によって Canvas で不具合が発生する
このような問題は、できれば Android OS のバージョンで統一しておいて欲しいのですが、どうも機種依存のようなので、弊社では機種に応じて GPUレンダリングONなのか、OFFなのかを個別で指定しています。
いくつか実例を挙げますと、現在のドコモのツートップ Xperia A と Galaxy S4は GPU レンダリングOFFでなければ WebView 上の Canvas は正常動作しません。それとは逆に、Nexus 7 や Arrows X は GPUレンダリングON でなければ WebView 上の Canvas は正しく表示されません。また、同じ OS のバージョンであっても Galaxy Nexus はGPUレンダリングONだとクラッシュし、Nexus 7はGPUレンダリングONでなければ Canvas が正常に表示されないということが発生します。
そのため、あんさんぶるガールズ!では、ONまたはOFFでしか動作しない端末については、次のようなメソッドを使って、アプリ側で強制的にGPUレンダリング設定を変更しています。
protected void setHardwareAccelerationEnabled(boolean enable) { if (enable) { getWindow().setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); } else { // 下位互換性を保つため、以下のコードをリフレクションで実行 // webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); try { Method setLayerTypeMethod = webView.getClass().getMethod("setLayerType", new Class[] {int.class, Paint.class}); setLayerTypeMethod.invoke(webView, new Object[] {View.LAYER_TYPE_SOFTWARE, null}); } catch (NoSuchMethodException e) { // Older OS, no HW acceleration anyway } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
2. GPUレンダリング設定以外に Canvas 周りのバグがある
例えば Galaxy S3、Xperia SX、Xperia GX などの端末が Android 4.0.4 を搭載しています。CreateJS のコミュニティにもこの問題は上がっており、かなりの期間対処法がなかったのですが、その後、Canvas を clearRect する際、縦横に 1px だけ加えたサイズをクリアするようにすることでクラッシュしなくなるという、華麗なテクニックが編み出され、この方法が EaselJS 0.6 系に取り込まれました。
CreateJS のコミュニティの該当エントリー
http://community.createjs.com/discussions/easeljs/220-samsung-galaxy-s3-stock-android-404-browser-freezescrashes-on-stageupdate
修正のコミットログ
https://github.com/CreateJS/EaselJS/commit/7c02f0d4a7e50908b284623d23e6897f15e3bff4
また、Galaxy S4(Android 4.2.2)に関しては WebView 上で表示させたときだけ、Canvas が正しく表示されない不具合があります。
GPUレンダリングONの場合は、Canvas 全体が常に水色表示で何も出ず、OFFの場合はアニメーションが表示されたり、されなかったりがかなり不安定になってしまいます。もろもろ調べていると、日本語で次のようなブログ記事を見つけました。
Galaxy S4のWebviewで、非同期処理の中でのCanvasの描画がバグってる - 車輪を再発明 / koba04の日記
http://d.hatena.ne.jp/koba04/20130629/1372437341
この記事を見ながら「最新端末で動作しないアプリって…」と絶望していたのですが、諦めず調べていると安定してアニメーションを表示できるスピリチュアルな方法を、編み出すことができました。それはアニメーション表示直前に Canvas をクリアすることです。CreateJS であれば、Stage を作って、まずクリアしてからアニメーションを実行すると WebView 上の Canvas でも安定してアニメーションを表示することができました。
var canvas = document.getElementById('canvas'); var stage = new createjs.Stage(canvas); stage.clear(); // clear した後アニメーションのコードを実行
3. GPUレンダリングOFFで特定のCSSが激重
例えば、今年の初めに発売されてツートップの発売前までドコモが推していた Xperia Z という端末は、WebView 上の Canvas を正常に表示するために GPUレンダリングを強制OFFにしたまでは良かったのですが、ほとんどのページでスクロールが激重になってしまいました。
いろいろ調べていると、shadow 系などアルファブレンディングを行うような css が GPUレンダリングOFF だと重くなってしまうようです。参考までに以下のような記事があります。
[CSS] border-radiusとbox-shadowを組み合わせると、それぞれ単体でのスタイルより5倍重たい!? - YoheiM .NET
http://yoheim.net/blog.php?q=20130713
単純に考えると、GPUレンダリングOFFの端末のみ、これらの css の重いプロパティを使用しない css で上書くというのが良さそうです。
Android には幸いなことに、Java Object を JavaScript Object として WebView に渡せるという、セキュリティ問題がとても発生しやすい、さわやかな機能があるので、これを利用します。そして、css を読み込む際、この WebView がGPUレンダリングOFFであれば、上書きする css を読み込むようにしました(以下は簡易的に書いたサンプルです)。
// this を droid という名前で JavaScript からアクセスできるようにします webView.addJavascriptInterface(this, "droid");
// GPUレンダリングOFFのときのみ、上書き用のCSSを読み込みます。 if (!droid.webView.isHardwareAccelerated()) { var fileref = document.createElement("link"); fileref.setAttribute("rel", "stylesheet"); fileref.setAttribute("type", "text/css"); fileref.setAttribute("href", '<%= path_to_stylesheet 'android_without_hardware_acceleration' %>'); document.getElementsByTagName("head")[0].appendChild(fileref); }
4. キーボード入力時に上下に揺れる端末
Android にはテキストボックスに文字を入力しようとすると、文字入力のたびになぜか画面がスクロールして、激しく上下に縦揺れするという恐ろしい端末があります。
例えばこちらに不具合が上がっています。
https://github.com/scottjehl/Device-Bugs/issues/32
.stop-scroll { outline: none; overflow: hidden; }
$('input[type=text]').on 'focus', (e)-> $('body').addClass('stop-scroll') $('input[type=text]').on 'blur', (e)-> $('body').removeClass('stop-scroll')