gluonhq / substrate

Create native Java(FX) apps for desktop, mobile and embedded
GNU General Public License v2.0
392 stars 52 forks source link

Resize the stage on keyboard show on Android #827

Closed DeanWookey closed 3 years ago

DeanWookey commented 3 years ago

In our case it's preferable for the stage to resize smaller when the keyboard is shown rather than overlapping the elements, including the ones you're inputting into to.

Supposedly there is a way to do that by editing the manifest by adding

android:windowSoftInputMode="adjustResize"

ie.

        <activity android:name='com.gluonhq.helloandroid.MainActivity' android:configChanges="orientation|keyboardHidden" android:windowSoftInputMode="adjustResize">

This apparently doesn't work with a translucent status bar? It also works as intended, so there doesn't seem to be much chance of a fix coming from the Android side. Anyway, there are lots of stackoverflow issues about this with a whole variety of solutions:

As a proof of concept, I edited my MainActivity and ran gradlew app:assembleDebug manually with one of the solutions, which seems to somewhat work (it's a bit flickery). I think one option would be to allow you to specify your own MainActivity in src/android in the same way you can specify your own AndroidManfiest (which I couldn't find any documentation for by the way) so that people wouldn't have to do what I did. The downside is that any fixes/changes you made to those auto generated files would have to be integrated manually.

Another option would be to more formally support this functionality.

If we could detect the keyboard was showing somehow we could also do this on the javafx side by adding appropriate padding to the root element.

