How to make the SoftKeyboard show up again

2019-01-28 23:04发布

问题:

When you hide the SoftKeyboard via the Android Back Button, touching the (still focused) inputNode won't make the keyboard show again. To solve this issue I'm using the following class:

public class RefocusableTextField extends TextField {

    private Region  fakeFocusTarget;

    public RefocusableTextField(String text) {
        this();
        setText(text);
    }

    public RefocusableTextField() {
        fakeFocusTarget = new Region();
        fakeFocusTarget.setManaged(false);
        getChildren().add(fakeFocusTarget);

        addEventFilter(MouseEvent.MOUSE_PRESSED, MouseEvent::consume);

        addEventHandler(MouseEvent.MOUSE_CLICKED, e ->
        {
            if (!isFocused()) {
                requestFocus();

            } else {
                fakeFocusTarget.requestFocus();
                requestFocus();

                HitInfo hitInfo = ((TextFieldSkin) getSkin()).getIndex(e.getX(), e.getY());
                ((TextFieldSkin) getSkin()).positionCaret(hitInfo, false);
            }
        });
    }
}

While this is working, it seems like an ugly workaround. How could this be done without using JDK internal classes (TextFieldSkin, HitInfo)?

EDIT: here is another solution, based on José Pereda's answer:

public class RefocusableTextField extends TextField {

    private Optional<KeyboardService> service;

    public RefocusableTextField(String text) {
        this();
        setText(text);
    }

    public RefocusableTextField() {
        service = Services.get(KeyboardService.class);

        addEventFilter(MouseEvent.MOUSE_PRESSED, event ->
        {
            if (!isFocused()) {
                event.consume();
            }
        });

        addEventHandler(MouseEvent.MOUSE_CLICKED, e ->
        {
            if (!isFocused()) {
                requestFocus();
                end();

            } else {
                service.ifPresent(KeyboardService::show);
            }
        });
    }
}


public class AndroidKeyboardService implements KeyboardService {

    private static final float       SCALE  = FXActivity.getInstance().getResources().getDisplayMetrics().density;

    private final InputMethodManager imm;

    private Rect                     currentBounds;
    private DoubleProperty           visibleHeight;

    private OnGlobalLayoutListener   layoutListener;

    private boolean                  keyboardVisible;

    public AndroidKeyboardService() {
        imm = (InputMethodManager) FXActivity.getInstance().getSystemService(FXActivity.INPUT_METHOD_SERVICE);
        initLayoutListener();
    }

    private void initLayoutListener() {
        double screenHeight = MobileApplication.getInstance().getScreenHeight();
        currentBounds = new Rect();    
        visibleHeight = new SimpleDoubleProperty(screenHeight);
        visibleHeight.addListener((ov, n, n1) -> onHeightChanged(n, n1));


        layoutListener = layoutListener(visibleHeight);

        FXActivity.getViewGroup().getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);
        Services.get(LifecycleService.class).ifPresent(l ->
        {
            l.addListener(LifecycleEvent.RESUME, () -> FXActivity.getViewGroup().getViewTreeObserver().addOnGlobalLayoutListener(layoutListener));
            l.addListener(LifecycleEvent.PAUSE, () -> FXActivity.getViewGroup().getViewTreeObserver().removeOnGlobalLayoutListener(layoutListener));
       });
    }

    private OnGlobalLayoutListener layoutListener(DoubleProperty height) {
        return () -> height.set(getCurrentHeigt());
    }

    private float getCurrentHeigt() {
        FXActivity.getViewGroup().getRootView().getWindowVisibleDisplayFrame(currentBounds);
        return currentBounds.height() / SCALE;
    }

    private void onHeightChanged(Number oldHeight, Number newHeight) {
        double heightDelta = newHeight.doubleValue() - oldHeight.doubleValue();
        keyboardVisible = heightDelta < 0;
    }

    @Override
    public boolean isKeyboardVisible() {
        return keyboardVisible;
    }

    @Override
    public void show() {
        if (!keyboardVisible) {
            imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
        }
    }

    @Override
    public void hide() {
        if (keyboardVisible) {
            imm.toggleSoftInput(0, InputMethodManager.HIDE_IMPLICIT_ONLY);
        }
    }
}

回答1:

As you know, the JavaFX layer for Android manages the soft keyboard, and it's actually triggered only by the focus gained/lost event.

So your approach is correct, but if you want to avoid private API, I see two possible solutions:

  • Go to the JavaFX layer, modify it and build it... It can be done, but it is a lot of work, and it will break on the next release of the JavaFXPorts version.

  • Create a custom plugin and provide API to manage the soft keyboard at your convenience.

