diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java index e59a9b028f6..9ea03d6d988 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java @@ -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; @@ -71,6 +67,12 @@ 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; @@ -78,10 +80,10 @@ public class HippyTextInput extends AppCompatEditText implements HippyViewBase, 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; @@ -148,7 +150,7 @@ public void onEditorAction(int actionCode) { protected void onAttachedToWindow() { super.onAttachedToWindow(); if (getRootView() != null) { - getRootView().getViewTreeObserver().addOnGlobalLayoutListener(globaListener); + getRootView().getViewTreeObserver().addOnGlobalLayoutListener(mKeyboardEventObserver); } } @@ -156,7 +158,7 @@ protected void onAttachedToWindow() { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (getRootView() != null) { - getRootView().getViewTreeObserver().removeOnGlobalLayoutListener(globaListener); + getRootView().getViewTreeObserver().removeOnGlobalLayoutListener(mKeyboardEventObserver); } } @@ -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 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 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() { @@ -510,23 +557,29 @@ public Map 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; } } @@ -632,7 +685,7 @@ public void setCursorColor(int color) { } } } - + public void refreshSoftInput() { InputMethodManager imm = getInputMethodManager(); if (imm.isActive(this)) { // refresh the showing soft keyboard diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java index b669b3835e9..d76e50eebc0 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java @@ -18,7 +18,6 @@ import android.content.Context; import android.graphics.Color; -import android.graphics.PorterDuff; import android.graphics.Typeface; import android.os.Build; import android.os.Looper; @@ -38,7 +37,6 @@ import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.tencent.mtt.hippy.annotation.HippyController; import com.tencent.mtt.hippy.annotation.HippyControllerProps; import com.tencent.mtt.hippy.common.HippyArray; @@ -47,7 +45,6 @@ import com.tencent.mtt.hippy.uimanager.HippyViewController; import com.tencent.mtt.hippy.utils.LogUtils; import com.tencent.mtt.hippy.utils.PixelUtil; -import com.tencent.mtt.hippy.views.hippypager.HippyPager; import com.tencent.renderer.NativeRender; import com.tencent.renderer.NativeRenderException; import com.tencent.renderer.NativeRendererManager; @@ -348,13 +345,23 @@ public void setEndEditing(HippyTextInput hippyTextInput, boolean change) { } @HippyControllerProps(name = "focus", defaultType = HippyControllerProps.BOOLEAN) - public void setOnFocus(HippyTextInput hippyTextInput, boolean change) { - hippyTextInput.setBlurOrOnFocus(change); + public void setOnFocus(HippyTextInput hippyTextInput, boolean enable) { + hippyTextInput.setEventListener(enable, HippyTextInput.EVENT_FOCUS); } @HippyControllerProps(name = "blur", defaultType = HippyControllerProps.BOOLEAN) - public void setBlur(HippyTextInput hippyTextInput, boolean change) { - hippyTextInput.setBlurOrOnFocus(change); + public void setBlur(HippyTextInput hippyTextInput, boolean enable) { + hippyTextInput.setEventListener(enable, HippyTextInput.EVENT_BLUR); + } + + @HippyControllerProps(name = "keyboardwillshow", defaultType = HippyControllerProps.BOOLEAN) + public void setOnKeyboardWillShow(HippyTextInput hippyTextInput, boolean enable) { + hippyTextInput.setEventListener(enable, HippyTextInput.EVENT_KEYBOARD_SHOW); + } + + @HippyControllerProps(name = "keyboardwillhide", defaultType = HippyControllerProps.BOOLEAN) + public void setOnKeyboardWillHide(HippyTextInput hippyTextInput, boolean enable) { + hippyTextInput.setEventListener(enable, HippyTextInput.EVENT_KEYBOARD_HIDE); } @HippyControllerProps(name = "contentSizeChange", defaultType = HippyControllerProps.BOOLEAN)