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);
}
}
}
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
KeyboardServiceFactory
And now under the Android package, add this class under the package
com.gluonhq.charm.down.plugins.android
:AndroidKeyboardService
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: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:
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...
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());".