Search code examples
androidcustom-keyboardandroid-popupwindow

PopupWindow getting clipped on custom keyboard for Android API 28


I made a custom keyboard. When you long press a key, a PopupWindow shows some extra choices above the key. The problem is that in API 28, this popup gets clipped (or even completely hidden for the top row).

enter image description here

I had solved this problem for API < 28 with

popupWindow.setClippingEnabled(false);

However, with API 28 the problem has come back. Here is more of the code:

private void layoutAndShowPopupWindow(Key key, int xPosition) {
    popupWindow = new PopupWindow(popupView,
            LinearLayout.LayoutParams.WRAP_CONTENT,
            LinearLayout.LayoutParams.WRAP_CONTENT);
    popupWindow.setClippingEnabled(false);
    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 screenWidth = getScreenWidth();
    if (x < 0) {
        x = 0;
    } else if (x + popupWidth > screenWidth) {
        x = screenWidth - popupWidth;
    }
    int y = location[1] - popupView.getMeasuredHeight() - spaceAboveKey;
    popupWindow.showAtLocation(key, Gravity.NO_GRAVITY, x, y);
}

Did something happen to no longer allow third party keyboards to show content outside of the keyboard view? (This is how it is in iOS.)

What do I need to do to get the PopupWindow to stop being clipped?


Solution

  • Updated to show a more tailored approach.
    Updated to work with windowSoftInputMode="adjustResize".

    It looks like clipping outside of windows may be a new fact of Android life although I have not found documentation to that effect. Regardless, the following method may be the preferred way to go and is, I believe, standard although not very well documented.

    In the following, MyInputMethodService instantiates a keyboard that has eight keys on the bottom and an empty view strip above where popups are displayed for the top row of keys. When a key is pressed, the key value is shown in a popup window above the key for the duration of the key press. Since the empty view above the keys encloses the popups, clipping does not occur. (Not a very useful keyboard, but it makes the point.)

    enter image description here

    The button and "Low text" EditText are under the top view strip. Invocation of onComputeInsets() permits touches on the keyboard keys but disallows keyboard touches in the empty area covered by the inset. In this area, touches are passed down to the underlying views - here the "Low text" EditText and a Button that displays "OK!" when clicked.

    "Gboard" seems to work in a similar fashion but uses a sister FrameLayout to display the popups with translation. Here is what a "4" popup looks like in the Layout Inspector for "Gboard".

    enter image description here

    MyInputMethodService

    public class MyInputMethodService extends InputMethodService
        implements View.OnTouchListener {
        private View mTopKey;
        private PopupWindow mPopupWindow;
        private View mPopupView;
    
        @Override
        public View onCreateInputView() {
            final ConstraintLayout keyboardView = (ConstraintLayout) getLayoutInflater().inflate(R.layout.keyboard, null);
            mTopKey = keyboardView.findViewById(R.id.a);
            mTopKey.setOnTouchListener(this);
            keyboardView.findViewById(R.id.b).setOnTouchListener(this);
            keyboardView.findViewById(R.id.c).setOnTouchListener(this);
            keyboardView.findViewById(R.id.d).setOnTouchListener(this);
            keyboardView.findViewById(R.id.e).setOnTouchListener(this);
            keyboardView.findViewById(R.id.f).setOnTouchListener(this);
            keyboardView.findViewById(R.id.g).setOnTouchListener(this);
            keyboardView.findViewById(R.id.h).setOnTouchListener(this);
    
            mPopupView = getLayoutInflater().inflate(R.layout.popup, keyboardView, false);
            int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
            mPopupView.measure(measureSpec, measureSpec);
            mPopupWindow = new PopupWindow(mPopupView, ViewGroup.LayoutParams.WRAP_CONTENT,
                                           ViewGroup.LayoutParams.WRAP_CONTENT);
    
            return keyboardView;
        }
    
        @Override
        public void onComputeInsets(InputMethodService.Insets outInsets) {
            // Do the standard stuff.
            super.onComputeInsets(outInsets);
    
            // Only the keyboard are with the keys is touchable. The rest should pass touches
            // through to the views behind. contentTopInsets set to play nice with windowSoftInputMode
            // defined in the manifest.
            outInsets.visibleTopInsets = mTopKey.getTop();
            outInsets.contentTopInsets = mTopKey.getTop();
        }
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    layoutAndShowPopupWindow((TextView) v);
                    break;
    
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mPopupWindow.dismiss();
                    break;
            }
            return true;
        }
    
        private void layoutAndShowPopupWindow(TextView key) {
            ((TextView) mPopupView.findViewById(R.id.popupKey)).setText(key.getText());
            int x = key.getLeft() + (key.getWidth() - mPopupView.getMeasuredWidth()) / 2;
            int y = key.getTop() - mPopupView.getMeasuredHeight();
            mPopupWindow.showAtLocation(key, Gravity.NO_GRAVITY, x, y);
        }
    }
    

    keyboard.xml
    The View is defined solely to give the popups a place to expand into and has no other purpose.

    <android.support.constraint.ConstraintLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <View
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toTopOf="@+id/a" />
    
        <Button
            android:id="@+id/a"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="A"
            app:layout_constraintBottom_toTopOf="@+id/e"
            app:layout_constraintEnd_toStartOf="@+id/b"
            app:layout_constraintStart_toStartOf="parent" />
    
        <Button
            android:id="@+id/b"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="B"
            app:layout_constraintBottom_toTopOf="@+id/f"
            app:layout_constraintEnd_toStartOf="@+id/c"
            app:layout_constraintStart_toEndOf="@+id/a" />
    
        <Button
            android:id="@+id/c"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="C"
            app:layout_constraintBottom_toTopOf="@+id/g"
            app:layout_constraintEnd_toStartOf="@+id/d"
            app:layout_constraintStart_toEndOf="@+id/b" />
    
        <Button
            android:id="@+id/d"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="D"
            app:layout_constraintBottom_toTopOf="@+id/h"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/c" />
    
        <Button
            android:id="@+id/e"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="E"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/f"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent" />
    
        <Button
            android:id="@+id/f"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="F"
            app:layout_constraintEnd_toStartOf="@+id/g"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/e"
            app:layout_constraintTop_toTopOf="@+id/e" />
    
        <Button
            android:id="@+id/g"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="G"
            app:layout_constraintEnd_toStartOf="@+id/h"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/f"
            app:layout_constraintTop_toTopOf="@+id/e" />
    
        <Button
            android:id="@+id/h"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="H"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/g"
            app:layout_constraintTop_toTopOf="@+id/g" />
    </android.support.constraint.ConstraintLayout>
    

    popup.xml
    Just the popup.

    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        xmlns:tools="http://schemas.android.com/tools"
        android:background="@android:color/black"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="3dp">
    
        <TextView
            android:id="@+id/popupKey"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:text="A"
            android:textColor="@android:color/white" />
    
    </LinearLayout>
    

    activity_main

    <android.support.constraint.ConstraintLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="High text"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="20dp"
            android:text="Button"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    
        <EditText
            android:id="@+id/editText"
            android:layout_width="133dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:ems="10"
            android:inputType="textPersonName"
            android:hint="Low text"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@+id/button" />
    
    </android.support.constraint.ConstraintLayout>