#背景
- チャットアプリなどでよくある、テキスト入力フィールドがソフトウェアキーボードの真上に移動するようなUIが作りたかった
- 調べてみたがcocos2d-xでそういったUIを作ることについてまとまった記事がなかった
- 今後の自分用のメモも兼ねて記事にまとめてみよう
こんな感じです。
なお、以下は全てiOS及びAndroidについて、ネイティブ実装を除いてはC++での実装になります。
実際に実装したコードと若干異なったり省略したりしている都合上、何か不具合があるかもしれませんので、その際はコメントで報告を頂けるとありがたいです
#iOS実装
##IMEDelegateについて
cocos2d-xにおいて、EditBoxの領域がキーボードの表示領域に重なるようになっている場合、画面レイアウトそのものが上に移動し、EditBoxがキーボードと重ならないようになっています。
これを実現しているのがcocos2d::IMEDelegate
というクラスで、cocos2d::ui::EditBox
もこのクラスを継承しています。
IMEDelegate
についてはあまり情報がなく、調べてみても読める言語で得られる情報は役に立つんだか立たないんだか分からないクラスリファレンスくらいで、かなり苦戦しました。
今回利用するのはこのIMEDelegate
の中でもキーボードの出入りを検知するkeyboardWillShow
、keyboardDidShow
、keyboardWillHide
、keyboardDidHide
になります。
実装を見てみると、それぞれcocos2d::IMEKeyboardNotificationInfo
という構造体を参照受けで引数に取っています。
更にIMEKeyboardNotificationInfo
の中を見てみると、以下の3つで構成されています。
-
Rect begin
: アニメーション開始時のキーボードの表示領域 -
Rect end
: アニメーション終了時のキーボードの表示領域 -
float duration
: アニメーション終了までにかかる秒数
##仮実装
以上のことを利用して以下のようなコードを書いてみます。
// 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
で領域が重なる分の高さを取得し、その分だけ画面レイアウトを動かす処理を行っているようです。
この機能、通常は便利なんじゃないかと思うんですが今回の場合は少し邪魔ですね
ということでこの機能を使わないようにしたいのですが、そのためには以下のどちらかの方法を取る必要があります。
-
cocos2d::ui::EditBox
に手を加える -
cocos2d::ui::EditBox
を継承したクラスを作成する
基本的にcocos2d-xのソースには手を加えたくはないと思うので、今回は後者の方法を取りましょう。
以下のようなクラスを作成してみます。
#include "cocos2d/ui/UIEditBox/UIEditBox.h"
class CustomEditBox : public cocos2d::ui::EditBox
{
// ctorやらcreateやらなんか適当に
void keyboardWillShow(cocos2d::IMEKeyboardNotificationInfo& info) override {}
};
keyboardWillShow
で何もしないようにするだけですね。
独自にやりたいことがあれば代わりに何か書いてみてもいいかなと思います。
##実装
仮実装の_editBox
を上記のCustomEditBox
に変更すればOKです。
アニメーションについてはかなり適当ですね……。
私もまだあまり調整できていないので、いい感じのアニメーションが実装できたという方は是非教えてください
2018/3/28追記:
いくつかイージングを試してみましたが、EaseSineOut
が割とそれっぽかったです。
とはいえキーボードと完全に同じ動きになったというわけでもないので、完全トレースするなら細かい調整が必要そうです。
#Android
##Androidのキーボード動作
AndroidでもiOS同様、キーボードとEditBoxの領域が重なる場合には画面レイアウトが移動するようになっています(デフォルトでは)。
しかし、iOSの場合とは異なり、この動作はOSによって行われるものです。
また、AndroidManifest.xml
内のandroid:windowSoftInputMode
の値(あるいはコード内でsetSoftInputMode
で設定した値)によって画面レイアウトが移動したりしなかったりといったところが変化します。
それぞれの値に応じた動作については公式のリファレンスから「android:windowSoftInputMode」の項目を参照してください。
そして、iOSの項目で使っていたIMEDelegate
はAndroidでは使えないようです
iOSと同じ実装で動かしてみると、keyboardWillShow
やkeyboardWillHide
といったメソッドが呼ばれてすらおらず、キーボードの出入りを検知することすら許されませんでした。
クラスリファレンスにも特に記述がない(使えないならせめてその旨は書いて欲しいが……)ので詳細は不明ですが、Androidにキーボードの出入りを検知するAPIがないのが原因だと思われます。
もしかすると自分の環境の問題かもしれないので、「AndroidでもIMEDelegate
使えるよ!」って方は教えていただけるとありがたいです。
##実装方針
残念ながらIMEDelegate
によってキーボードの情報を直接取得することが出来ないので、IMEDelegate
で行っていることを気合で行っていきます
具体的には以下のような感じです。
- キーボードが出現した、あるいはEditBoxがフォーカスを得たということを検知する
- キーボードの表示領域を取得する
- EditBoxの位置を動かす
- 変換候補の表示などでキーボードの大きさが変わった場合、それも検知して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に正に探している情報について質問されているスレッドがあったのでここの回答を丸パクリ参考にして実装します。
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;
}
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は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
}
こんな感じでしょうか。
本当はもう少しいい感じの実装があるのではないだろうかと思わなくもないです。
何か良いアイデアお持ちの方、ぜひコメントお願いいたします
ちなみに私はiOSの場合はIMEDelegate
を使った方法、Androidの場合はEditBoxDelegate
を使った方法という感じに分けて実装しましたが、iOSの方でもキーボードの高さを取得するような関数さえ作成してしまえれば両方のOSでEditBoxDelegate
を使った実装を使い回すことも(一応)可能かと思います。