Android4.xでSurfaceViewのCanvasの描画が一部できないケースについて

#発端
現在開発中のアプリ
https://play.google.com/store/apps/details?id=com.game.sh_crew.rebuildingsaga の
β版オープンテスターから報告があったのは二週間前だった。
　
**「戦闘画面の下の部分だけ、初期表示のあとに描画がされないようです！　画像送りますね！」**


<img width="400" alt="001" src="https://qiita-image-store.s3.amazonaws.com/0/183689/d63d5013-0d3c-a56b-8581-ec823fbfe4bd.png">
　
確かに、画面の下の部分
敵モンスターのHP表示部分が切れている……。
　
これは、なにが起こっているかというと

1. ダンジョンを探索して、モンスターと遭遇
2. 戦闘画面へ遷移し、SurfaceViewに画面を表示（キャラのHPが表示される）
3. 「戦闘開始」ボタンを押下すると、キャラのHP表示消える
4. 戦闘アニメーションが始まり、キャラの攻撃アニメーションが流れる

という流れなのですが
上記の3で、キャラのHPを消えるタイミングで
SurfaceViewのCanvasの最下部の一部分が更新されず
初期表示のまま残ってしまうという不具合なのです。
　
手持ちの**Android7の端末と、Android5の端末では問題なく描画できている**。
「まさか……」と思い、AndroidStudioのエミュレータを
Android4で起動してみると
同様の問題が再現した……。
　
この日から、この問題を調査する日々が始まった……。
　
#情報集め
原因がまったくわからなかったので
WEBで情報を集めてみることにした。
「Android4」「SurfaceView」「Canvas」「描画できない」
　
情報収集の結果、以下のことがわかった。

- Android4.x系でのSurfaceViewでのCanvas描画は、遅い＆描画失敗することがある。
- アップデートすると、治った。


　
どうも、Android4のバグなんじゃないか？っていう論評もあり……。
　
リリースしているアプリのAndroidバージョン要件を4.0から5.0にして
プレイできなくすれば、自分としては楽だったんだけど
やっぱりそれは「逃げ」になるし、なんとかしようと思いました。

そして、以下のサイトを発見。
http://mohammedari.blogspot.jp/2013/07/androidsurfaceview.html?m=1

```
SurfaceViewの描画スレッドで
surfaceHolder.unlockCanvasAndPost(canvas);
としても、画面が更新されない現象に遭遇しました…
機種はXperia SX、Android4.1.2で再現しています。
※ただし、drawメソッドをオーバーライドするとViewが配置された最初の一回だけ描画が行われるようです。
この現象は、xmlで指定せずに、コードで直接SurafaceViewをsetContentView刷ることで解決しました。
```

お！
同じ事象に遭遇している人がいるぞ！
……ってことで、さっそく試してみました。

#SurafaceViewをxmlでなく、Activityで生成するよう実装してみた。

自分のアプリは
Avtivityでxmlファイルで画面レイアウトを読み込み
レイアウト上のViewをフィールド変数に保持し
ビジネスロジックでは、activityのgetter経由で
viewを操作するような設計となっています。
　
xmlファイルを設定し、Viewを取得する処理は以下のような感じ

◆activity_battle.xml（修正前）
```
    <SurfaceView
        android:id="@+id/battle_main"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_above="@+id/battle_dummy01"
        android:layout_alignParentTop="true">
    </SurfaceView>
```

◆ActivityBattle（修正前）
```
setContentView(R.layout.activity_battle);
 viewBattleMain = (SurfaceView) findViewById(R.id.battle_main);
```

これを、xmlファイルのSurfaceViewの記載を削除したうえで、こんな感じになおしました。


◆ActivityBattle（修正後）
```
        RelativeLayout relativeLayout = (RelativeLayout) findViewById(R.id.TableLayoutMain);
        viewBattleMain = new SurfaceView(this);
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(WC, WC);
        params.addRule(RelativeLayout.ABOVE, R.id.battle_dummy01);
        relativeLayout.addView(viewBattleMain, 0, params);
```

これでどうだ！！と思い、エミュレータを起動してみましたが
不具合は治りませんでした。

#解決への糸口をつかむ
いろいろ試しても全然ダメで、数日が経過しました……。

ここまでの問題を整理しなおして考えてみました。

①ActivityBattleのSurfaceViewでの戦闘画面の描画処理で「画面の下部分だけ描画できない」
②問題はAndroid4のみ発生（Android5以降は発生しない）

しかし……なんで「画面の下部分」だけなんだろう……。
しかも絶妙な、以下の部分だけ描画できない……。

<img width="400" alt="002" src="https://qiita-image-store.s3.amazonaws.com/0/183689/66276b9f-399c-58cd-771d-529e11d141dc.png">

このレイアウトの下の部分について見直してみよう！！

ということでactivity_battle.xmlをシンプルにしてみました。

◆activity_battle.xml
```
    <SurfaceView
        android:id="@+id/battle_main"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
    </SurfaceView>
```
これでためしたみたところ
戦闘画面の最下部のボタンが、SurfaceViewで塗りつぶされてしまったが

**Canvasの描画処理は正しく動くではないか！！**
　
削除したのは以下で
battle_dummy01というTextViewの上に
SurfaceViewを表示するよう指示している。
```
        android:layout_above="@+id/battle_dummy01"
        android:layout_alignParentTop="true">
```
　
**Android4では、SurfaceViewのサイズを変更するようなケースでは
初期表示はできるけど、その後の描画でおかしくなるということだ……**

#そして、修正へ……。

レイアウトを以下のように再設計した。

 - layout_aboveを廃止し、SurfaceViewは画面全体を満たすように設計（赤線の部分）
 - ボタンはSurfaceViewの上にかぶらせるように配置（60dp）（黄線の部分）

<img width="400" alt="003" src="https://qiita-image-store.s3.amazonaws.com/0/183689/fbb910c9-d62f-d8ff-5602-88ab8330ea09.png">


これだけだと、ボタンの下にモンスターが隠れてしまうので
SurfaceHolderのsetFixedSizeを調整した。

```
// 最下部のボタンの縦（dp=60）のピクセルサイズを算出
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
battleButtonHeightPix = 60 * metrics.density;

// ディスプレイサイズを保持
Display display = activity.getWindowManager().getDefaultDisplay();
Point p = new Point();
displayWidth = p.x;
displayHeight = p.y;

// SurfaceHolder設定
//holder.setFixedSize(displayWidth, displayHeight);
holder.setFixedSize(displayWidth, displayHeight + (int) (battleButtonHeightPix));
```

コメントアウトしている箇所が修正前のコード。
setFixedSizeはもともと端末ディスプレイサイズを設定していたんですが
画面の高さにボタン分のpixを追加するようにすることで
いままでの表示比率でボタンの上にキャラクターを表示することができるようになった。
（これは自分のアプリ固有の設定かも）

**これで無事にAndroid4でもSurfaceViewでのCanvas描画ができるようになった！！**

#結論
本当は最初に書くべきでしたが
結論として

 - Android4ではSurfaceViewでのCanvas描画について、初期描画以降に失敗することがある
 - SurfaceViewのサイズを変更するようなレイアウト設定が問題
 - SurfaceViewのサイズを変更しないようにレイアウトを設定すれば描画できるようになる。

ということでした。

WEBにはここまで書いた情報は全然、載っていなかったです（英語のサイトでもなかった……）


**今後、誰かの役に立つ情報であれば幸いです！**