Пользовательская клавиатура Android - предварительный просмотр ограничен родительским макетом

Я создал пользовательскую клавиатуру, которая прекрасно работает - за исключением того, что предварительные просмотры для двух верхних рядов клавиш не отображаются достаточно высоко. Их вертикальное положение ограничено родительским макетом.

Эти скриншоты иллюстрируют проблему - позиция предварительного просмотра для "0" и "8" хорошая, но для "5" и "2" это не так:

Предварительный просмотр для ключа '0' показан над кнопкой...

Кнопка

Предварительный просмотр для ключа '8' также показан над кнопкой...

Кнопка

Но предварительный просмотр для клавиши "5" не отображается над кнопкой...

Кнопка

И предварительный просмотр для клавиши "2" не отображается над кнопкой...

Кнопка

Как преодолеть, чтобы предварительный просмотр для "5" и "2" отображался на том же расстоянии над их соответствующими клавишами, как и для "0" и "8".

Вот моя клавиатура.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" />

Вот мой key_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" />

Вот моя раскладка клавиатура_имя.xml...

<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>

3 ответа

Ключ предварительного просмотра на самом деле PopupWindow созданный в конструкторе для KeyboardView, (См. Код здесь).

Проблема, которую вы видите, заключается в том, что PopupWindow обрезается его родителем. Если отсечение может быть отключено для PopupWindow тогда клавиша предварительного просмотра сможет "выскочить" за пределы клавиатуры. Вы можете увидеть, что этот подход работает, если вы установите точку останова в приведенном выше коде и выполните следующий код:

mPreviewPopup.setClippingEnabled(false)

Смотрите setClippingEnabled в документации для PopupWindow, По умолчанию отсечение включено.

setClippingEnabled

void setClippingEnabled (логическое значение включено)

Позволяет всплывающему окну выходить за границы экрана. По умолчанию окно обрезается до границ экрана. Если установить значение false, окна будут точно расположены.

Он говорит "экран", но это относится и к окну клавиатуры.

Остается вопрос: как отключить отсечение? К несчастью, mPreviewPopup является частной переменной Отражение даст доступ, но не идеально. Я не нашел другого способа отключить отсечение для этого частного PopupWindowтак что это просто указывает путь к разрешению, а не само разрешение.


Обновление: я еще раз взглянул на то, что происходит. Вот что я нашел с различными уровнями API.

API-интерфейсы 17-21: предварительный просмотр клавиш разрешен за пределами клавиатуры.

API 22,23,25-26: предварительный просмотр клавиш ограничен границами клавиатуры, но отображается полностью. Это то, что видел ОП.

API 24: предварительный просмотр клавиш ограничен границами клавиатуры, но в остальном ограничен. (Сломанный)

Таким образом, изменение произошло с API 22. Единственное существенное различие, которое я вижу между API 21 и API 22, заключается в поддержке флага FLAG_LAYOUT_ATTACHED_IN_DECOR. (Также см. SetAttachedInDecor.) Этот код имеет дело с размещением всплывающего окна, поэтому он может быть связан с этой проблемой. (Я больше не верю, что это правда.)

Я также нашел это, что также может быть связано. (Может быть...)

setClippingEnabled() работает на API 22-26, чтобы позволить клавише предварительного просмотра выскочить за пределы раскладки клавиатуры. Кроме использования отражения, я не вижу способа исправить проблему.

Это может претендовать на сообщение об ошибке, хотя новым ограниченным поведением может быть ожидаемое поведение.

Вот видео API 26, демонстрирующее проблему, затем я установил mClippingEnabled флаг для false в PopupWindow.java и показать исправленное поведение.

Для тех, кто придет сюда в будущем, я рекомендую не использовать KeyboardView, По крайней мере, на момент написания этой статьи (API 27) она долгое время не обновлялась. Проблема, описанная в приведенном выше вопросе, является лишь одним из многих ее недостатков.

Это больше работы, но вы можете сделать свой собственный вид для использования в качестве клавиатуры. Я опишу этот процесс в конце этого ответа.

Когда вы создаете всплывающее окно предварительного просмотра на пользовательской клавиатуре, вам нужно вызвать popupWindow.setClippingEnabled(false) чтобы всплывающее окно отображалось над клавиатурой для Android API 22+. (Благодаря этому ответу за это понимание.)

Вот пример в контексте одного из моих недавних проектов.

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

Потратив несколько часов на эту проблему, я, наконец, смог найти решение, которое, по моему мнению, не слишком удачно. В вашем обычае InputMethodServiceсохранить ссылку на ваш KeyboardView, mKeyboardViewи затем переопределить onStartInputView(), Затем возьмите родителя KeyboardView, примените некоторые отступы к его верхней границе и, наконец, вызовите setPopupParent() на KeyboardView с этим измененным родительским макетом. Вот код, который у меня есть:

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

Вы можете отфильтровать поведение этого переопределенного метода в соответствии с API, в которых существует эта проблема.

Слава Танг Ке за упоминание setPopupParent(), Вы можете попробовать переопределить другой метод и использовать ту же логику, если хотите, однако вы не можете сделать это в onCreateInputView() так как родитель KeyboardView еще не будет создан.

Другие вопросы по тегам