C++
Android
iOS
cocos2d-x

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

背景

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

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

iOS実装

IMEDelegateについて

cocos2d-xにおいて、EditBoxの領域がキーボードの表示領域に重なるようになっている場合、画面レイアウトそのものが上に移動し、EditBoxがキーボードと重ならないようになっています。
これを実現しているのがcocos2d::IMEDelegateというクラスで、cocos2d::ui::EditBoxもこのクラスを継承しています。
IMEDelegateについてはあまり情報がなく、調べてみても読める言語で得られる情報は役に立つんだか立たないんだか分からないクラスリファレンスくらいで、かなり苦戦しました。

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

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

仮実装

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

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は呼ばれているのですが、その際のduration0となっています。
正確なことまでは分からないのですが、おそらくkeyboardWillShowが呼ばれている時点でキーボードは出現済という扱いになっており、そのせいでEditBoxの位置が変換候補によって拡大されたキーボードの表示領域と重なって画面レイアウトが上に移動、その後遅れてEditBoxが上に移動という形になってしまうのだと思います。

EditBoxがキーボードと重なっても何もしないようにする

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

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

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

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です。
アニメーションについてはかなり適当ですね……。
私もまだあまり調整できていないので、いい感じのアニメーションが実装できたという方は是非教えてください:thumbsup:

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

Android

Androidのキーボード動作

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

そして、iOSの項目で使っていたIMEDelegateはAndroidでは使えないようです:sob:
iOSと同じ実装で動かしてみると、keyboardWillShowkeyboardWillHideといったメソッドが呼ばれてすらおらず、キーボードの出入りを検知することすら許されませんでした。
クラスリファレンスにも特に記述がない(使えないならせめてその旨は書いて欲しいが……)ので詳細は不明ですが、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については今回特に触れないので省略。
なお、今回の実装で用いるのはeditBoxDidBegineditBoxEditingDidEndWithActionだけです。

キーボードの表示領域の取得

さて、EditBoxへの入力が開始したことは検知できそうですが、残念ながらEditBoxDelegateではキーボードの大きさを取得することは不可能です。
これについて調べているとstackoverflowに正に探している情報について質問されているスレッドがあったのでここの回答を丸パクリ参考にして実装します。

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;
}
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の位置を動かすことにしてみます。

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