For the second option, this is very easy to do with the new Down plugin API. On your main package create under the package com.gluonhq.charm.down.plugins these two classes:

KeyboardService

package com.gluonhq.charm.down.plugins;

public interface KeyboardService {
    public void show();
    public void hide();
}

KeyboardServiceFactory

package com.gluonhq.charm.down.plugins;

import com.gluonhq.charm.down.DefaultServiceFactory;

public class KeyboardServiceFactory extends DefaultServiceFactory<KeyboardService> {

    public KeyboardServiceFactory() {
        super(KeyboardService.class);
    }

}

And now under the Android package, add this class under the package com.gluonhq.charm.down.plugins.android:

AndroidKeyboardService

package com.gluonhq.charm.down.plugins.android;

import android.view.inputmethod.InputMethodManager;
import com.gluonhq.charm.down.plugins.KeyboardService;
import javafxports.android.FXActivity;

public class AndroidKeyboardService implements KeyboardService {

    private final InputMethodManager imm;

    private boolean visible = false;

    public AndroidKeyboardService() {
        imm = (InputMethodManager) FXActivity.getInstance().getSystemService(FXActivity.INPUT_METHOD_SERVICE);

        final ViewTreeObserver.OnGlobalLayoutListener listener = () -> {
            Rect rect = new Rect();
            FXActivity.getViewGroup().getWindowVisibleDisplayFrame(rect);
            int heightDiff = FXActivity.getViewGroup().getRootView().getHeight() - rect.height();
            visible = (heightDiff > FXActivity.getViewGroup().getRootView().getHeight() / 4);
        };

        Services.get(LifecycleService.class).ifPresent(l -> {
            l.addListener(LifecycleEvent.RESUME, () -> 
                    FXActivity.getViewGroup().getViewTreeObserver().addOnGlobalLayoutListener(listener));
            l.addListener(LifecycleEvent.PAUSE, () -> 
                    FXActivity.getViewGroup().getViewTreeObserver().removeOnGlobalLayoutListener(listener));
        });
        FXActivity.getViewGroup().getViewTreeObserver().addOnGlobalLayoutListener(listener))
    }

    @Override
    public void show() {
        if (!visible) {
            imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
        }
    }

    @Override
    public void hide() {
        if (visible) {
            imm.toggleSoftInput(0, InputMethodManager.HIDE_IMPLICIT_ONLY);
        }
    }

}

Now from your code, you can easily call the keyboard from your textfield.

I've added a long-press type of event, based on this implementation:

private void addPressAndHoldHandler(Node node, Duration holdTime, EventHandler<MouseEvent> handler) {
    class Wrapper<T> { 
        T content; 
    }

    Wrapper<MouseEvent> eventWrapper = new Wrapper<>();

    PauseTransition holdTimer = new PauseTransition(holdTime);
    holdTimer.setOnFinished(event -> handler.handle(eventWrapper.content));

    node.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
        eventWrapper.content = event;
        holdTimer.playFromStart();
    });
    node.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> holdTimer.stop());
    node.addEventHandler(MouseEvent.DRAG_DETECTED, event -> holdTimer.stop());
}

so in case you have a TextField and want to call the keyboard when the user presses and holds on it for a while, all you need is:

TextField textField = new TextField();

addPressAndHoldHandler(textField, Duration.seconds(1), event -> 
    Services.get(KeyboardService.class)
         .ifPresent(KeyboardService::show));

Edit Note: I have added visibility control of the keyboard, based on this answer.

Extra Hint: Following this approach you are just one step away of providing haptic feedback on that long press...



回答2:

We improved the initial version of yours with adding a listener for KeyEvents and tested it successfully on Android and iOS.

The defocus-method can easily be called at the start of a view, so that it won't automatically focus the first textfield with "Platform.runlater(() -> textField.defocus());".

public class RefocusableTextField extends TextField {

  private Region fakeFocusTarget;

  public RefocusableTextField(String text) {
    this();
    setText(text);
  }

  public RefocusableTextField() {
    fakeFocusTarget = new Region();
    fakeFocusTarget.setManaged(false);
    getChildren().add(fakeFocusTarget);

    addEventFilter(MouseEvent.MOUSE_PRESSED, MouseEvent::consume);

    addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
      if (!isFocused()) {
        requestFocus();
      } else {
        defocus();
      }
    });

    addEventHandler(KeyEvent.KEY_PRESSED, e -> {
      System.out.println(e.getCode());
      if (e.getCode().equals(KeyCode.ENTER)) {
        defocus();
      }
    });
  }

  public void defocus() {
    fakeFocusTarget.requestFocus();
  }
}