平台
RK3288 + Android 8.1
显示: 1920x1080 @ 160 dpi
概述
碰到一个问题: 弹出的输入法会覆盖文本输入框。
原因:输入框使用了setTranslationY() 位置偏移后, 输入法无法正确获取焦点的位置。
分析
先上图: 初始布局
调用etTranslationY(700);
弹出输入法
最后一张图中, 输入框大概在红框的位置, 也是本文所描述的问题: 输入法遮挡了输入框控件
- 布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayout android:id="@+id/llEdit"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:id="@+id/tv"android:textSize="28sp"android:gravity="center"android:text="--------------FOOTER---------------------"android:layout_width="match_parent"android:layout_height="wrap_content"/><EditText android:id="@+id/et"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="36sp"/><TextViewandroid:id="@+id/tv2"android:textSize="28sp"android:gravity="center"android:text="--------------HEADER---------------------"android:layout_width="match_parent"android:layout_height="wrap_content"/></LinearLayout><Button android:id="@+id/btTy"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:text="TranslationY"/>
</RelativeLayout>
- java
package com.android.apitester.test;import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;import com.android.apitester.R;public class EditTextTranslationTest extends Activity {EditText et;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.test_edittext_translation);et = (EditText) findViewById(R.id.et);findViewById(R.id.btTy).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {et.setTranslationY(et.getTranslationY() != 0 ? 0 : 700);}});}
}
稍微改下代码,把输入框放到界面底部
输入法正常弹出,并把整体UI往上顶。
后续做了一些数据, getTranslationY不同的大小以作比对
位移大小 | 展示效果 | 备注 |
---|---|---|
-300 | 被覆盖 | - |
-70 | 被覆盖 | - |
-69 | 第一次后正常 | 第一次被覆盖 |
-50 | 第一次后正常 | 第一次被覆盖 |
>0 | 被覆盖 | - |
70 是控件的高度!
输入法是怎么把布局顶上去的? 答案在ViewRootImpl中。
frameworks/base/core/java/android/view/ViewRootImpl.java
public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {String innerPrefix = prefix + " ";//..... 省略 .....writer.print(innerPrefix); writer.print("getCurrY=");writer.print(mScroller != null ? mScroller.getCurrY():0);writer.print("mScrollY=");writer.print(mScrollY);writer.print("mCurScrollY=");writer.print(mCurScrollY);}
dumpsys activity name
//未打开输入法
getCurrY=0,mScrollY=0,mCurScrollY=0//打开输入法
getCurrY=372,mScrollY=372,mCurScrollY=372
准确地说,是滚上去的
boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) {final Rect ci = getWindowInsets(false).getSystemWindowInsetsAsRect();final Rect vi = mAttachInfo.mVisibleInsets;int scrollY = 0;boolean handled = false;if (vi.left > ci.left || vi.top > ci.top|| vi.right > ci.right || vi.bottom > ci.bottom) {// We'll assume that we aren't going to change the scroll// offset, since we want to avoid that unless it is actually// going to make the focus visible... otherwise we scroll// all over the place.scrollY = mScrollY;// We can be called for two different situations: during a draw,// to update the scroll position if the focus has changed (in which// case 'rectangle' is null), or in response to a// requestChildRectangleOnScreen() call (in which case 'rectangle'// is non-null and we just want to scroll to whatever that// rectangle is).final View focus = mView.findFocus();if (focus == null) {return false;}View lastScrolledFocus = (mLastScrolledFocus != null) ? mLastScrolledFocus.get() : null;if (focus != lastScrolledFocus) {// If the focus has changed, then ignore any requests to scroll// to a rectangle; first we want to make sure the entire focus// view is visible.rectangle = null;}if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Eval scroll: focus=" + focus+ " rectangle=" + rectangle + " ci=" + ci+ " vi=" + vi);if (focus == lastScrolledFocus && !mScrollMayChange && rectangle == null) {// Optimization: if the focus hasn't changed since last// time, and no layout has happened, then just leave things// as they are.if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Keeping scroll y="+ mScrollY + " vi=" + vi.toShortString());} else {// We need to determine if the currently focused view is// within the visible part of the window and, if not, apply// a pan so it can be seen.mLastScrolledFocus = new WeakReference<View>(focus);mScrollMayChange = false;if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Need to scroll?");// Try to find the rectangle from the focus view.if (focus.getGlobalVisibleRect(mVisRect, null)) {if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Root w="+ mView.getWidth() + " h=" + mView.getHeight()+ " ci=" + ci.toShortString()+ " vi=" + vi.toShortString());if (rectangle == null) {focus.getFocusedRect(mTempRect);if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Focus " + focus+ ": focusRect=" + mTempRect.toShortString());if (mView instanceof ViewGroup) {((ViewGroup) mView).offsetDescendantRectToMyCoords(focus, mTempRect);}if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Focus in window: focusRect="+ mTempRect.toShortString()+ " visRect=" + mVisRect.toShortString());} else {mTempRect.set(rectangle);if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Request scroll to rect: "+ mTempRect.toShortString()+ " visRect=" + mVisRect.toShortString());}if (mTempRect.intersect(mVisRect)) {if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Focus window visible rect: "+ mTempRect.toShortString());if (mTempRect.height() >(mView.getHeight()-vi.top-vi.bottom)) {// If the focus simply is not going to fit, then// best is probably just to leave things as-is.if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Too tall; leaving scrollY=" + scrollY);}// Next, check whether top or bottom is covered based on the non-scrolled// position, and calculate new scrollY (or set it to 0).// We can't keep using mScrollY here. For example mScrollY is non-zero// due to IME, then IME goes away. The current value of mScrollY leaves top// and bottom both visible, but we still need to scroll it back to 0.else if (mTempRect.top < vi.top) {scrollY = mTempRect.top - vi.top;if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Top covered; scrollY=" + scrollY);} else if (mTempRect.bottom > (mView.getHeight()-vi.bottom)) {scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Bottom covered; scrollY=" + scrollY);} else {scrollY = 0;}handled = true;}}}}if (scrollY != mScrollY) {if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Pan scroll changed: old="+ mScrollY + " , new=" + scrollY);if (!immediate) {if (mScroller == null) {mScroller = new Scroller(mView.getContext());}mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY);} else if (mScroller != null) {mScroller.abortAnimation();}mScrollY = scrollY;}return handled;}
frameworks/base/core/java/android/view/View.java
public void getDrawingRect(Rect outRect) {outRect.left = mScrollX;outRect.top = mScrollY;outRect.right = mScrollX + (mRight - mLeft);outRect.bottom = mScrollY + (mBottom - mTop);}public void getFocusedRect(Rect r) {getDrawingRect(r);}
获取当前聚焦的控件的位置信息与当前ViewRootImpl的可见区域进行比对计算出滚动距离。
在绘制的过程中不断更新并计算滚动位置
通过修改mScroller的动画时长,可以清晰看到滚动的过程效果
mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY);
//改为
mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY, 1000);
为什么刚好位移 setTranslationY(70) 无法滚动主窗口
if (mTempRect.intersect(mVisRect)) {if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Focus window visible rect: "+ mTempRect.toShortString());if (mTempRect.height() >(mView.getHeight()-vi.top-vi.bottom)) {// If the focus simply is not going to fit, then// best is probably just to leave things as-is.if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Too tall; leaving scrollY=" + scrollY);}// Next, check whether top or bottom is covered based on the non-scrolled// position, and calculate new scrollY (or set it to 0).// We can't keep using mScrollY here. For example mScrollY is non-zero// due to IME, then IME goes away. The current value of mScrollY leaves top// and bottom both visible, but we still need to scroll it back to 0.else if (mTempRect.top < vi.top) {scrollY = mTempRect.top - vi.top;if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Top covered; scrollY=" + scrollY);} else if (mTempRect.bottom > (mView.getHeight()-vi.bottom)) {scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);if (DEBUG_INPUT_RESIZE) Log.v(mTag,"Bottom covered; scrollY=" + scrollY);} else {scrollY = 0;}handled = true;}
获取的控件的焦点区域和可视区域不存在交集, 导致后续的mScroller部分的代码没有执行。
在TextView中重写了 getFocusedRect,返回的是 光标的坐标,在测试的DEMO中输出如下 [2,10][6,70] 的坐标。
/**
//弹
Need to scroll?
Root w=1920 h=1080 ci=[0,24][0,56] vi=[0,24][0,466]
Focus android.widget.EditText{4146188 VFED..CL. .F..H.I. 0,828-1920,898 #7f03000a app:id/et aid=1073741824}: focusRect=[2,10][6,70]
Focus in window: focusRect=[2,926][6,986] visRect=[0,916][1920,986]
Focus window visible rect: [2,926][6,986]
Bottom covered; scrollY=372
Pan scroll changed: old=0 , new=372//不弹
Eval scroll: focus=android.widget.EditText{40fcecd VFED..CL. .F..H.I. 0,828-1920,898 #7f03000a app:id/et aid=1073741824} rectangle=null ci=Rect(0, 24 -
Need to scroll?
Root w=1920 h=1080 ci=[0,24][0,56] vi=[0,24][0,466]
Focus android.widget.EditText{40fcecd VFED..CL. .F..H.I. 0,828-1920,898 #7f03000a app:id/et aid=1073741824}: focusRect=[2,10][6,70]
Focus in window: focusRect=[2,926][6,986] visRect=[0,846][1920,916]
if (mTempRect.intersect(mVisRect)) 对应的两个矩形:
- focusRect=[2,926][6,986] visRect=[0,916][1920,986] <- 弹
- focusRect=[2,926][6,986] visRect=[0,846][1920,916] <- 不弹,无交集
参考
Android软键盘弹出时把布局顶上去的解决方法
Android EditText默认不弹出输入法的实现方法
5种方法完美解决android软键盘挡住输入框方法详解
Android输入法弹出流程