From bc787f20e33b2f1023f0798abfcbd3665c247e1c Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Tue, 7 Jul 2015 14:25:41 -0700 Subject: [PATCH] Resolve emoji keyboard layout issues 1) orientation changes are now properly handled 2) emoji panel will not overrun the actionbar and composition area in space-contentious situations (quick reply popup) Closes #3553 fixes #3501 fixes #3485 fixes #3199 // FREEBIE --- res/values/dimens.xml | 1 + .../securesms/ConversationActivity.java | 39 ++++++--- .../securesms/ConversationPopupActivity.java | 5 ++ .../components/KeyboardAwareLinearLayout.java | 84 ++++++++----------- .../components/emoji/EmojiDrawer.java | 1 + .../components/emoji/EmojiPageView.java | 51 +++++------ .../components/emoji/EmojiPopup.java | 26 ++++-- .../securesms/components/emoji/EmojiView.java | 13 ++- .../securesms/util/ServiceUtil.java | 16 ++++ 9 files changed, 129 insertions(+), 107 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/ServiceUtil.java diff --git a/res/values/dimens.xml b/res/values/dimens.xml index b05db94018..d333d0e4eb 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -3,6 +3,7 @@ 32sp 50dp 200dp + 170dp 5dp 1.5dp 5dp diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index e585fa15d4..a2f81bf938 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PorterDuff; @@ -113,6 +114,7 @@ import org.thoughtcrime.securesms.util.DirectoryHelper; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.GroupUtil; +import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; @@ -264,8 +266,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - Log.w(TAG, String.format("onConfigurationChanged(%d -> %d)", getResources().getConfiguration().orientation, newConfig.orientation)); quickAttachmentDrawer.onConfigurationChanged(); + hideEmojiPopup(false); } @Override @@ -274,6 +276,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (recipients != null) recipients.removeListener(this); if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver); if (groupUpdateReceiver != null) unregisterReceiver(groupUpdateReceiver); + hideEmojiPopup(false); super.onDestroy(); } @@ -858,13 +861,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private EmojiPopup getEmojiPopup() { if (!emojiPopup.isPresent()) { - EmojiPopup emojiPopup = new EmojiPopup(getWindow().getDecorView()); + EmojiPopup emojiPopup = new EmojiPopup(container); emojiPopup.setEmojiEventListener(new EmojiEventListener() { @Override public void onKeyEvent(KeyEvent keyEvent) { composeText.dispatchKeyEvent(keyEvent); } @Override public void onEmojiSelected(String emoji) { + Log.w(TAG, "onEmojiSelected()"); composeText.insertEmoji(emoji); } }); @@ -874,17 +878,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void showEmojiPopup() { - int height = Math.max(getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height), - container.getKeyboardHeight()); - container.padForCustomKeyboard(height); - getEmojiPopup().show(height); + getEmojiPopup().show(); emojiToggle.setToIme(); } - private void hideEmojiPopup(boolean expectingKeyboard) { + protected void hideEmojiPopup(boolean expectingKeyboard) { if (isEmojiDrawerOpen()) { getEmojiPopup().dismiss(); - if (!expectingKeyboard) { + if (!expectingKeyboard || container.isLandscape()) { container.unpadForCustomKeyboard(); } } @@ -1330,22 +1331,34 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } } + private void openKeyboardForComposition() { + composeText.post(new Runnable() { + @Override public void run() { + composeText.requestFocus(); + ServiceUtil.getInputMethodManager(ConversationActivity.this).showSoftInput(composeText, 0); + } + }); + } + + private void hideKeyboard() { + ServiceUtil.getInputMethodManager(this).hideSoftInputFromWindow(composeText.getWindowToken(), 0); + } + private class EmojiToggleListener implements OnClickListener { @Override public void onClick(View v) { - InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); - + Log.w(TAG, "EmojiToggleListener onClick()"); if (isEmojiDrawerOpen()) { hideEmojiPopup(true); - input.showSoftInput(composeText, 0); + openKeyboardForComposition(); } else { container.postOnKeyboardClose(new Runnable() { @Override public void run() { showEmojiPopup(); } }); - input.hideSoftInputFromWindow(composeText.getWindowToken(), 0); - quickAttachmentDrawer.setDrawerStateAndAnimate(DrawerState.COLLAPSED); + quickAttachmentDrawer.close(); + hideKeyboard(); } } } diff --git a/src/org/thoughtcrime/securesms/ConversationPopupActivity.java b/src/org/thoughtcrime/securesms/ConversationPopupActivity.java index 4c16eb71d9..96d79d7862 100644 --- a/src/org/thoughtcrime/securesms/ConversationPopupActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationPopupActivity.java @@ -112,6 +112,11 @@ public class ConversationPopupActivity extends ConversationActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(false); } + @Override + protected void hideEmojiPopup(boolean expectingKeyboard) { + super.hideEmojiPopup(false); + } + @Override protected void sendComplete(long threadId) { super.sendComplete(threadId); diff --git a/src/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java b/src/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java index 5590ac4ba9..264a84c32e 100644 --- a/src/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java +++ b/src/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java @@ -16,7 +16,6 @@ */ package org.thoughtcrime.securesms.components; -import android.app.Activity; import android.content.Context; import android.graphics.Rect; import android.os.Build; @@ -27,9 +26,9 @@ import android.util.AttributeSet; import android.util.Log; import android.view.Surface; import android.view.View; -import android.view.WindowManager; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.ServiceUtil; import java.lang.reflect.Field; import java.util.HashSet; @@ -46,9 +45,10 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat { private final Rect newRect = new Rect(); private final Set hiddenListeners = new HashSet<>(); private final Set shownListeners = new HashSet<>(); - private final int minKeyboardSize; + private final int minKeyboardSize; - private boolean keyboardOpen; + private boolean keyboardOpen = false; + private int rotation = -1; public KeyboardAwareLinearLayout(Context context) { this(context, null); @@ -64,16 +64,29 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat { } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + updateRotation(); + updateKeyboardState(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } - int res = getResources().getIdentifier("status_bar_height", "dimen", "android"); + private void updateRotation() { + int oldRotation = rotation; + rotation = getDeviceRotation(); + if (oldRotation != rotation) { + onKeyboardClose(); + oldRect.setEmpty(); + } + } + + private void updateKeyboardState() { + int res = getResources().getIdentifier("status_bar_height", "dimen", "android"); int statusBarHeight = res > 0 ? getResources().getDimensionPixelSize(res) : 0; final int availableHeight = this.getRootView().getHeight() - statusBarHeight - getViewInset(); getWindowVisibleDisplayFrame(newRect); final int oldKeyboardHeight = availableHeight - (oldRect.bottom - oldRect.top); - final int keyboardHeight = availableHeight - (newRect.bottom - newRect.top); + final int keyboardHeight = availableHeight - (newRect.bottom - newRect.top); if (keyboardHeight - oldKeyboardHeight > minKeyboardSize && !keyboardOpen) { onKeyboardOpen(keyboardHeight); @@ -84,7 +97,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat { oldRect.set(newRect); } - public void padForCustomKeyboard(int height) { + public void padForCustomKeyboard(final int height) { setPadding(0, 0, 0, height); } @@ -117,22 +130,9 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat { protected void onKeyboardOpen(int keyboardHeight) { keyboardOpen = true; - Log.w(TAG, "onKeyboardOpen(" + keyboardHeight + ")"); - WindowManager wm = (WindowManager) getContext().getSystemService(Activity.WINDOW_SERVICE); - if (wm == null || wm.getDefaultDisplay() == null) { - return; - } - int rotation = wm.getDefaultDisplay().getRotation(); - - switch (rotation) { - case Surface.ROTATION_270: - case Surface.ROTATION_90: - setKeyboardLandscapeHeight(keyboardHeight); - break; - case Surface.ROTATION_0: - case Surface.ROTATION_180: - setKeyboardPortraitHeight(keyboardHeight); + if (!isLandscape()) { + setKeyboardPortraitHeight(keyboardHeight); } notifyShownListeners(); unpadForCustomKeyboard(); @@ -140,7 +140,6 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat { protected void onKeyboardClose() { keyboardOpen = false; - Log.w(TAG, "onKeyboardClose()"); notifyHiddenListeners(); } @@ -149,39 +148,26 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat { } public int getKeyboardHeight() { - WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); - if (wm == null || wm.getDefaultDisplay() == null) { - throw new AssertionError("WindowManager was null or there is no default display"); - } - - int rotation = wm.getDefaultDisplay().getRotation(); + return isLandscape() ? getKeyboardLandscapeHeight() : getKeyboardPortraitHeight(); + } - switch (rotation) { - case Surface.ROTATION_270: - case Surface.ROTATION_90: - return getKeyboardLandscapeHeight(); - case Surface.ROTATION_0: - case Surface.ROTATION_180: - default: - return getKeyboardPortraitHeight(); - } + public boolean isLandscape() { + int rotation = getDeviceRotation(); + return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270; + } + private int getDeviceRotation() { + return ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRotation(); } private int getKeyboardLandscapeHeight() { - return PreferenceManager.getDefaultSharedPreferences(getContext()) - .getInt("keyboard_height_landscape", - getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height)); + return Math.max(getHeight(), getRootView().getHeight()) / 2; } private int getKeyboardPortraitHeight() { - return PreferenceManager.getDefaultSharedPreferences(getContext()) - .getInt("keyboard_height_portrait", - getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height)); - } - - private void setKeyboardLandscapeHeight(int height) { - PreferenceManager.getDefaultSharedPreferences(getContext()) - .edit().putInt("keyboard_height_landscape", height).apply(); + int keyboardHeight = PreferenceManager.getDefaultSharedPreferences(getContext()) + .getInt("keyboard_height_portrait", + getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height)); + return Math.min(keyboardHeight, getRootView().getHeight() - getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_top_margin)); } private void setKeyboardPortraitHeight(int height) { diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java index 26695412d4..1e6eb8f16b 100644 --- a/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java @@ -80,6 +80,7 @@ public class EmojiDrawer extends LinearLayoutCompat { models, new EmojiSelectionListener() { @Override public void onEmojiSelected(String emoji) { + Log.w("EmojiDrawer", "onEmojiSelected()"); recentModel.onCodePointSelected(emoji); if (listener != null) listener.onEmojiSelected(emoji); } diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java index 1147c63a77..7046d1ce08 100644 --- a/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java @@ -1,16 +1,12 @@ package org.thoughtcrime.securesms.components.emoji; -import android.annotation.TargetApi; import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.Build.VERSION_CODES; import android.util.AttributeSet; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.GridView; @@ -25,24 +21,27 @@ public class EmojiPageView extends FrameLayout { private GridView grid; public EmojiPageView(Context context) { - super(context); - init(); + this(context, null); } public EmojiPageView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); + this(context, attrs, 0); } public EmojiPageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(); - } - - @TargetApi(VERSION_CODES.LOLLIPOP) - public EmojiPageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(); + final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true); + grid = (GridView) view.findViewById(R.id.emoji); + grid.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size) + 2 * getResources().getDimensionPixelSize(R.dimen.emoji_drawer_item_padding)); + grid.setOnTouchListener(new OnTouchListener() { + @Override public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + EmojiView emojiView = (EmojiView)grid.getChildAt(grid.pointToPosition((int)event.getX(), (int)event.getY())); + if (listener != null && emojiView != null) listener.onEmojiSelected(emojiView.getEmoji()); + } + return false; + } + }); } public void onSelected() { @@ -51,17 +50,6 @@ public class EmojiPageView extends FrameLayout { } } - private void init() { - final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true); - grid = (GridView) view.findViewById(R.id.emoji); - grid.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size) + 2 * getResources().getDimensionPixelSize(R.dimen.emoji_drawer_item_padding)); - grid.setOnItemClickListener(new OnItemClickListener() { - @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - if (listener != null) listener.onEmojiSelected((String)view.getTag()); - } - }); - } - public void setModel(EmojiPageModel model) { this.model = model; grid.setAdapter(new EmojiGridAdapter(getContext(), model)); @@ -73,9 +61,9 @@ public class EmojiPageView extends FrameLayout { private static class EmojiGridAdapter extends BaseAdapter { - protected final Context context; - private final int emojiSize; - private final EmojiPageModel model; + protected final Context context; + private final int emojiSize; + private final EmojiPageModel model; public EmojiGridAdapter(Context context, EmojiPageModel model) { this.context = context; @@ -104,14 +92,13 @@ public class EmojiPageView extends FrameLayout { if (convertView != null && convertView instanceof EmojiView) { view = (EmojiView)convertView; } else { - EmojiView emojiView = new EmojiView(context); + final EmojiView emojiView = new EmojiView(context); emojiView.setPadding(pad, pad, pad, pad); emojiView.setLayoutParams(new AbsListView.LayoutParams(emojiSize + 2 * pad, emojiSize + 2 * pad)); view = emojiView; } view.setEmoji(model.getEmoji()[position]); - view.setTag(model.getEmoji()[position]); return view; } } diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiPopup.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiPopup.java index 0423844df1..b9ed9c6eef 100644 --- a/src/org/thoughtcrime/securesms/components/emoji/EmojiPopup.java +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiPopup.java @@ -1,31 +1,45 @@ package org.thoughtcrime.securesms.components.emoji; -import android.app.Activity; -import android.content.Context; import android.util.Log; import android.view.Gravity; import android.view.View; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.widget.PopupWindow; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout; import org.thoughtcrime.securesms.components.emoji.EmojiDrawer.EmojiEventListener; public class EmojiPopup extends PopupWindow { - private View parent; + private static final String TAG = EmojiPopup.class.getSimpleName(); + private KeyboardAwareLinearLayout parent; - public EmojiPopup(View parent) { + public EmojiPopup(KeyboardAwareLinearLayout parent) { super(new EmojiDrawer(parent.getContext()), parent.getWidth(), parent.getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height)); this.parent = parent; + Log.w("EmojiPopup", "popup initialized with width " + parent.getWidth()); } public void setEmojiEventListener(EmojiEventListener listener) { ((EmojiDrawer)getContentView()).setEmojiEventListener(listener); } - public void show(int height) { - setHeight(height); + public void show() { + setHeight(parent.getKeyboardHeight()); + setWidth(parent.getWidth()); + parent.padForCustomKeyboard(getHeight()); + Log.w(TAG, String.format("show(%d, %d)", getWidth(), getHeight())); showAtLocation(parent, Gravity.BOTTOM | Gravity.LEFT, 0, 0); } + + @Override + public void dismiss() { + super.dismiss(); + } + + public void update() { + update(parent, 0, 0, parent.getWidth(), -1); + } } diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java index 7d35dee120..b464cc52ec 100644 --- a/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java @@ -23,22 +23,17 @@ public class EmojiView extends View implements Drawable.Callback { private final Rect textBounds = new Rect(); public EmojiView(Context context) { - super(context); + this(context, null); } public EmojiView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } public EmojiView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - @TargetApi(VERSION_CODES.LOLLIPOP) - public EmojiView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - public void setEmoji(String emoji) { this.emoji = emoji; this.drawable = EmojiProvider.getInstance(getContext()) @@ -47,6 +42,10 @@ public class EmojiView extends View implements Drawable.Callback { postInvalidate(); } + public String getEmoji() { + return emoji; + } + @Override protected void onDraw(Canvas canvas) { if (drawable != null) { drawable.setBounds(getPaddingLeft(), diff --git a/src/org/thoughtcrime/securesms/util/ServiceUtil.java b/src/org/thoughtcrime/securesms/util/ServiceUtil.java new file mode 100644 index 0000000000..049433d437 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/ServiceUtil.java @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.util; + +import android.app.Activity; +import android.content.Context; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; + +public class ServiceUtil { + public static InputMethodManager getInputMethodManager(Context context) { + return (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + } + + public static WindowManager getWindowManager(Context context) { + return (WindowManager) context.getSystemService(Activity.WINDOW_SERVICE); + } +}