Android custom keyboard - Preview view constrained to parent layout
Asked Answered
I

3

12

I have created a custom keyboard, which works fine - except the preview views for the top two rows of keys are not displayed high enough. Their vertical position is being constrained by the parent layout.

These screenshots illustrate the problem - the position of the preview for '0' and '8' is good, but for '5' and '2' it is not:

The preview for key '0' is shown above the button...

Button '0'

The preview for key '8' is also shown above the button...

Button '8'

But the preview for key '5' is not shown above the button...

Button '5'

And the preview for key '2' is not shown above the button...

Button '2'

How to overcome, so the preview for '5' and '2' are displayed at the same distance above their respective key as it is for '0' and '8'.

Here is my keyboard.xml...

<android.inputmethodservice.KeyboardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/keyboard"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:keyPreviewLayout="@layout/keyboard_key_preview" />

Here is my keyboard_key_preview.xml...

<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:background="@color/keyboard_preview_bg"
    android:textColor="@color/keyboard_preview_fg"
    android:textStyle="bold"
    android:textSize="@dimen/keyboard_preview_text_size" />

Here is my keyboard_numeric.xml layout...

<Keyboard
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:keyWidth="33.33%p"
    android:keyHeight="@dimen/keyboard_key_height"
    android:horizontalGap="@dimen/keyboard_horizontal_gap"
    android:verticalGap="@dimen/keyboard_vertical_gap">
    <Row android:rowEdgeFlags="top">
        <Key android:codes="49" android:keyLabel="1" android:keyEdgeFlags="left"/>
        <Key android:codes="50" android:keyLabel="2"/>
        <Key android:codes="51" android:keyLabel="3" android:keyEdgeFlags="right"/>
    </Row>
    <Row>
        <Key android:codes="52" android:keyLabel="4" android:keyEdgeFlags="left"/>
        <Key android:codes="53" android:keyLabel="5"/>
        <Key android:codes="54" android:keyLabel="6" android:keyEdgeFlags="right"/>
    </Row>
    <Row>
        <Key android:codes="55" android:keyLabel="7" android:keyEdgeFlags="left"/>
        <Key android:codes="56" android:keyLabel="8"/>
        <Key android:codes="57" android:keyLabel="9" android:keyEdgeFlags="right"/>
    </Row>
    <Row android:rowEdgeFlags="bottom">
        <Key android:codes="-5" android:keyIcon="@drawable/ic_backspace_white_24dp" android:isRepeatable="true" android:keyEdgeFlags="left" />
        <Key android:codes="48" android:keyLabel="0"/>
        <Key android:codes="-4" android:keyLabel="DONE" android:keyEdgeFlags="right"/>
    </Row>
</Keyboard>
Inexpiable answered 4/10, 2017 at 12:33 Comment(7)
Did you ever find a solution? In my tests this happens in at least Android 7.1 and 8.0. However, it doesn't happen in 5.1. Constraining the popup view to the keyboard area is what iOS does, too. I would like to find a workaround.Cadency
Also happens in Android 6.0.Cadency
For my keyboard I am using a custom view for the keyboard layout and a PopupWindow for the preview view. Neither showViewAtLocation nor showAsDropdown are able to make the popup display outside of the keyboard constraints. So this is not specifically a KeyboardView class problem.Cadency
For quick instructions on how to set this up, see this answer. Just add a preview to it.Cadency
I think there is an answer because this custom keyboard (see image) seems to be showing a popup going above the keyboard in Android 7.1.Cadency
Try to invoke KeyboardView.setPopupParent(View) method to change preview layout positionFranklinfranklinite
This issue is not present in API 29, at least not in the emulator. But is in API 28.Berey
E
9

The preview key is actually a PopupWindow created in the constructor for KeyboardView. (See the code here).

The problem that you are seeing is because the PopupWindow is being clipped by its parent. If clipping can be disabled for the PopupWindow then the preview key will be able to "pop" outside of the keyboard view. You can see that this approach works if you set a breakpoint at the above-referenced code and execute the following code:

mPreviewPopup.setClippingEnabled(false)

See setClippingEnabled in the documentation for PopupWindow. By default, clipping is enabled.

setClippingEnabled

void setClippingEnabled (boolean enabled)

Allows the popup window to extend beyond the bounds of the screen. By default the window is clipped to the screen boundaries. Setting this to false will allow windows to be accurately positioned.

It says "screen," but it applies to the keyboard window as well.

The remaining question is: How to get clipping disabled? Unfortunately, mPreviewPopup is a private variable. Reflection will give access but is not ideal. I have not found another way to disable clipping for this private PopupWindow, so this is just pointing a way to a resolution and not the resolution itself.


Update: I took another look at what is going on. Here is what I found with the various API levels.

APIs 17-21: Key preview is allowed to pop outside of the boundaries of the keyboard.

APIs 22,23,25-26: The key preview is constrained to the boundaries of the keyboard but does display in its entirety. This is what the OP was seeing.

API 24: Key preview is constrained to the boundaries of the keyboard but is otherwise clipped. (Broken)

So, the change occurred with API 22. The only substantive difference I see between API 21 and API 22 is support for the FLAG_LAYOUT_ATTACHED_IN_DECOR flag. (Also see setAttachedInDecor.) This code does deal with the placement of the popup window, so it may be related to this problem. (I no longer believe that this is true.)

