Skip to content

Commit

Permalink
Significantly improve layout re-calculations
Browse files Browse the repository at this point in the history
Signed-off-by: Mario Danic <[email protected]>
  • Loading branch information
mario committed Jan 26, 2019
1 parent 738fe41 commit 6ad9ce9
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 82 deletions.
165 changes: 83 additions & 82 deletions emoji/src/main/java/com/vanniktech/emoji/EmojiPopup.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package com.vanniktech.emoji;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.support.annotation.CheckResult;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.PopupWindow;

import com.vanniktech.emoji.emoji.Emoji;
import com.vanniktech.emoji.listeners.OnEmojiBackspaceClickListener;
import com.vanniktech.emoji.listeners.OnEmojiClickListener;
Expand All @@ -25,12 +26,15 @@
import com.vanniktech.emoji.listeners.OnEmojiPopupShownListener;
import com.vanniktech.emoji.listeners.OnSoftKeyboardCloseListener;
import com.vanniktech.emoji.listeners.OnSoftKeyboardOpenListener;
import java.lang.reflect.Field;

import static com.vanniktech.emoji.Utils.backspace;
import static com.vanniktech.emoji.Utils.checkNotNull;
import static com.vanniktech.emoji.Utils.shouldOverrideRegularCondition;

public final class EmojiPopup {
static final String TAG = "EmojiPopup";

static final int MIN_KEYBOARD_HEIGHT = 100;

final View rootView;
Expand All @@ -54,57 +58,74 @@ public final class EmojiPopup {
@Nullable OnEmojiClickListener onEmojiClickListener;
@Nullable OnEmojiPopupDismissListener onEmojiPopupDismissListener;

int correctionFactor;
int viewInset;
Rect rect = new Rect();

final ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override @SuppressWarnings("PMD.CyclomaticComplexity") public void onGlobalLayout() {
final Rect rect = Utils.windowVisibleDisplayFrame(context);
final int heightDifference = Utils.getScreenHeight(context) - rect.bottom;

final boolean shouldOverrideRegularCondition = Utils.shouldOverrideRegularCondition(context, editText);

if (heightDifference > Utils.dpToPx(context, MIN_KEYBOARD_HEIGHT) || shouldOverrideRegularCondition) {
correctionFactor = rect.top;
updateKeyboardState();
}
};

int height = 0;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private int getViewInset() {
try {
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
attachInfoField.setAccessible(true);
Object attachInfo = attachInfoField.get(rootView);
if (attachInfo != null) {
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
stableInsetsField.setAccessible(true);
Rect insets = (Rect) stableInsetsField.get(attachInfo);
return insets.bottom;
}
} catch (NoSuchFieldException nsfe) {
Log.w(TAG, "Field error when measuring view inset", nsfe);
} catch (IllegalAccessException iae) {
Log.w(TAG, "Illegal Access reflection error when measuring view inset", iae);
}

if (shouldOverrideRegularCondition) {
height = Utils.getScreenHeight(context) - editText.getHeight() - correctionFactor ;
popupWindow.setHeight(height);
} else {
height = heightDifference + correctionFactor;
popupWindow.setHeight(height);
}
popupWindow.setWidth(rect.right);
return 0;
}

if (!isKeyboardOpen && onSoftKeyboardOpenListener != null) {
onSoftKeyboardOpenListener.onKeyboardOpen(height);
}
private void updateKeyboardState() {
if (viewInset == 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
viewInset = getViewInset();
}

isKeyboardOpen = true;
rect = Utils.windowVisibleDisplayFrame(context);

if (isPendingOpen) {
showAtBottom();
isPendingOpen = false;
}
} else {
if (heightDifference < 0) {
correctionFactor = heightDifference;
}
final int availableHeight = rootView.getHeight() - viewInset - (!Utils.isFullScreen(context) ? rect.top : 0);
final int keyboardHeight = Utils.shouldOverrideRegularCondition(context, editText) ?
Utils.getScreenHeight(context) - editText.getBottom() - (!Utils.isFullScreen(context) ? rect.top : 0)
: availableHeight - (rect.bottom - rect.top);

if (isKeyboardOpen) {
isKeyboardOpen = false;
if (keyboardHeight > Utils.dpToPx(context, MIN_KEYBOARD_HEIGHT)) {
if (popupWindow.getHeight() != keyboardHeight) {
popupWindow.setHeight(keyboardHeight);
}

if (onSoftKeyboardCloseListener != null) {
onSoftKeyboardCloseListener.onKeyboardClose();
}
if (popupWindow.getWidth() != rect.right) {
popupWindow.setWidth(rect.right);
}

dismiss();
Utils.removeOnGlobalLayoutListener(context.getWindow().getDecorView(), onGlobalLayoutListener);
if (!isKeyboardOpen) {
isKeyboardOpen = true;
if (onSoftKeyboardOpenListener != null) {
onSoftKeyboardOpenListener.onKeyboardOpen(keyboardHeight);
}
}

if (isPendingOpen) {
showAtBottom();
}
} else if (isKeyboardOpen) {
isKeyboardOpen = false;
if (onSoftKeyboardCloseListener != null) {
onSoftKeyboardCloseListener.onKeyboardClose();
}
}
};
}

EmojiPopup(@NonNull final View rootView, @NonNull final EditText editText,
@Nullable final RecentEmoji recent, @Nullable final VariantEmoji variant,
Expand Down Expand Up @@ -141,7 +162,9 @@ public final class EmojiPopup {

variantPopup = new EmojiVariantPopup(this.rootView, clickListener);

final EmojiView emojiView = new EmojiView(context, clickListener, longClickListener, recentEmoji, variantEmoji, backgroundColor, iconColor, dividerColor);
final EmojiView emojiView =
new EmojiView(context, clickListener, longClickListener, recentEmoji, variantEmoji, backgroundColor, iconColor,
dividerColor);
emojiView.setOnEmojiBackspaceClickListener(new OnEmojiBackspaceClickListener() {
@Override public void onEmojiBackspaceClick(final View v) {
backspace(editText);
Expand All @@ -154,7 +177,8 @@ public final class EmojiPopup {

popupWindow.setContentView(emojiView);
popupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
popupWindow.setBackgroundDrawable(new BitmapDrawable(context.getResources(), (Bitmap) null)); // To avoid borders and overdraw.
popupWindow.setBackgroundDrawable(
new BitmapDrawable(context.getResources(), (Bitmap) null)); // To avoid borders and overdraw.
popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override public void onDismiss() {
if (onEmojiPopupDismissListener != null) {
Expand All @@ -164,44 +188,30 @@ public final class EmojiPopup {
});

popupWindow.setFocusable(true);
viewInset = getViewInset();
rootView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
}

public void toggle() {
if (!popupWindow.isShowing()) {
// Remove any previous listeners to avoid duplicates.
Utils.removeOnGlobalLayoutListener(context.getWindow().getDecorView(), onGlobalLayoutListener);
context.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);

if (isKeyboardOpen) {
// If the keyboard is visible, simply show the emoji popup.
editText.postDelayed(new Runnable() {
@Override public void run() {
showAtBottom();
}
}, 50);
} else if (editText != null) {
final View view = editText;

// Open the text keyboard first and immediately after that show the emoji popup.
view.setFocusableInTouchMode(true);
view.setFocusable(true);
view.requestFocus();

final InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
showAtBottomPending();
} else {
throw new IllegalArgumentException("The provided EditText is null.");
}
final View view = editText;

// Open the text keyboard first and immediately after that show the emoji popup.
view.setFocusableInTouchMode(true);
view.setFocusable(true);
view.requestFocus();

showAtBottomPending();

final InputMethodManager inputMethodManager =
(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.showSoftInput(view, shouldOverrideRegularCondition(context, editText) ?
InputMethodManager.SHOW_FORCED : InputMethodManager.SHOW_IMPLICIT);
} else {
dismiss();
}

// Manually dispatch the event. In some cases this does not work out of the box reliably.
context.getWindow().getDecorView().getViewTreeObserver().dispatchOnGlobalLayout();
}


public boolean isShowing() {
return popupWindow.isShowing();
}
Expand All @@ -214,26 +224,17 @@ public void dismiss() {
}

void showAtBottom() {
final Point desiredLocation = new Point(0, Utils.getScreenHeight(context) - popupWindow.getHeight() + correctionFactor);

popupWindow.showAtLocation(rootView, Gravity.NO_GRAVITY, desiredLocation.x, desiredLocation.y);
isPendingOpen = false;
popupWindow.showAtLocation(rootView, Gravity.BOTTOM, 0, 0);

if (onEmojiPopupShownListener != null) {
if (shouldOverrideRegularCondition(context, editText)) {
final InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.showSoftInput(editText, 0);
}
onEmojiPopupShownListener.onEmojiPopupShown();
}
}

private void showAtBottomPending() {
if (isKeyboardOpen) {
editText.postDelayed(new Runnable() {
@Override public void run() {
showAtBottom();
}
}, 50);
showAtBottom();
} else {
isPendingOpen = true;
}
Expand Down
5 changes: 5 additions & 0 deletions emoji/src/main/java/com/vanniktech/emoji/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.PopupWindow;
Expand Down Expand Up @@ -106,6 +107,10 @@ static Activity asActivity(@NonNull final Context context) {
throw new IllegalArgumentException("The passed Context is not an Activity.");
}

static boolean isFullScreen(Activity activity) {
return (activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) > 0;
}

static void fixPopupLocation(@NonNull final PopupWindow popupWindow, @NonNull final Point desiredLocation) {
popupWindow.getContentView().post(new Runnable() {
@Override public void run() {
Expand Down

0 comments on commit 6ad9ce9

Please sign in to comment.