PopupWindow получает привязку к пользовательской клавиатуре для Android API 28
Я сделал пользовательскую клавиатуру. Когда вы долго нажимаете клавишу, PopupWindow
показывает некоторые дополнительные варианты над клавишей. Проблема в том, что в API 28 это всплывающее окно обрезается (или даже полностью скрывается для верхнего ряда).
![enter image description here]()
Я решил эту проблему для API <28 с
popupWindow.setClippingEnabled(false);
Однако с API 28 проблема вернулась. Вот еще код:
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);
}
Что-то случилось, что больше не позволяло сторонним клавиатурам показывать контент вне вида клавиатуры? (Это как в iOS.)
Что мне нужно сделать, чтобы PopupWindow
перестало обрезаться?
Ответы
Ответ 1
Обновлен, чтобы показать более индивидуальный подход.
Обновлено для работы с windowSoftInputMode="adjustResize"
.
Похоже, отсечение за пределами окон может быть новым фактом жизни Android, хотя я не нашел документации на этот счет. Несмотря на это, следующий метод может быть предпочтительным путем и, я полагаю, стандарт, хотя и не очень хорошо документирован.
В последующем MyInputMethodService
создает клавиатуру с восемью клавишами внизу и пустой полосой просмотра сверху, где отображаются всплывающие окна для верхнего ряда клавиш. Когда клавиша нажата, значение клавиши отображается во всплывающем окне над клавишей на время нажатия клавиши. Поскольку пустое представление над клавишами закрывает всплывающие окна, отсечение не происходит. (Не очень полезная клавиатура, но она имеет смысл.)
![enter image description here]()
Кнопка и "Низкий текст" EditText
находятся под верхней полосой просмотра. Вызов onComputeInsets()
разрешает касания клавиш клавиатуры, но запрещает касания клавиатуры в пустой области, закрытой вставкой. В этой области прикосновения передаются к базовым представлениям - здесь EditText
"Низкий текст" и Button
которая отображает "OK!" при нажатии.
"Gboard", кажется, работает аналогичным образом, но использует сестринский FrameLayout
для отображения всплывающих окон с переводом. Вот как выглядит всплывающее окно "4" в инспекторе макетов для "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
View
определено исключительно для того, чтобы дать всплывающим окнам возможность расширяться и не имеет никакой другой цели.
<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
Просто всплывающее окно.
<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>
Ответ 2
PopupWindow
идея показа всплывающих окон - создать их с помощью WindowManager
который не имеет ограничений PopupWindow
.
Я предполагаю, что InputMethodService
отвечает за отображение всплывающего окна. Поскольку для отображения такого окна необходимо получить разрешение наложения в API 23 и более поздних версиях, нам необходимо создать временное Activity
чтобы сделать это для нас. Результат получения разрешения будет доставлен в InputMethodService
с использованием события EventBus
. Вы можете проверить разрешение наложения в соответствии с архитектурой (например, каждый раз, когда клавиатура поднимается).
Вот реализация этой идеи, которая может потребовать некоторых манипуляций, чтобы работать именно так, как вы хотите. Я надеюсь, что это помогает.
MyInputMethodService.java
import android.content.Intent;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.provider.Settings;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
public class MyInputMethodService extends InputMethodService {
private FloatViewManager mFloatViewManager;
@Override
public void onCreate() {
super.onCreate();
EventBus.getDefault().register(this);
checkDrawOverlayPermission();
}
@Override
public void onDestroy() {
super.onDestroy();
EventBus.getDefault().unregister(this);
}
private boolean checkDrawOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
Intent intent = new Intent(this, CheckPermissionActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
return false;
} else {
return true;
}
}
private void showPopup(Key key, int xPosition){
mFloatViewManager = new FloatViewManager(this);
if (checkDrawOverlayPermission()) {
mFloatViewManager.showFloatView(key, xPosition);
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(CanDrawOverlaysEvent event) {
if (event.isAllowed()) {
mFloatViewManager.showFloatView(key, xPosition);
} else {
// Maybe show an error
}
}
}
FloatViewManager.java
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Build;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import static android.content.Context.WINDOW_SERVICE;
public class FloatViewManager {
private WindowManager mWindowManager;
private View mFloatView;
private WindowManager.LayoutParams mFloatViewLayoutParams;
@SuppressLint("InflateParams")
public FloatViewManager(Context context) {
mWindowManager = (WindowManager) context.getSystemService(WINDOW_SERVICE);
LayoutInflater inflater = LayoutInflater.from(context);
mFloatView = inflater.inflate(R.layout.float_view_layout, null);
// --------- do initializations:
TextView textView = mFloatView.findViewById(R.id.textView);
// ...
// ---------
mFloatViewLayoutParams = new WindowManager.LayoutParams();
mFloatViewLayoutParams.format = PixelFormat.TRANSLUCENT;
mFloatViewLayoutParams.flags = WindowManager.LayoutParams.FORMAT_CHANGED;
mFloatViewLayoutParams.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
: WindowManager.LayoutParams.TYPE_PHONE;
mFloatViewLayoutParams.gravity = Gravity.NO_GRAVITY;
mFloatViewLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mFloatViewLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
}
public void dismissFloatView() {
mWindowManager.removeViewImmediate(mFloatView);
}
public void showFloatView(Key key, int xPosition) {
// calculate x and y position as you did instead of 0
mFloatViewLayoutParams.x = 0;
mFloatViewLayoutParams.y = 0;
mWindowManager.addView(mFloatView, mFloatViewLayoutParams);
mWindowManager.updateViewLayout(mFloatView, mFloatViewLayoutParams);
}
}
CheckPermissionActivity.java
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import org.greenrobot.eventbus.EventBus;
public class CheckPermissionActivity extends AppCompatActivity {
private static final int REQUEST_CODE_DRAW_OVERLAY_PERMISSION = 5;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE_DRAW_OVERLAY_PERMISSION);
} else {
finish();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_DRAW_OVERLAY_PERMISSION) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(this)) {
EventBus.getDefault().post(new CanDrawOverlaysEvent(true));
} else {
EventBus.getDefault().post(new CanDrawOverlaysEvent(false));
}
finish();
}
}
}
CanDrawOverlaysEvent.java
public class CanDrawOverlaysEvent {
private boolean mIsAllowed;
public CanDrawOverlaysEvent(boolean isAllowed) {
mIsAllowed = isAllowed;
}
public boolean isAllowed() {
return mIsAllowed;
}
}
build.gradle
dependencies {
implementation 'org.greenrobot:eventbus:3.1.1'
}
Ответ 3
Я исправил это с помощью LatinIME (AOSP), например:
- мой xml файл макета ввода
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.my.android.ime.InputView
android:id="@+id/input_view"
android:background="@color/black"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>
- скопировать функцию "updateSoftInputWindowLayoutParameters" из LatinIME.java
private void updateSoftInputWindowLayoutParameters() {
// Override layout parameters to expand {@link SoftInputWindow} to the entire screen.
// See {@link InputMethodService#setinputView(View)} and
// {@link SoftInputWindow#updateWidthHeight(WindowManager.LayoutParams)}.
final Window window = getWindow().getWindow();
ViewLayoutUtils.updateLayoutHeightOf(window, LayoutParams.MATCH_PARENT);
// This method may be called before {@link #setInputView(View)}.
if (mInputView != null) {
// In non-fullscreen mode, {@link InputView} and its parent inputArea should expand to
// the entire screen and be placed at the bottom of {@link SoftInputWindow}.
// In fullscreen mode, these shouldn't expand to the entire screen and should be
// coexistent with {@link #mExtractedArea} above.
// See {@link InputMethodService#setInputView(View) and
// com.android.internal.R.layout.input_method.xml.
final int layoutHeight = isFullscreenMode()
? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT;
final View inputArea = window.findViewById(android.R.id.inputArea);
ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight);
ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM);
ViewLayoutUtils.updateLayoutHeightOf(mInputView, layoutHeight);
}
}
- Переопределяющая функция: "updateFullscreenMode", "setInputView", "onComputeInsets" и копирование кода из LatinIME.java - наконец, измените код, например,
private View mInputView;
private InsetsUpdater mInsetsUpdater;
...
@Override
public void onStartInputView(EditorInfo info, boolean restarting) {
...
updateFullscreenMode();
super.onStartInputView(info, restarting);
}
@Override
public void updateFullscreenMode() {
super.updateFullscreenMode();
updateSoftInputWindowLayoutParameters();
}
@Override
public void setInputView(final View view) {
super.setInputView(view);
mInputView = view;
mInsetsUpdater = ViewOutlineProviderCompatUtils.setInsetsOutlineProvider(view);
updateSoftInputWindowLayoutParameters();
//mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
//if (hasSuggestionStripView()) {
// mSuggestionStripView.setListener(this, view);
//}
}
@Override
public void onComputeInsets(final InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
// This method may be called before {@link #setInputView(View)}.
if (mInputView == null) {
return;
}
//final SettingsValues settingsValues = mSettings.getCurrent();
//final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
final View visibleKeyboardView = mInputView.findViewById(R.id.input_view);
//if (visibleKeyboardView == null || !hasSuggestionStripView()) {
// return;
//}
final int inputHeight = mInputView.getHeight();
//if (isImeSuppressedByHardwareKeyboard() && !visibleKeyboardView.isShown()) {
// // If there is a hardware keyboard and a visible software keyboard view has been hidden,
// // no visual element will be shown on the screen.
// outInsets.contentTopInsets = inputHeight;
// outInsets.visibleTopInsets = inputHeight;
// mInsetsUpdater.setInsets(outInsets);
// return;
//}
//final int suggestionsHeight = (!mKeyboardSwitcher.isShowingEmojiPalettes()
// && mSuggestionStripView.getVisibility() == View.VISIBLE)
// ? mSuggestionStripView.getHeight() : 0;
final int visibleTopY = inputHeight - visibleKeyboardView.getHeight();// - suggestionsHeight;
//mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
// Need to set expanded touchable region only if a keyboard view is being shown.
if (visibleKeyboardView.isShown()) {
final int touchLeft = 0;
//final int touchTop = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
final int touchTop = visibleTopY;
final int touchRight = visibleKeyboardView.getWidth();
final int touchBottom = inputHeight;
outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom);
Log.i(TAG, "onComputeInsets: left=" + touchLeft + ", top=" + touchTop + ", right=" + touchRight + ", bottom=" + touchBottom);
}
Log.i(TAG, "onComputeInsets: visibleTopY=" + visibleTopY);
outInsets.contentTopInsets = visibleTopY;
outInsets.visibleTopInsets = visibleTopY;
mInsetsUpdater.setInsets(outInsets);
}
- скопировать файл "ViewLayoutUtils.java", "ViewOutlineProviderCompatUtils.java", "ViewOutlineProviderCompatUtilsLXX.java" из пакета LatinIME (AOSP) и изменить имя пакета