I also found this which also may be related. (Maybe...)

setClippingEnabled() works on API 22-26 to permit the preview key to pop outside the boundaries of the keyboard layout. Other than using reflection, I don't see a way to correct the problem.

This may qualify for a bug report although the new constrained behavior may be the expected behavior.

Here is a video of API 26 exhibiting the problem, then I set the mClippingEnabled flag to false in PopupWindow.java and show the corrected behavior.

enter image description here

Erleena answered 5/4, 2018 at 23:24 Comment(6)
Very interesting! This is a big step forward in finding a solution. I will consider using reflection if no other solution can be found.Cadency
@Cadency Key preview pops high and outside the bounds of the keyboard for API 17. By API 23, the preview is wholly visible but is constrained by the keyboard. In API 24 the key preview is just broken. If there is no solution in the meantime, I will look at this again next week.Erleena
I am testing setClippingEnabled(false) in a custom keyboard view (thus bypassing KeyboardView's private mPreviewPopup), but I am still getting getting the clipping (tested on API 23 and 26). When you say setClippingEnabled works on API 22-26, would you mind posting the code you are using? I may also need to write a new question with a minimal example.Cadency
@Cadency In PopupWindow.java there is a line that reads if (!mClippingEnabled || mClipToScreen) {. I am setting a breakpoint on that line and manually setting mClippingEnabled to false and that does it.Erleena
@Cadency Added a video of what I am seeing before and after manually setting the mClippingEnabled flag to false.Erleena
You comment about the PopupWidow source code was helpful. That made me dig into why setClippingEnabled(false) wasn't working in my code. It was because I was setting it too late. Calling update() or moving it to right after creating the PopupWindow caused it to work. This solves the problem for my purposes. Thanks a lot for all your research and hard work!Cadency
C
5

For anyone coming here in the future, I recommend not using KeyboardView. At least at the time of this writing (API 27), it hasn't been updated for a long time. The problem described in the question above is just one of its many shortcomings.

It is more work, but you can make your own custom view to use as a keyboard. I describe that process at the end of this answer.

When you create the popup preview window in your custom keyboard, you need to call popupWindow.setClippingEnabled(false) in order to get the popup window to display above the keyboard for Android API 22+. (Thanks to this answer for that insight.)

Here is an example in context from one of my recent projects.

private void layoutAndShowPopupWindow(Key key, int xPosition) {
    popupWindow = new PopupWindow(popupView,
            LinearLayout.LayoutParams.WRAP_CONTENT,
            LinearLayout.LayoutParams.WRAP_CONTENT);
    popupWindow.setClippingEnabled(false);// <-- let popup display above keyboard
    int location[] = new int[2];
    key.getLocationInWindow(location);
    int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    popupView.measure(measureSpec, measureSpec);
    int popupWidth = popupView.getMeasuredWidth();
    int spaceAboveKey = key.getHeight() / 4;
    int x = xPosition - popupWidth / popupView.getChildCount() / 2;
    int y = location[1] - popupView.getMeasuredHeight() - spaceAboveKey;
    popupWindow.showAtLocation(key, Gravity.NO_GRAVITY, x, y);

    // using popupWindow.showAsDropDown(key, 0, yOffset) might be
    // easier than popupWindow.showAtLocation() 
}
Cadency answered 9/4, 2018 at 4:39 Comment(0)
F
1

After spending hours on this issue, I was finally able to find a solution which I feel is reasonably un-hacky. In your custom InputMethodService, maintain a reference to your KeyboardView, mKeyboardView, and then override onStartInputView(). Then, grab the KeyboardView's parent, apply some padding to its top boundary, and finally, call setPopupParent() on the KeyboardView with this modified parent layout. Here's the code that I have:

@Override
public void onStartInputView(EditorInfo info, boolean restarting){
    ViewGroup originalParent = (ViewGroup) mKeyboardView.getParent();
    if (originalParent != null) {
        originalParent.setPadding(0,LAYOUT_PADDING, 0, 0);
        mKeyboardView.setPopupParent(originalParent);
    }
}

You can filter the behavior of this overriden method according to the APIs on which this problem exists.

Kudos to Tang Ke for mentioning setPopupParent(). You could try overriding a different method and using the same logic if you wanted, however, you cannot do it in onCreateInputView() as the KeyboardView's parent will not have been created yet.

Foley answered 9/4, 2018 at 2:6 Comment(4)
+1 for your work and creative approach. However, a problem with this method is that any content under the padding (although visible) is no longer selectable. This would be undesirable if a user was trying to update the cursor position in an EditText appearing directly above what appears to be the top of the keyboard.Cadency
Thanks, and that's definitely a good point, I didn't realize that. I just tried a possible workaround, which is to set the padding in onPress() and then unset it in onRelease(), but this causes the keyboard to jitter a bit. There might some other workaround though.Foley
My current workaround is to just abandon KeyboardView in favor of a custom view that does popupWindow.setClippingEnabled(false).Cadency
Yes, that's definitely the way to go, and I believe that's how most of the nicer keyboards on the Play Store are implemented. KeyboardView isn't maintained well, and frankly, the documentation is lacking. Having methods like setPopupParent() and setPopupOffset() public and without documentation alone should be enough to raise eyebrows.Foley

© 2022 - 2024 — McMap. All rights reserved.