如何让软键盘再次出现

How to make the SoftKeyboard show up again

当您通过 Android Back Button 隐藏 SoftKeyboard 时,触摸(仍然聚焦的)inputNode 不会再次显示键盘。 为了解决这个问题,我使用了以下 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);
            }
        });
    }
}

虽然这可行,但它似乎是一个丑陋的解决方法。如果不使用 JDK 内部 classes (TextFieldSkin, HitInfo) 怎么能做到这一点?

编辑: 这是另一个解决方案,基于 José Pereda 的回答:

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);
        }
    }
}

如您所知,Android 的 JavaFX 层管理软键盘,它实际上仅由焦点 gained/lost event.

触发

所以你的做法是正确的,但是如果你想避免私有 API,我看到两种可能的解决方案:

  • 进入JavaFX层,修改并构建... 可以做到,但是工作量大,下一次发布JavaFXPorts版本时会中断。

  • 创建自定义插件并提供API方便管理软键盘。

对于第二个选项,使用新的 Down 插件很容易做到 API。在你的主包下创建包 com.gluonhq.charm.down.plugins 这两个 classes:

键盘服务

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);
    }

}

现在在Android包下,在com.gluonhq.charm.down.plugins.android包下添加这个class:

Android键盘服务

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);
        }
    }

}

现在,通过您的代码,您可以轻松地从文本字段调用键盘。

我添加了 long-press 类型的事件,基于此 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());
}

因此,如果您有一个 TextField 并且想要在用户按住它一段时间时调用键盘,您只需要:

TextField textField = new TextField();

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

编辑 注意:我添加了键盘的可见性控制,基于此 answer.

额外提示:按照这种方法,您只需一步就可以在长按时提供触觉反馈...

我们通过添加 KeyEvents 侦听器改进了您的初始版本,并在 Android 和 iOS 上成功测试了它。

defocus-method 可以很容易地在视图开始时调用,这样它就不会自动聚焦第一个 "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();
  }
}