Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): onKeyboardWillShow only triggers on focused component #3547

Merged
merged 2 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,48 @@

package com.tencent.mtt.hippy.views.textinput;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.BlendMode;
import android.graphics.BlendModeColorFilter;

import android.graphics.Paint;
import android.graphics.Typeface;
import androidx.annotation.Nullable;
import com.tencent.mtt.hippy.common.HippyMap;
import com.tencent.mtt.hippy.uimanager.HippyViewBase;
import com.tencent.mtt.hippy.uimanager.NativeGestureDispatcher;
import com.tencent.mtt.hippy.uimanager.RenderManager;
import com.tencent.renderer.NativeRender;
import com.tencent.renderer.NativeRendererManager;
import com.tencent.renderer.component.text.FontAdapter;
import com.tencent.renderer.component.text.TypeFaceUtil;
import com.tencent.renderer.node.RenderNode;
import com.tencent.mtt.hippy.utils.ContextHolder;
import com.tencent.mtt.hippy.utils.LogUtils;
import com.tencent.mtt.hippy.utils.PixelUtil;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;

import com.tencent.mtt.hippy.common.HippyMap;
import com.tencent.mtt.hippy.uimanager.HippyViewBase;
import com.tencent.mtt.hippy.uimanager.NativeGestureDispatcher;
import com.tencent.mtt.hippy.uimanager.RenderManager;
import com.tencent.mtt.hippy.utils.LogUtils;
import com.tencent.mtt.hippy.utils.PixelUtil;
import com.tencent.renderer.NativeRender;
import com.tencent.renderer.NativeRendererManager;
import com.tencent.renderer.component.Component;
import com.tencent.renderer.component.FlatViewGroup;
import com.tencent.renderer.component.text.FontAdapter;
import com.tencent.renderer.component.text.TypeFaceUtil;
import com.tencent.renderer.node.RenderNode;
import com.tencent.renderer.utils.EventUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
Expand All @@ -71,17 +67,23 @@ public class HippyTextInput extends AppCompatEditText implements HippyViewBase,
TextView.OnEditorActionListener, View.OnFocusChangeListener {

private static final String TAG = "HippyTextInput";
static final int EVENT_FOCUS = 1;
static final int EVENT_BLUR = 1 << 1;
static final int EVENT_KEYBOARD_SHOW = 1 << 2;
static final int EVENT_KEYBOARD_HIDE = 1 << 3;
static final int MASK_FOCUS = EVENT_FOCUS | EVENT_BLUR | EVENT_KEYBOARD_SHOW;
static final int MASK_KEYBOARD = EVENT_KEYBOARD_SHOW | EVENT_KEYBOARD_HIDE;
boolean mHasAddWatcher = false;
private String mPreviousText = "";
TextWatcher mTextWatcher = null;
boolean mHasSetOnSelectListener = false;

private final int mDefaultGravityHorizontal;
private final int mDefaultGravityVertical;
//输入法键盘的相关方法
private final Rect mRect = new Rect(); //获取当前RootView的大小位置信息
private int mLastRootViewVisibleHeight = -1; //当前RootView的上一次大小
private final ViewTreeObserver.OnGlobalLayoutListener mKeyboardEventObserver = this::checkSendKeyboardEvent;
private boolean mIsKeyBoardShow = false; //键盘是否在显示
private boolean mIsKeyBoardShowBySelf = false;
private int mListenerFlag = 0;
private ReactContentSizeWatcher mReactContentSizeWatcher = null;
private boolean mItalic = false;
private int mFontWeight = TypeFaceUtil.WEIGHT_NORMAL;
Expand Down Expand Up @@ -148,15 +150,15 @@ public void onEditorAction(int actionCode) {
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (getRootView() != null) {
getRootView().getViewTreeObserver().addOnGlobalLayoutListener(globaListener);
getRootView().getViewTreeObserver().addOnGlobalLayoutListener(mKeyboardEventObserver);
}
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (getRootView() != null) {
getRootView().getViewTreeObserver().removeOnGlobalLayoutListener(globaListener);
getRootView().getViewTreeObserver().removeOnGlobalLayoutListener(mKeyboardEventObserver);
}
}

Expand Down Expand Up @@ -274,97 +276,142 @@ public void hideInputMethod() {

}

//成功的話返回手機屏幕的高度,失敗返回-1
private int getScreenHeight() {
private static boolean sVisibleRectReflectionFetched = false;
private static Method sGetViewRootImplMethod;
private static Field sVisibleInsetsField;
private static Field sAttachInfoField;

private static Field sViewAttachInfoField;
private static Field sStableInsets;
private static Field sContentInsets;

@SuppressLint({ "PrivateApi", "SoonBlockedPrivateApi" }) // PrivateApi is only accessed below SDK 30
private static void loadReflectionField() {
try {
Context context = ContextHolder.getAppContext();
android.view.WindowManager manager = (android.view.WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();

if (display != null) {
int width = manager.getDefaultDisplay().getWidth();
int height = manager.getDefaultDisplay().getHeight();
return Math.max(width, height);
sGetViewRootImplMethod = View.class.getDeclaredMethod("getViewRootImpl");
Class<?> sAttachInfoClass = Class.forName("android.view.View$AttachInfo");
sVisibleInsetsField = sAttachInfoClass.getDeclaredField("mVisibleInsets");
Class<?> viewRootImplClass = Class.forName("android.view.ViewRootImpl");
sAttachInfoField = viewRootImplClass.getDeclaredField("mAttachInfo");
sVisibleInsetsField.setAccessible(true);
sAttachInfoField.setAccessible(true);
} catch (ReflectiveOperationException e) {
LogUtils.e(TAG, "Failed to get visible insets. (Reflection error). " + e.getMessage(), e);
}

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
try {
sViewAttachInfoField = View.class.getDeclaredField("mAttachInfo");
sViewAttachInfoField.setAccessible(true);
Class<?> sAttachInfoClass = Class.forName("android.view.View$AttachInfo");
sStableInsets = sAttachInfoClass.getDeclaredField("mStableInsets");
sStableInsets.setAccessible(true);
sContentInsets = sAttachInfoClass.getDeclaredField("mContentInsets");
sContentInsets.setAccessible(true);
} catch (ReflectiveOperationException e) {
LogUtils.w(TAG, "Failed to get visible insets from AttachInfo " + e.getMessage());
}

} catch (SecurityException e) {
LogUtils.d(TAG, "getScreenHeight: " + e.getMessage());
}
return -1;

sVisibleRectReflectionFetched = true;
}

/**
* 返回RootView的高度,要注意即使全屏,他應該也少了一個狀態欄的高度
* Get the software keyboard height. This code is migrated from androidx, in case we are running in a lower version
* androidx library, feel free to use androidx directly in the future.
*
* @see androidx.core.view.ViewCompat#getRootWindowInsets(View)
* @see androidx.core.view.WindowInsetsCompat#isVisible(int)
* @see androidx.core.view.WindowInsetsCompat#getInsets(int)
* @see androidx.core.view.WindowInsetsCompat.Type#ime()
*/
private int getRootViewHeight() {
int height = -1;
View rootView = getRootView();
if (rootView == null) {
return height;
private int getImeHeight(View view) {
final WindowInsets insets = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? getRootWindowInsets() : null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return insets == null ? 0 : insets.getInsets(WindowInsets.Type.ime()).bottom;
}

final View rootView = getRootView();
int systemWindowBottom = 0;
int rootStableBottom = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (insets == null) {
return 0;
}
systemWindowBottom = insets.getSystemWindowInsetBottom();
rootStableBottom = insets.getStableInsetBottom();
} else {
if (!rootView.isAttachedToWindow()) {
return 0;
}
if (!sVisibleRectReflectionFetched) {
loadReflectionField();
}
if (sViewAttachInfoField != null && sStableInsets != null && sContentInsets != null) {
try {
Object attachInfo = sViewAttachInfoField.get(rootView);
if (attachInfo != null) {
Rect stableInsets = (Rect) sStableInsets.get(attachInfo);
Rect visibleInsets = (Rect) sContentInsets.get(attachInfo);
if (stableInsets != null && visibleInsets != null) {
systemWindowBottom = visibleInsets.bottom;
rootStableBottom = stableInsets.bottom;
}
}
} catch (IllegalAccessException e) {
LogUtils.w(TAG, "Failed to get insets from AttachInfo. " + e.getMessage());
}
}
}
// 问题ID: 106874510 某些奇葩手机ROM调用此方法会报错,做下捕获吧
try {
rootView.getWindowVisibleDisplayFrame(mRect);
} catch (Throwable e) {
LogUtils.d("InputMethodStatusMonitor:", "getWindowVisibleDisplayFrame failed !" + e);
e.printStackTrace();
if (systemWindowBottom > rootStableBottom) {
return systemWindowBottom;
}

int visibleHeight = mRect.bottom - mRect.top;
if (visibleHeight < 0) {
return -1;
if (!sVisibleRectReflectionFetched) {
loadReflectionField();
}
return visibleHeight;
}

//监听RootView布局变化的listener
final ViewTreeObserver.OnGlobalLayoutListener globaListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int rootViewVisibleHeight = getRootViewHeight(); //RootView的高度
int screenHeight = getScreenHeight(); //屏幕高度
if (rootViewVisibleHeight == -1 || screenHeight == -1) //如果有失败直接返回 //TODO...仔细检查下这里的逻辑
{
mLastRootViewVisibleHeight = rootViewVisibleHeight;
return;
}
if (mLastRootViewVisibleHeight == -1) // 首次
{
//假设输入键盘的高度位屏幕高度20%
if (rootViewVisibleHeight > screenHeight * 0.8f) {

mIsKeyBoardShow = false; //键盘没有显示
} else {
if (!mIsKeyBoardShow) {
Map<String, Object> keyboardHeightMap = new HashMap<>();
int height = Math.abs(screenHeight - rootViewVisibleHeight);
keyboardHeightMap.put("keyboardHeight", Math.round(PixelUtil.px2dp(height)));
EventUtils.sendComponentEvent(HippyTextInput.this, "keyboardWillShow", keyboardHeightMap);
}
mIsKeyBoardShow = true; //键盘显示 ----s首次需要通知
}
} else {
//假设输入键盘的高度位屏幕高度20%
if (rootViewVisibleHeight > screenHeight * 0.8f) {
if (mIsKeyBoardShow) {
EventUtils.sendComponentEvent(HippyTextInput.this, "keyboardWillHide", null);
}
mIsKeyBoardShow = false; //键盘没有显示
} else {
if (!mIsKeyBoardShow) {
HippyMap hippyMap = new HippyMap();
hippyMap.pushInt("keyboardHeight",
Math.abs(mLastRootViewVisibleHeight - rootViewVisibleHeight));
EventUtils.sendComponentEvent(HippyTextInput.this, "keyboardWillShow", hippyMap);
if (sGetViewRootImplMethod != null && sAttachInfoField != null && sVisibleInsetsField != null) {
try {
Object viewRootImpl = sGetViewRootImplMethod.invoke(rootView);
if (viewRootImpl != null) {
Object mAttachInfo = sAttachInfoField.get(viewRootImpl);
Rect visibleRect = (Rect) sVisibleInsetsField.get(mAttachInfo);
int visibleRectBottom = visibleRect != null ? visibleRect.bottom : 0;
if (visibleRectBottom > rootStableBottom) {
return visibleRectBottom;
}
mIsKeyBoardShow = true; //键盘显示 ----s首次需要通知
}
} catch (ReflectiveOperationException e) {
LogUtils.e(TAG,
"Failed to get visible insets. (Reflection error). " + e.getMessage(), e);
}
}
return 0;
}

mLastRootViewVisibleHeight = rootViewVisibleHeight;
private void checkSendKeyboardEvent() {
if ((mListenerFlag & MASK_KEYBOARD) == 0) {
return;
}
};
final int imeHeight = getImeHeight(this);
if (imeHeight > 0) {
mIsKeyBoardShow = true;
boolean bySelf = hasFocus();
if (bySelf != mIsKeyBoardShowBySelf) {
mIsKeyBoardShowBySelf = bySelf;
if (bySelf && (mListenerFlag & EVENT_KEYBOARD_SHOW) != 0) {
Map<String, Object> keyboardHeightMap = new HashMap<>();
keyboardHeightMap.put("keyboardHeight", Math.round(PixelUtil.px2dp(imeHeight)));
EventUtils.sendComponentEvent(HippyTextInput.this, "keyboardWillShow", keyboardHeightMap);
}
}
} else if (mIsKeyBoardShow) {
mIsKeyBoardShow = mIsKeyBoardShowBySelf = false;
if ((mListenerFlag & EVENT_KEYBOARD_HIDE) != 0) {
EventUtils.sendComponentEvent(HippyTextInput.this, "keyboardWillHide", null);
}
}
}

public void showInputMethodManager() {

Expand Down Expand Up @@ -510,23 +557,29 @@ public Map<String, Object> jsIsFocused() {
return result;
}

public void setBlurOrOnFocus(boolean blur) {
if (blur) {
setOnFocusChangeListener(this);
public void setEventListener(boolean enable, int flag) {
final boolean oldHasFocusListener = (mListenerFlag & MASK_FOCUS) != 0;
if (enable) {
mListenerFlag |= flag;
} else {
setOnFocusChangeListener(null);
mListenerFlag &= ~flag;
}
boolean newHasFocusListener = (mListenerFlag & MASK_FOCUS) != 0;
if (oldHasFocusListener != newHasFocusListener) {
setOnFocusChangeListener(newHasFocusListener ? this : null);
}
}

@Override
public void onFocusChange(View v, boolean hasFocus) {

HippyMap hippyMap = new HippyMap();
hippyMap.pushString("text", getText().toString());
if (hasFocus) {
EventUtils.sendComponentEvent(v, "focus", hippyMap);
checkSendKeyboardEvent();
} else {
EventUtils.sendComponentEvent(v, "blur", hippyMap);
mIsKeyBoardShowBySelf = false;
}
}

Expand Down Expand Up @@ -632,7 +685,7 @@ public void setCursorColor(int color) {
}
}
}

public void refreshSoftInput() {
InputMethodManager imm = getInputMethodManager();
if (imm.isActive(this)) { // refresh the showing soft keyboard
Expand Down
Loading
Loading