cocos2d-xでソフトウェアキーボードの上に入力フィールドを持っていく実装

#背景
- チャットアプリなどでよくある、テキスト入力フィールドがソフトウェアキーボードの真上に移動するようなUIが作りたかった
- 調べてみたがcocos2d-xでそういったUIを作ることについてまとまった記事がなかった
- 今後の自分用のメモも兼ねて記事にまとめてみよう

こんな感じです。
なお、以下は全てiOS及びAndroidについて、ネイティブ実装を除いてはC++での実装になります。
実際に実装したコードと若干異なったり省略したりしている都合上、何か不具合があるかもしれませんので、その際はコメントで報告を頂けるとありがたいです:pray:

#iOS実装
##IMEDelegateについて
cocos2d-xにおいて、EditBoxの領域がキーボードの表示領域に重なるようになっている場合、画面レイアウトそのものが上に移動し、EditBoxがキーボードと重ならないようになっています。
これを実現しているのが`cocos2d::IMEDelegate`というクラスで、`cocos2d::ui::EditBox`もこのクラスを継承しています。
`IMEDelegate`についてはあまり情報がなく、調べてみても読める言語で得られる情報は[~~役に立つんだか立たないんだか分からない~~クラスリファレンス](http://www.cocos2d-x.org/reference/native-cpp/V3.0beta2/dc/d19/classcocos2d_1_1_i_m_e_delegate.html)くらいで、かなり苦戦しました。

今回利用するのはこの`IMEDelegate`の中でもキーボードの出入りを検知する`keyboardWillShow`、`keyboardDidShow`、`keyboardWillHide`、`keyboardDidHide`になります。
実装を見てみると、それぞれ`cocos2d::IMEKeyboardNotificationInfo`という構造体を参照受けで引数に取っています。
更に`IMEKeyboardNotificationInfo`の中を見てみると、以下の3つで構成されています。

- `Rect begin`: アニメーション開始時のキーボードの表示領域
- `Rect end`: アニメーション終了時のキーボードの表示領域
- `float duration`: アニメーション終了までにかかる秒数

##仮実装
以上のことを利用して以下のようなコードを書いてみます。

```c++:HogeView.cpp
// HogeViewはcocos2d::IMEDelegate継承済

using namespace cocos2d;

// 記述省略
// 以下のEditBoxをメンバとして宣言し、create済とする
// cocos2d::ui::EditBox* _editBox

void HogeView::keyboardWillShow(IMEKeyboardNotificationInfo& info)
{
    _editBox->setPositionY(info.begin.getMaxY());
    _editBox->runAction(MoveTo::create(info.duration, Vec2(editBox->getPositionX, info.end.getMaxY()));
}

void HogeView::keyboardWillHide(IMEKeyboardNotificationInfo& info)
{
    _editBox->runAction(moveTo::create(info.duration, Vec2(editBox->getPositionX, info.end.getMaxY()));
}
```

意外とうまく動いてくれますが、実際に日本語で入力を開始してみると、変換候補が表示された瞬間に画面レイアウトが上に移動して、更にキーボードから浮いた位置にEditBoxが配置されるようなことになるかと思います。
変換候補が出現する瞬間にもきちんと`keyboardWillShow`は呼ばれているのですが、その際の`duration`は`0`となっています。
正確なことまでは分からないのですが、おそらく`keyboardWillShow`が呼ばれた時点でキーボードは出現済という扱いになっていて、EditBoxの位置が変換候補表示の領域と重なって画面レイアウトが上に移動→その後遅れてEditBoxが上に移動という形になってしまうのだと思います。

##EditBoxがキーボードと重なっても何もしないようにする
最初に少し触れましたが、EditBoxの領域がキーボードの表示領域と重なる場合に画面レイアウトが移動するのは`cocos2d::ui::EditBox`自体も`IMEDelegate`を継承しているからです。
実装を見る感じ、`keyboardWillShow`で領域が重なる分の高さを取得し、その分だけ画面レイアウトを動かす処理を行っているようです。
この機能、通常は便利なんじゃないかと思うんですが今回の場合は少し邪魔ですね:angry:
ということでこの機能を使わないようにしたいのですが、そのためには以下のどちらかの方法を取る必要があります。

- `cocos2d::ui::EditBox`に手を加える
- `cocos2d::ui::EditBox`を継承したクラスを作成する

基本的にcocos2d-xのソースには手を加えたくはないと思うので、今回は後者の方法を取りましょう。
以下のようなクラスを作成してみます。

```c++:CustomEditBox.h
#include "cocos2d/ui/UIEditBox/UIEditBox.h"

class CustomEditBox : public cocos2d::ui::EditBox
{
    // ctorやらcreateやらなんか適当に

    void keyboardWillShow(cocos2d::IMEKeyboardNotificationInfo& info) override {}
};
```

`keyboardWillShow`で何もしないようにするだけですね。
独自にやりたいことがあれば代わりに何か書いてみてもいいかなと思います。

##実装
仮実装の`_editBox`を上記の`CustomEditBox`に変更すればOKです。
アニメーションについてはかなり適当ですね……。
私もまだあまり調整できていないので、いい感じのアニメーションが実装できたという方は是非教えてください:+1:

**2018/3/28追記：**
いくつかイージングを試してみましたが、`EaseSineOut`が割とそれっぽかったです。
とはいえキーボードと完全に同じ動きになったというわけでもないので、完全トレースするなら細かい調整が必要そうです。

#Android
##Androidのキーボード動作
AndroidでもiOS同様、キーボードとEditBoxの領域が重なる場合には画面レイアウトが移動するようになっています（デフォルトでは）。
しかし、iOSの場合とは異なり、この動作はOSによって行われるものです。
また、`AndroidManifest.xml`内の`android:windowSoftInputMode`の値（あるいはコード内で`setSoftInputMode`で設定した値）によって画面レイアウトが移動したりしなかったりといったところが変化します。
それぞれの値に応じた動作については[公式のリファレンス](https://developer.android.com/guide/topics/manifest/activity-element.html?hl=ja)から「android:windowSoftInputMode」の項目を参照してください。

そして、iOSの項目で使っていた`IMEDelegate`はAndroidでは使えないようです:sob:
iOSと同じ実装で動かしてみると、`keyboardWillShow`や`keyboardWillHide`といったメソッドが呼ばれてすらおらず、キーボードの出入りを検知することすら許されませんでした。
クラスリファレンスにも特に記述がない（使えないならせめてその旨は書いて欲しいが……）ので詳細は不明ですが、Androidにキーボードの出入りを検知するAPIがないのが原因だと思われます。
もしかすると自分の環境の問題かもしれないので、「Androidでも`IMEDelegate`使えるよ！」って方は教えていただけるとありがたいです。

##実装方針
残念ながら`IMEDelegate`によってキーボードの情報を直接取得することが出来ないので、`IMEDelegate`で行っていることを気合で行っていきます:muscle:
具体的には以下のような感じです。

1. キーボードが出現した、あるいはEditBoxがフォーカスを得たということを検知する
2. キーボードの表示領域を取得する
3. EditBoxの位置を動かす
4. 変換候補の表示などでキーボードの大きさが変わった場合、それも検知してEditBoxを動かす

1〜3は比較的楽に見えます~~が実際にはそんなに楽ではなかった~~。
4が少々厄介ですね。
気合と根性でこれらを解決していきましょう。

##EditBoxDelegate
`IMEDelegate`が使えないのがなんぼのもんじゃい、ということで`cocos2d::ui::EditBoxDelegate`の出番です。
`EditBoxDelegate`はEditBoxへの操作を検知し、それぞれのイベントに応じたメソッドを呼び出してくれるこれまた便利なクラスとなります。
具体的には、

- 入力が開始する: `editBoxDidBegin(cocos2d::ui::EditBox*)`
- 入力文字が変化した: `editBoxTextChanged(cocos2d::ui::EditBox*, const std::string&)`
- 入力が終わる: `editBoxReturn(cocos2d::ui::EditBox*)`
- 入力が終わった: `editBoxEditingDidEndWithAction(EditBox*, EditBoxEndAction)`

といった感じで呼ばれます。
`EditBoxEndAction`については今回特に触れないので省略。
なお、今回の実装で用いるのは`editBoxDidBegin`と`editBoxEditingDidEndWithAction`だけです。

##キーボードの表示領域の取得
さて、`EditBox`への入力が開始したことは検知できそうですが、残念ながら`EditBoxDelegate`ではキーボードの大きさを取得することは不可能です。
これについて調べているとstackoverflowに[正に探している情報について質問されているスレッド](https://stackoverflow.com/questions/16788959/is-there-any-way-in-android-to-get-the-height-of-virtual-keyboard-of-device)があったのでここの回答を~~丸パクリ~~参考にして実装します。

```java:AppActivity.java
public static int getKeyboardHeight() {
    // 実際には、画面サイズと現在の実効画面サイズの差を取っているだけ

    // rootViewを取得する
    AppActivity app = (AppActivity)Cocos2dxActivity.getContext();
    View rootView = ((ViewGroup)app.findViewById(R.id.content)).getChildAt(0);

    // rootViewの表示領域を取得する
    Rect rect = new Rect();
    rootView.getWindowVisibleDisplayFrame(rect);

    // 画面全体の表示領域の高さとrootViewの表示領域の高さとの差分を取得する
    int height = rootView.getRootView().getHeight() - rect.height();

    return height;
}
```

```cpp:NativeLauncher.cpp
static constexpr char[] ActivityName = "com/your/project/AppActivity"

// NativeLauncherはネイティブ連携コードをまとめたクラス

int NativeLauncher::getKeyboardHeight() {
    JniMethodInfo info;
    if (JniHelper::getStaticMethodInfo(info, ActivityName, "getKeyboardHeight", "()I")) {
        int height = info.env->CallStaticIntMethod(info.classID, info.methodID);
        info.env->DeleteLocalRef(info.classID);

        return height;
    }

    return 0;
}
```

##逐一動かす
さて、あとは「キーボードの大きさが変わった時に動かせるようにする」ということを達成できればOKです。
が。
やはりというかなんというか、残念ながらAndroidではリアルタイムにはそういったイベントが検知出来ないようです（やる方法があったら教えてください）。
ということで困ったら力技。
0.5秒ごとにキーボードの大きさを取得して逐一`EditBox`の位置を動かすことにしてみます。

```cpp:HogeView.cpp
// HogeViewはcocos2d::ui::EditBoxDelegate継承済

using namespace cocos2d;

// ヘッダーで以下を宣言済とする
// float _elapsedTime = 0.0f;
// bool _needToAdjust = false;

// どこかでcocos2d::ui::EditBox* _editBoxをcreate済とする

void HogeView::update(float dt)
{
    if (_needToAdjust) {
        _elapsedTime += dt;
        if (dt > 0.5f) {
            this->adjustEditBoxPosition();
        }
    }
}

void HogeView::adjustEditBoxPosition()
{
    _editBox->setPositionY(NativeLauncher::getKeyboardHeight());
}

void HogeView::editBoxEditingDidBegin(ui::EditBox* editbox)
{
    // Androidだけの処理にしておく
#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
    _needToAdjust = true;
#endif
}

void HogeView::editBoxEditingDidEndWithAction(ui::EditBox* editbox, EditBoxEndAction action)
{
    // Androidだけの処理にしておく
#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
    _needToAdjust = false;
#endif
}
```

こんな感じでしょうか。
本当はもう少しいい感じの実装があるのではないだろうかと思わなくもないです。
何か良いアイデアお持ちの方、ぜひコメントお願いいたします:bow:

ちなみに私はiOSの場合は`IMEDelegate`を使った方法、Androidの場合は`EditBoxDelegate`を使った方法という感じに分けて実装しましたが、iOSの方でもキーボードの高さを取得するような関数さえ作成してしまえれば両方のOSで`EditBoxDelegate`を使った実装を使い回すことも（一応）可能かと思います。

#参考
https://stackoverflow.com/questions/16788959/is-there-any-way-in-android-to-get-the-height-of-virtual-keyboard-of-device