QML: Resize controls when Android virtual keyboard come up


QML have currently a bug (current version 5.8 still have) in resize window when android keyboard come up. It happen when virtual keyboard is requested the QML window show a bad flicker by repaint the top control on the center of the screen and back on top again. This look really unprofessional.


Full project example can be found here

In await of Qt team to fix the bug will show here a small workaround for allow the QML window to be correctly resized when android virtual keyboard come up over the window. The solution consist in detect when virtual keyboard is showed, calculate the height and resize our window control based to the "new" screen space available (decreased). Qt framework already export a method to get virtual keyboard size using QInputMethod::keyboardRectangle() but the documentation cleary write this doesn't work with floating keyboard like android virtual keyboard, this mean we have to find another way. Solution proposed here will consist in use mixed C++ and native java code to install a keyboard listener informing about new available screen size when keyboard is present. At first you have to add the followinf tag to the AndroidManifest.xml app file:

<activity .... android:windowSoftInputMode="adjustPan">

This will instruct android keyboard to don't try to resize the window but just show keyboard below the current window. Once done this is the java native code to add to your project:

public class VirtualKeyboardListener
{
    private static Activity m_ActivityInstance;

    public static void Init(Activity ActivityInstance)
    {
        m_ActivityInstance = ActivityInstance;
    }

    public static native void VirtualKeyboardStateChanged(int VirtualKeyboardHeight);

    public static void InstallKeyboardListener()
    {
        final View AppRootView = ((ViewGroup)m_ActivityInstance.findViewById(android.R.id.content)).getChildAt(0);

        AppRootView.getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener()
            {
                @Override
                public void onGlobalLayout()
                {
                    int ScreenHeight, VirtualKeyboardHeight;
                    Rect WindowFrameRect = new Rect();
                    Rect ContentFrameRect = new Rect();

                    m_ActivityInstance.getWindow().getDecorView().getWindowVisibleDisplayFrame(WindowFrameRect);
                    AppRootView.getWindowVisibleDisplayFrame(ContentFrameRect);
                    ScreenHeight = AppRootView.getRootView().getHeight();

                    VirtualKeyboardHeight = (ScreenHeight - (ContentFrameRect.bottom - ContentFrameRect.top) - WindowFrameRect.top);

                    if(VirtualKeyboardHeight < 100) VirtualKeyboardHeight = 0;

                    VirtualKeyboardStateChanged(VirtualKeyboardHeight);
                }
            }
        );
    }
}

At app startup is necessary to call the function for install the virtual keyboard listener and register the callback JNI function from C++ side that will be called by the listener when virtual keyboard show up and passing as param the keyboard height. This same C++ function will pass the parameter to QML code for allow correct resize:

QObject *pQmlRootObject = NULL;

void AndroidVirtualKeyboardStateChanged(JNIEnv *env, jobject thiz, jint VirtualKeyboardHeight)
{
    Q_UNUSED(env)
    Q_UNUSED(thiz)

    if(pQmlRootObject != NULL)
    {
        QMetaObject::invokeMethod(pQmlRootObject,
                                  "androidVirtualKeyboardStateChanged",
                                  Qt::AutoConnection,
                                  Q_ARG(QVariant, VirtualKeyboardHeight)
                                  );
    }
}

void InitializeForAndroid()
{
    JNINativeMethod methods[] = {
        {
            "VirtualKeyboardStateChanged",
            "(I)V",
            reinterpret_cast(AndroidVirtualKeyboardStateChanged)
        }
    };

    QAndroidJniObject javaClass("com/falsinsoft/example/keyboardsize/VirtualKeyboardListener");
    QAndroidJniEnvironment env;

    jclass objectClass = env->GetObjectClass(javaClass.object());

    env->RegisterNatives(objectClass,
        methods,
        sizeof(methods) / sizeof(methods[0]));
    env->DeleteLocalRef(objectClass);

    QAndroidJniObject::callStaticMethod("com/falsinsoft/example/keyboardsize/VirtualKeyboardListener", "InstallKeyboardListener");
}

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QLatin1String("qrc:/main.qml")));

    pQmlRootObject = engine.rootObjects().at(0);
    InitializeForAndroid();

    return app.exec();
}

Please note you can not resize the QML main window since android OS need the main window to be full screen. You need to resize the child controls inside the window to "compensate" the part of the screen currently covered by the virtual keyboard.

function androidVirtualKeyboardStateChanged(virtualKeyboardHeight)
{
    if(virtualKeyboardHeight > 0)
        appBody.height = (appBody.parent.height - (virtualKeyboardHeight / Screen.devicePixelRatio));
    else
        appBody.height = appBody.parent.height;
}

Virtual keyboard size returned is in pixel but you need to calculate the space to "free" also considering the currect screen DPI as showed in the code above.

Comments

  1. Thanks i'msearching the same workaround for javascript only. Possible ?

    ReplyDelete
  2. Unfortuntely, from the tests I made, using QML javascript only doesn't work very well cause is not possible to know the exact moment the android virtual keyboard come up. Using android API seem the only way to be "advised" on time when keyboard show and than resize the control based to the new reduct space available.

    ReplyDelete

Post a Comment

Popular posts from this blog

Access GPIO from Linux user space

Launch an app from Android shell terminal