/*
 * Copyright (c) 2019, 2020, Gluon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.gluonhq.helloandroid;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;

import android.view.inputmethod.InputConnection;

import android.view.KeyEvent;
import android.view.KeyCharacterMap;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;

import java.util.TimeZone;
import javafx.scene.input.KeyCode;

public class MainActivity extends AndroidBug5497Workaround implements SurfaceHolder.Callback,
        SurfaceHolder.Callback2 {

    private static MainActivity   instance;
    private static FrameLayout  mViewGroup;
    private static SurfaceView  mView;
    private long nativeWindowPtr;
    private float density;

    private static final String TAG     = "GraalActivity";

    boolean graalStarted = false;

    private static InputMethodManager imm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.v(TAG, "onCreate start, using Android Logging v1");
        System.err.println("onCreate called, writing this to System.err");
        super.onCreate(savedInstanceState);

        getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        getWindow().setSoftInputMode(
                WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED
                );
        getWindow().setFormat(PixelFormat.RGBA_8888);

        mView = new InternalSurfaceView(this);
        mView.getHolder().addCallback(this);
        mViewGroup = new FrameLayout(this);
        mViewGroup.addView(mView);
        setContentView(mViewGroup);
        instance = this;

        imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        Log.v(TAG, "onCreate done");
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.v(TAG, "surfaceCreated for "+this);
        Log.v(TAG, "loading substrate library");
        System.loadLibrary("substrate");
        Log.v(TAG, "loaded substrate library");
        nativeSetSurface(holder.getSurface());
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        density = metrics.density;
        Log.v(TAG, "metrics = "+metrics+", density = "+density);
        nativeWindowPtr = surfaceReady(holder.getSurface(), density);
        Rect currentBounds = new Rect();
        mView.getRootView().getWindowVisibleDisplayFrame(currentBounds);

        Log.v(TAG, "Surface created, native code informed about it, nativeWindow at "+nativeWindowPtr);
        if (graalStarted) {
            Log.v(TAG, "GraalApp is already started.");
        } else {
            Log.v(TAG, "We will now launch Graal in a separate thread");
            final String[] launchArgs = {
                    "-Duser.home=" + getApplicationInfo().dataDir,
                    "-Djava.io.tmpdir=" + getApplicationInfo().dataDir,
                    "-Duser.timezone=" + TimeZone.getDefault().getID(),
                    "-DLaunch.URL=" + System.getProperty("Launch.URL", ""),
                    "-DLaunch.LocalNotification=" + System.getProperty("Launch.LocalNotification", "")
            };
            Thread t = new Thread() {
                @Override public void run() {
                    startGraalApp(launchArgs);
                }
            };
            t.start();
            graalStarted = true;
            Log.v(TAG, "graalStarted true");
        }
        Log.v(TAG, "surfaceCreated done");
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
                    int height) {
        Log.v(TAG, "surfaceChanged start");
        Log.v(TAG, "[MainActivity] surfaceChanged, format = "+format+", width = "+width+", height = "+height);
        nativeSetSurface(holder.getSurface());
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        density = metrics.density;
        Log.v(TAG, "surfaceChanged done, metrics = "+metrics+", density = "+density);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        System.err.println("[MainGraalActivity] surfaceDestroyed, ignore for now");
            // _surfaceChanged(null);
    }

    @Override
    public void surfaceRedrawNeeded(SurfaceHolder holder) {
        Log.v(TAG, "SurfaceRedraw needed start");
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        Log.v(TAG, "ask native graallayer to redraw surface");
        nativeSurfaceRedrawNeeded();
        try {
            Thread.sleep(500);
            Log.v(TAG, "surfaceredraw needed part 1 done");
            nativeSurfaceRedrawNeeded();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.v(TAG, "surfaceredraw needed (and wait) done");
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
        Log.v(TAG, "onActivityResult with requestCode " + requestCode + " and resultCode = " + resultCode + " and intent = " + intent);
        nativeDispatchActivityResult(requestCode, resultCode, intent);
    }

    private static void showIME() {
        Log.v(TAG, "Called notify_showIME for imm = "+imm+", mv = "+mView);
        instance.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mView.requestFocus();
                boolean answer = imm.showSoftInput(mView, 0);
                Log.v(TAG, "Done calling notify_showIME, answer = " + answer);
            }
        });
    }

    private static void hideIME() {
        Log.v(TAG, "Called notify_hideIME");
        instance.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mView.requestFocus();
                boolean answer = imm.hideSoftInputFromWindow(mView.getWindowToken(), 0);
                Log.v(TAG, "Done Calling notify_hideIME, answer = " + answer);
            }
        });
    }

    private native void startGraalApp(String[] launchArgs);
    private native long surfaceReady(Surface surface, float density);
    private native void nativeSetSurface(Surface surface);
    private native void nativeSurfaceRedrawNeeded();
    private native void nativeGotTouchEvent(int pcount, int[] actions, int[] ids, int[] touchXs, int[] touchYs);
    private native void nativeGotKeyEvent(int action, int keycode);
    private native void nativedispatchKeyEvent(int type, int key, char[] chars, int charCount, int modifiers);
    private native void nativeDispatchLifecycleEvent(String event);
    private native void nativeDispatchActivityResult(int requestCode, int resultCode, Intent intent);

    class InternalSurfaceView extends SurfaceView {
        private static final int ACTION_POINTER_STILL = -1;

        public InternalSurfaceView(Context context) {
            super(context);
            setFocusableInTouchMode(true);
            setFocusable(true);
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            int action = event.getAction();
            int actionCode = action & MotionEvent.ACTION_MASK;
            final int pcount = event.getPointerCount();
            final int[] actions = new int[pcount];
            final int[] ids = new int[pcount];
            final int[] touchXs = new int[pcount];
            final int[] touchYs = new int[pcount];
            Log.v(TAG, "Activity, get touch event, pcount = "+pcount);
            if (pcount > 1) {
                //multitouch
                if (actionCode == MotionEvent.ACTION_POINTER_DOWN
                        || actionCode == MotionEvent.ACTION_POINTER_UP) {

                    int pointerIndex = event.getActionIndex();
                    for (int i = 0; i < pcount; i++) {
                        actions[i] = pointerIndex == i ? actionCode : ACTION_POINTER_STILL;
                        ids[i] = event.getPointerId(i);
                        touchXs[i] = (int) (event.getX(i)/density);
                        touchYs[i] = (int) (event.getY(i)/density);
                    }
                } else if (actionCode == MotionEvent.ACTION_MOVE) {
                    for (int i = 0; i < pcount; i++) {
                        touchXs[i] = (int) (event.getX(i)/density);
                        touchYs[i] = (int) (event.getY(i)/density);
                        actions[i] = MotionEvent.ACTION_MOVE;
                        ids[i] = event.getPointerId(i);
                    }
                }
            } else {
                //single touch
                actions[0] = actionCode;
                ids[0] = event.getPointerId(0);
                touchXs[0] = (int) (event.getX()/density);
                touchYs[0] = (int) (event.getY()/density);
            }
            if (!isFocused()) {
                Log.v(TAG, "View wasn't focused, requesting focus");
                requestFocus();
            }
            nativeGotTouchEvent(pcount, actions, ids, touchXs, touchYs);
            return true;
        }

        @Override
        public boolean dispatchKeyEvent(final KeyEvent event) {
            Log.v(TAG, "Activity, process get key event, action = "+event.getAction());
            processAndroidKeyEvent (event);
            // nativeGotKeyEvent(event.getAction(), event.getKeyCode());
            return true;
        }
    }

    @Override
    protected void onDestroy() {
        Log.v(TAG, "onDestroy");
        super.onDestroy();
        notifyLifecycleEvent("destroy");
        android.os.Process.killProcess(android.os.Process.myPid());
    }

    @Override
    protected void onPause() {
        Log.v(TAG, "onPause");
        super.onPause();
        notifyLifecycleEvent("pause");
    }

    @Override
    protected void onResume() {
        Log.v(TAG, "onResume");
        super.onResume();
        notifyLifecycleEvent("resume");
        Log.v(TAG, "onResume done");
    }

    @Override
    protected void onStart() {
        Log.v(TAG, "onStart");
        super.onStart();
        notifyLifecycleEvent("start");
        Log.v(TAG, "onStart done");
    }

    @Override
    protected void onRestart() {
        Log.v(TAG, "onRestart");
        super.onRestart();
        notifyLifecycleEvent("restart");
    }

    @Override
    protected void onStop() {
        Log.v(TAG, "onStop");
        super.onStop();
        notifyLifecycleEvent("stop");
    }

    private void notifyLifecycleEvent(String event) {
        if (graalStarted) {
            nativeDispatchLifecycleEvent(event);
        }
    }

    public final static int PRESS   = 111;
    public final static int RELEASE = 112;
    public final static int TYPED   = 113;

    private int deadKey = 0;

    void processAndroidKeyEvent (KeyEvent event) {
        System.out.println("KeyEvent: " + event+" with action = "+event.getAction());
        int jfxModifiers = mapAndroidModifierToJfx(event.getMetaState());
        switch (event.getAction()) {
            case KeyEvent.ACTION_DOWN:
                KeyCode jfxKeyCode = mapAndroidKeyCodeToJfx(event.getKeyCode());
System.out.println ("[JVDBG] eventkeycode = "+event.getKeyCode()+" and jfxkc = "+jfxKeyCode+" with code "+ jfxKeyCode.impl_getCode());
                nativedispatchKeyEvent(PRESS, jfxKeyCode.impl_getCode(), jfxKeyCode.impl_getChar().toCharArray(), jfxKeyCode.impl_getChar().toCharArray().length, jfxModifiers);
                break;

            case KeyEvent.ACTION_UP:
                jfxKeyCode = mapAndroidKeyCodeToJfx(event.getKeyCode());
                nativedispatchKeyEvent(RELEASE, jfxKeyCode.impl_getCode(), jfxKeyCode.impl_getChar().toCharArray(), jfxKeyCode.impl_getChar().toCharArray().length, jfxModifiers);
                int unicodeChar = event.getUnicodeChar();
                if ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) != 0) {
                    deadKey = unicodeChar & KeyCharacterMap.COMBINING_ACCENT_MASK;
                    return;
                }

                if (deadKey != 0 && unicodeChar != 0) {
                    unicodeChar = KeyCharacterMap.getDeadChar(deadKey, unicodeChar);
                    deadKey = 0;
                }

                if (unicodeChar != 0) {
                    nativedispatchKeyEvent(TYPED, KeyCode.UNDEFINED.impl_getCode(), Character.toChars(unicodeChar), 1, jfxModifiers);
                }

                break;

            case KeyEvent.ACTION_MULTIPLE:
                if (event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
                    nativedispatchKeyEvent(TYPED, KeyCode.UNDEFINED.impl_getCode(), event.getCharacters().toCharArray(), event.getCharacters().toCharArray().length,    jfxModifiers);
                } else {
                    jfxKeyCode = mapAndroidKeyCodeToJfx(event.getKeyCode());
                    for (int i = 0; i < event.getRepeatCount(); i++) {
                        nativedispatchKeyEvent(PRESS, jfxKeyCode.impl_getCode(), null, 0, jfxModifiers);
                        nativedispatchKeyEvent(RELEASE, jfxKeyCode.impl_getCode(), null, 0, jfxModifiers);
                        nativedispatchKeyEvent(TYPED, jfxKeyCode.impl_getCode(), null, 0, jfxModifiers);
                    }
                }

                break;
            default:
                System.err.println("DalvikInput.onKeyEvent Unknown Action " + event.getAction());
                break;
        }
    }

    private final static int MODIFIER_SHIFT = 1;
    private final static int MODIFIER_CONTROL = 2;
    private final static int MODIFIER_ALT = 4;
    private final static int MODIFIER_WINDOWS = 8;

    private static int mapAndroidModifierToJfx(int androidMetaStates) {
        int jfxModifiers = 0;

        if ((androidMetaStates & KeyEvent.META_SHIFT_MASK) != 0) {
            jfxModifiers += MODIFIER_SHIFT;
        }

        if ((androidMetaStates & KeyEvent.META_CTRL_MASK) != 0) {
            jfxModifiers += MODIFIER_CONTROL;
        }

        if ((androidMetaStates & KeyEvent.META_ALT_MASK) != 0) {
            jfxModifiers += MODIFIER_ALT;
        }

        if ((androidMetaStates & KeyEvent.META_META_ON) != 0) {
            jfxModifiers += MODIFIER_WINDOWS;
        }
        return jfxModifiers;
    }

    private static KeyCode mapAndroidKeyCodeToJfx(int keycode) {
        switch (keycode) {
            case KeyEvent.KEYCODE_UNKNOWN: return KeyCode.UNDEFINED;
            case KeyEvent.KEYCODE_HOME: return KeyCode.HOME;
            case KeyEvent.KEYCODE_BACK: return KeyCode.ESCAPE; // special back key mapped to ESC
            case KeyEvent.KEYCODE_0: return KeyCode.DIGIT0;
            case KeyEvent.KEYCODE_1: return KeyCode.DIGIT1;
            case KeyEvent.KEYCODE_2: return KeyCode.DIGIT2;
            case KeyEvent.KEYCODE_3: return KeyCode.DIGIT3;
            case KeyEvent.KEYCODE_4: return KeyCode.DIGIT4;
            case KeyEvent.KEYCODE_5: return KeyCode.DIGIT5;
            case KeyEvent.KEYCODE_6: return KeyCode.DIGIT6;
            case KeyEvent.KEYCODE_7: return KeyCode.DIGIT7;
            case KeyEvent.KEYCODE_8: return KeyCode.DIGIT8;
            case KeyEvent.KEYCODE_9: return KeyCode.DIGIT9;
            case KeyEvent.KEYCODE_STAR: return KeyCode.STAR;
            case KeyEvent.KEYCODE_POUND: return KeyCode.POUND;
            case KeyEvent.KEYCODE_DPAD_UP: return KeyCode.UP;
            case KeyEvent.KEYCODE_DPAD_DOWN: return KeyCode.DOWN;
            case KeyEvent.KEYCODE_DPAD_LEFT: return KeyCode.LEFT;
            case KeyEvent.KEYCODE_DPAD_RIGHT: return KeyCode.RIGHT;
            case KeyEvent.KEYCODE_VOLUME_UP: return KeyCode.VOLUME_UP;
            case KeyEvent.KEYCODE_VOLUME_DOWN: return KeyCode.VOLUME_DOWN;
            case KeyEvent.KEYCODE_POWER: return KeyCode.POWER;
            case KeyEvent.KEYCODE_CLEAR: return KeyCode.CLEAR;
            case KeyEvent.KEYCODE_A: return KeyCode.A;
            case KeyEvent.KEYCODE_B: return KeyCode.B;
            case KeyEvent.KEYCODE_C: return KeyCode.C;
            case KeyEvent.KEYCODE_D: return KeyCode.D;
            case KeyEvent.KEYCODE_E: return KeyCode.E;
            case KeyEvent.KEYCODE_F: return KeyCode.F;
            case KeyEvent.KEYCODE_G: return KeyCode.G;
            case KeyEvent.KEYCODE_H: return KeyCode.H;
            case KeyEvent.KEYCODE_I: return KeyCode.I;
            case KeyEvent.KEYCODE_J: return KeyCode.J;
            case KeyEvent.KEYCODE_K: return KeyCode.K;
            case KeyEvent.KEYCODE_L: return KeyCode.L;
            case KeyEvent.KEYCODE_M: return KeyCode.M;
            case KeyEvent.KEYCODE_N: return KeyCode.N;
            case KeyEvent.KEYCODE_O: return KeyCode.O;
            case KeyEvent.KEYCODE_P: return KeyCode.P;
            case KeyEvent.KEYCODE_Q: return KeyCode.Q;
            case KeyEvent.KEYCODE_R: return KeyCode.R;
            case KeyEvent.KEYCODE_S: return KeyCode.S;
            case KeyEvent.KEYCODE_T: return KeyCode.T;
            case KeyEvent.KEYCODE_U: return KeyCode.U;
            case KeyEvent.KEYCODE_V: return KeyCode.V;
            case KeyEvent.KEYCODE_W: return KeyCode.W;
            case KeyEvent.KEYCODE_X: return KeyCode.X;
            case KeyEvent.KEYCODE_Y: return KeyCode.Y;
            case KeyEvent.KEYCODE_Z: return KeyCode.Z;
            case KeyEvent.KEYCODE_COMMA: return KeyCode.COMMA;
            case KeyEvent.KEYCODE_PERIOD: return KeyCode.PERIOD;
            case KeyEvent.KEYCODE_ALT_LEFT: return KeyCode.ALT;
            case KeyEvent.KEYCODE_ALT_RIGHT: return KeyCode.ALT;
            case KeyEvent.KEYCODE_SHIFT_LEFT: return KeyCode.SHIFT;
            case KeyEvent.KEYCODE_SHIFT_RIGHT: return KeyCode.SHIFT;
            case KeyEvent.KEYCODE_TAB: return KeyCode.TAB;
            case KeyEvent.KEYCODE_SPACE: return KeyCode.SPACE;
            case KeyEvent.KEYCODE_ENTER: return KeyCode.ENTER;
            case KeyEvent.KEYCODE_DEL: return KeyCode.BACK_SPACE;
            case KeyEvent.KEYCODE_GRAVE: return KeyCode.DEAD_GRAVE;
            case KeyEvent.KEYCODE_MINUS: return KeyCode.MINUS;
            case KeyEvent.KEYCODE_EQUALS: return KeyCode.EQUALS;
            case KeyEvent.KEYCODE_LEFT_BRACKET: return KeyCode.BRACELEFT;
            case KeyEvent.KEYCODE_RIGHT_BRACKET: return KeyCode.BRACERIGHT;
            case KeyEvent.KEYCODE_BACKSLASH: return KeyCode.BACK_SLASH;
            case KeyEvent.KEYCODE_SEMICOLON: return KeyCode.SEMICOLON;
            case KeyEvent.KEYCODE_SLASH: return KeyCode.SLASH;
            case KeyEvent.KEYCODE_AT: return KeyCode.AT;
            case KeyEvent.KEYCODE_PLUS: return KeyCode.PLUS;
            case KeyEvent.KEYCODE_MENU: return KeyCode.CONTEXT_MENU;
            case KeyEvent.KEYCODE_SEARCH: return KeyCode.FIND;
            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: return KeyCode.PLAY;
            case KeyEvent.KEYCODE_MEDIA_STOP: return KeyCode.STOP;
            case KeyEvent.KEYCODE_MEDIA_NEXT: return KeyCode.TRACK_NEXT;
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS: return KeyCode.TRACK_PREV;
            case KeyEvent.KEYCODE_MEDIA_REWIND: return KeyCode.REWIND;
            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: return KeyCode.FAST_FWD;
            case KeyEvent.KEYCODE_MUTE: return KeyCode.MUTE;
            case KeyEvent.KEYCODE_PAGE_UP: return KeyCode.PAGE_UP;
            case KeyEvent.KEYCODE_PAGE_DOWN: return KeyCode.PAGE_DOWN;
            case KeyEvent.KEYCODE_BUTTON_A: return KeyCode.GAME_A;
            case KeyEvent.KEYCODE_BUTTON_B: return KeyCode.GAME_B;
            case KeyEvent.KEYCODE_BUTTON_C: return KeyCode.GAME_C;
            case KeyEvent.KEYCODE_BUTTON_X: return KeyCode.GAME_D;
            case KeyEvent.KEYCODE_BUTTON_MODE: return KeyCode.MODECHANGE;
            case KeyEvent.KEYCODE_ESCAPE: return KeyCode.ESCAPE;
            case KeyEvent.KEYCODE_CTRL_LEFT: return KeyCode.CONTROL;
            case KeyEvent.KEYCODE_CTRL_RIGHT: return KeyCode.CONTROL;
            case KeyEvent.KEYCODE_CAPS_LOCK: return KeyCode.CAPS;
            case KeyEvent.KEYCODE_SCROLL_LOCK: return KeyCode.SCROLL_LOCK;
            case KeyEvent.KEYCODE_META_LEFT: return KeyCode.META;
            case KeyEvent.KEYCODE_META_RIGHT: return KeyCode.META;
            case KeyEvent.KEYCODE_SYSRQ: return KeyCode.PRINTSCREEN;
            case KeyEvent.KEYCODE_BREAK: return KeyCode.PAUSE;
            case KeyEvent.KEYCODE_MOVE_HOME: return KeyCode.BEGIN;
            case KeyEvent.KEYCODE_MOVE_END: return KeyCode.END;
            case KeyEvent.KEYCODE_INSERT: return KeyCode.INSERT;
            case KeyEvent.KEYCODE_MEDIA_PLAY: return KeyCode.PLAY;
            case KeyEvent.KEYCODE_MEDIA_EJECT: return KeyCode.EJECT_TOGGLE;
            case KeyEvent.KEYCODE_MEDIA_RECORD: return KeyCode.RECORD;
            case KeyEvent.KEYCODE_F1: return KeyCode.F1;
            case KeyEvent.KEYCODE_F2: return KeyCode.F2;
            case KeyEvent.KEYCODE_F3: return KeyCode.F3;
            case KeyEvent.KEYCODE_F4: return KeyCode.F4;
            case KeyEvent.KEYCODE_F5: return KeyCode.F5;
            case KeyEvent.KEYCODE_F6: return KeyCode.F6;
            case KeyEvent.KEYCODE_F7: return KeyCode.F7;
            case KeyEvent.KEYCODE_F8: return KeyCode.F8;
            case KeyEvent.KEYCODE_F9: return KeyCode.F9;
            case KeyEvent.KEYCODE_F10: return KeyCode.F10;
            case KeyEvent.KEYCODE_F11: return KeyCode.F11;
            case KeyEvent.KEYCODE_F12: return KeyCode.F12;
            case KeyEvent.KEYCODE_NUM_LOCK: return KeyCode.NUM_LOCK;
            case KeyEvent.KEYCODE_NUMPAD_0: return KeyCode.NUMPAD0;
            case KeyEvent.KEYCODE_NUMPAD_1: return KeyCode.NUMPAD1;
            case KeyEvent.KEYCODE_NUMPAD_2: return KeyCode.NUMPAD2;
            case KeyEvent.KEYCODE_NUMPAD_3: return KeyCode.NUMPAD3;
            case KeyEvent.KEYCODE_NUMPAD_4: return KeyCode.NUMPAD4;
            case KeyEvent.KEYCODE_NUMPAD_5: return KeyCode.NUMPAD5;
            case KeyEvent.KEYCODE_NUMPAD_6: return KeyCode.NUMPAD6;
            case KeyEvent.KEYCODE_NUMPAD_7: return KeyCode.NUMPAD7;
            case KeyEvent.KEYCODE_NUMPAD_8: return KeyCode.NUMPAD8;
            case KeyEvent.KEYCODE_NUMPAD_9: return KeyCode.NUMPAD9;
            case KeyEvent.KEYCODE_NUMPAD_DIVIDE: return KeyCode.DIVIDE;
            case KeyEvent.KEYCODE_NUMPAD_MULTIPLY: return KeyCode.MULTIPLY;
            case KeyEvent.KEYCODE_NUMPAD_SUBTRACT: return KeyCode.SUBTRACT;
            case KeyEvent.KEYCODE_NUMPAD_ADD: return KeyCode.ADD;
            case KeyEvent.KEYCODE_NUMPAD_DOT: return KeyCode.PERIOD;
            case KeyEvent.KEYCODE_NUMPAD_COMMA: return KeyCode.COMMA;
            case KeyEvent.KEYCODE_NUMPAD_ENTER: return KeyCode.ENTER;
            case KeyEvent.KEYCODE_NUMPAD_EQUALS: return KeyCode.EQUALS;
            case KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN: return KeyCode.LEFT_PARENTHESIS;
            case KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN: return KeyCode.RIGHT_PARENTHESIS;
            case KeyEvent.KEYCODE_VOLUME_MUTE: return KeyCode.MUTE;
            case KeyEvent.KEYCODE_INFO: return KeyCode.INFO;
            case KeyEvent.KEYCODE_CHANNEL_UP: return KeyCode.CHANNEL_UP;
            case KeyEvent.KEYCODE_CHANNEL_DOWN: return KeyCode.CHANNEL_DOWN;
            case KeyEvent.KEYCODE_PROG_RED: return KeyCode.COLORED_KEY_0;
            case KeyEvent.KEYCODE_PROG_GREEN: return KeyCode.COLORED_KEY_1;
            case KeyEvent.KEYCODE_PROG_YELLOW: return KeyCode.COLORED_KEY_2;
            case KeyEvent.KEYCODE_PROG_BLUE: return KeyCode.COLORED_KEY_3;
            case KeyEvent.KEYCODE_KATAKANA_HIRAGANA: return KeyCode.JAPANESE_HIRAGANA;
            case KeyEvent.KEYCODE_KANA: return KeyCode.KANA;
            default:
                return KeyCode.UNDEFINED;
        }
    }

}
 class AndroidBug5497Workaround extends Activity {

    // For more information, see https://issuetracker.google.com/issues/36911528
    // To use this class, simply invoke assistActivity() on an Activity that already has its content view set.

    private View rootView;
    private ViewGroup contentContainer;
    private ViewTreeObserver viewTreeObserver;
    private ViewTreeObserver.OnGlobalLayoutListener listener = new ViewTreeObserver.OnGlobalLayoutListener(){
        public void onGlobalLayout(){
            possiblyResizeChildOfContent();
        }
    };
    private Rect contentAreaOfWindowBounds = new Rect();
    private FrameLayout.LayoutParams rootViewLayout;
    private int usableHeightPrevious = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (viewTreeObserver.isAlive()) {
            if (contentContainer == null) {
                contentContainer = (ViewGroup) findViewById(android.R.id.content);
                rootView = contentContainer.getChildAt(0);
                rootViewLayout = (FrameLayout.LayoutParams) rootView.getLayoutParams();
            }
            viewTreeObserver.removeOnGlobalLayoutListener(listener);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (viewTreeObserver == null || !viewTreeObserver.isAlive()) {
            if (contentContainer == null) {
                contentContainer = (ViewGroup) findViewById(android.R.id.content);
                rootView = contentContainer.getChildAt(0);
                rootViewLayout = (FrameLayout.LayoutParams) rootView.getLayoutParams();
            }
            viewTreeObserver = rootView.getViewTreeObserver();
        }

        viewTreeObserver.addOnGlobalLayoutListener(listener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        rootView = null;
        contentContainer = null;
        viewTreeObserver = null;
    }

    private void possiblyResizeChildOfContent() {
        if (contentContainer == null) {
            contentContainer = (ViewGroup) findViewById(android.R.id.content);
            rootView = contentContainer.getChildAt(0);
            rootViewLayout = (FrameLayout.LayoutParams) rootView.getLayoutParams();
        }
        contentContainer.getWindowVisibleDisplayFrame(contentAreaOfWindowBounds);
        int usableHeightNow = contentAreaOfWindowBounds.height();

        if (usableHeightNow != usableHeightPrevious) {
            rootViewLayout.height = usableHeightNow;
            rootView.layout(contentAreaOfWindowBounds.left, contentAreaOfWindowBounds.top, contentAreaOfWindowBounds.right, contentAreaOfWindowBounds.bottom);
            rootView.requestLayout();

            usableHeightPrevious = usableHeightNow;
        }
    }
}
DeanWookey commented 3 years ago

I see Attach has a keyboard service with functionality to get the keyboard height. I'll look into using that instead.

jperedadnr commented 3 years ago

Currently on Android there is no interaction between the JavaFX view with the keyboard view, so that's why the KeyboardService provides some API that you can use in several ways.

Note this service also applies in the exact same way to iOS.

DeanWookey commented 3 years ago

@jperedadnr Thanks. It works much better than the MainActivity hack.