Android输入法遮挡问题的解决思路
Github Demo:https://github.com/imflyn/InputManagerHelper
我们在做登录界面时经常会遇到上图的情况,输入法不但遮住了登录按钮而且第二个输入框也挡住了一部分。
网上很多文章把android:windowSoftInputMode="adjustPan",android:windowSoftInputMode="adjustResize"这两行代码一帖,很多同学把代码复制过来,再到manifest文件里黏贴上去,试了一遍又一遍。发现完全没有效果啊!!!
所以一定要先搞清楚windowSoftInputMode中各个属性的作用。各个属性的作用官方文档定义的很清楚,不再做具体解释,不懂的同学一定仔细看明白。
Android原生的效果无法满足,我们只能自己写实现来解决问题。
“adjustResize” 始终调整 Activity 主窗口的尺寸来为屏幕上的软键盘腾出空间。
这一句话说当android:windowSoftInputMode="adjustResize"时,window会调整尺寸,也就是说布局大小会改变并且window中的视图树会重新绘制,那么根据这个思路就诞生了两种解决方案。
1.自定义Layout,监听布局的大小改变,动态调整布局的位置
2.布局重绘时监听View树的变化,知道软键盘弹出的时机,依此来动态调整布局
</br>
有了初步的思路,那么我们再来看看该如何知道布局需要调整的距离。
从图中我们可以看到登录按钮被输入法遮挡以后大概的位置,那么为了让登录按钮能够完全显示,就需要布局整体向上位移,移动的高度就是登录按钮底部到键盘最顶端的位置。
确定了思路我们就可以具体着手实现功能了。
1.自定义Layout,监听布局的大小改变
首先需要继承RelativeLayout自定义Layout,监听onSizeChanged方法:
public class KeyboardListenLayout extends RelativeLayout {
private onSizeChangedListener mChangedListener;
public KeyboardListenLayout(Context context) {
super(context);
}
public KeyboardListenLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public KeyboardListenLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (null != mChangedListener && 0 != oldw && 0 != oldh) {
boolean showKeyboard = h < oldh;
mChangedListener.onChanged(showKeyboard, h, oldh);
}
}
public void setOnSizeChangedListener(onSizeChangedListener listener) {
mChangedListener = listener;
}
public interface onSizeChangedListener {
void onChanged(boolean showKeyboard, int h, int oldh);
}
}
在XML中引用自定义的layout,这里需要注意的是,如果你的界面中使用了Toolbar,一定不能把KeyboardListenLayout放在最外层 ,因为如果放最外层,调整布局时布局向上移动会把Toolbar挤出整个界面的可见范围内。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:elevation="1dp" />
</android.support.design.widget.AppBarLayout>
<com.flyn.inputmanagerhelper.view.KeyboardListenLayout
android:id="@+id/layout_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_top"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="60dip"
android:scaleType="fitXY"
android:src="@drawable/timg" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="40dip"
android:layout_marginEnd="36dip"
android:layout_marginStart="36dip"
android:layout_marginTop="48dip">
<EditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_marginEnd="8dip"
android:layout_marginStart="8dip"
android:background="@null"
android:imeOptions="actionNext"
android:textColor="@color/textColorPrimary"
android:textSize="14sp"
tools:text="13712345678" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="@color/dividerColor" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="40dip"
android:layout_marginEnd="36dip"
android:layout_marginStart="36dip"
android:layout_marginTop="8dip">
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_marginEnd="8dip"
android:layout_marginStart="8dip"
android:background="@null"
android:imeOptions="actionDone"
android:textColor="@color/textColorPrimary"
android:textSize="14sp"
tools:text="123456" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="@color/dividerColor" />
</RelativeLayout>
<Button
android:id="@+id/tv_login"
android:layout_width="match_parent"
android:layout_height="80dip"
android:layout_marginEnd="36dip"
android:layout_marginStart="36dip"
android:layout_marginTop="24dip"
android:background="@color/colorPrimary"
android:text="登录"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
</com.flyn.inputmanagerhelper.view.KeyboardListenLayout>
</LinearLayout>
为布局设置OnSizeChanged监听,并在layout大小发生变化时判断软键盘是弹出还是隐藏。
private void bindKeyboardListenLayout(final KeyboardListenLayout keyboardListenLayout, final View lastVisibleView) {
keyboardListenLayout.setOnSizeChangedListener(new KeyboardListenLayout.onSizeChangedListener() {
@Override
public void onChanged(final boolean showKeyboard, final int h, final int oldh) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (showKeyboard) {
//oldh代表输入法未弹出前最外层布局高度,h代表当前最外层布局高度,oldh-h可以计算出布局大小改变后输入法的高度
//整个布局的高度-输入法高度=键盘最顶端处在布局中的位置,其实直接用h计算就可以,代码这么写为了便于理解
int keyboardTop = oldh - (oldh - h);
int[] location = new int[2];
lastVisibleView.getLocationOnScreen(location);
//登录按钮顶部在屏幕中的位置+登录按钮的高度=登录按钮底部在屏幕中的位置
int lastVisibleViewBottom = location[1] + lastVisibleView.getHeight();
//登录按钮底部在布局中的位置-输入法顶部的位置=需要将布局弹起多少高度
int reSizeLayoutHeight = lastVisibleViewBottom - keyboardTop;
//因为keyboardListenLayout的高度不包括外层的statusbar的高度和actionbar的高度
//所以需要减去status bar的高度
reSizeLayoutHeight -= getStatusBarHeight();
//如果界面里有actionbar并且处于显示状态则需要少减去actionbar的高度
if (null != (((AppCompatActivity) activity).getSupportActionBar()) && (((AppCompatActivity) activity).getSupportActionBar()).isShowing()) {
reSizeLayoutHeight -= getActionBarHeight();
}
//设置登录按钮与输入法之间间距
reSizeLayoutHeight += offset;
if (reSizeLayoutHeight > 0)
keyboardListenLayout.setPadding(0, -reSizeLayoutHeight, 0, 0);
} else {
//还原布局
keyboardListenLayout.setPadding(0, 0, 0, 0);
}
}
}, 50);
}
});
}
2.监听View树的变化
添加ViewTreeObserver的监听,并通过计算键盘高度可以知道键盘是弹出还是关闭状态
private void bindLayout(final ViewGroup viewGroup, final View lastVisibleView ) {
viewGroup.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//获得屏幕高度
int screenHeight = viewGroup.getRootView().getHeight();
//r.bottom - r.top计算出输入法弹起后viewGroup的高度,屏幕高度-viewGroup高度=键盘高度
Rect r = new Rect();
viewGroup.getWindowVisibleDisplayFrame(r);
int keyboardHeight = screenHeight - (r.bottom - r.top);
//当设置layout_keyboard设置完padding以后会重绘布局再次执行onGlobalLayout()
//所以判断如果键盘高度未改变就不执行下去
if (keyboardHeight == lastKeyBoardHeight) {
return;
}
lastKeyBoardHeight = keyboardHeight;
if (keyboardHeight < 300) {
//键盘关闭后恢复布局
viewGroup.setPadding(0, 0, 0, 0);
} else {
//计算出键盘最顶端在布局中的位置
int keyboardTop = screenHeight - keyboardHeight;
int[] location = new int[2];
lastVisibleView.getLocationOnScreen(location);
//获取登录按钮底部在屏幕中的位置
int lastVisibleViewBottom = location[1] + lastVisibleView.getHeight();
//登录按钮底部在布局中的位置-输入法顶部的位置=需要将布局弹起多少高度
int reSizeLayoutHeight = lastVisibleViewBottom - keyboardTop;
//需要多弹起一个StatusBar的高度
reSizeLayoutHeight -= getStatusBarHeight();
//设置登录按钮与输入法之间间距
reSizeLayoutHeight += offset;
if (reSizeLayoutHeight > 0)
viewGroup.setPadding(0, -reSizeLayoutHeight, 0, 0);
}
}
}, 50);
}
});
}
3.在ScrollView和RecycleView中处理监听View树的变化
很多时候我们做填写表单的界面时用到ScrollView,甚至在RecycleView中也有输入框时,同样可以运用监听ViewTreeObserver的思路来实现ScrollView或RecycleView位置的调整。
private void bindViewGroup(final ViewGroup viewGroup) {
viewGroup.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//获得屏幕高度
int screenHeight = viewGroup.getRootView().getHeight();
//r.bottom - r.top计算出输入法弹起后viewGroup的高度,屏幕高度-viewGroup高度即为键盘高度
Rect r = new Rect();
viewGroup.getWindowVisibleDisplayFrame(r);
int keyboardHeight = screenHeight - (r.bottom - r.top);
//当设置layout_keyboard设置完padding以后会重绘布局再次执行onGlobalLayout()
//所以判断如果键盘高度未改变就不执行下去
if (keyboardHeight == lastKeyBoardHeight) {
return;
}
lastKeyBoardHeight = keyboardHeight;
View view = activity.getWindow().getCurrentFocus();
if (keyboardHeight > 300 && null != view) {
if (view instanceof TextView) {
//计算出键盘最顶端在布局中的位置
int keyboardTop = screenHeight - keyboardHeight;
int[] location = new int[2];
view.getLocationOnScreen(location);
//获取登录按钮底部在屏幕中的位置
int viewBottom = location[1] + view.getHeight();
//比较输入框与键盘的位置关系,如果输入框在键盘之上的位置就不做处理
if (viewBottom <= keyboardTop)
return;
//需要滚动的距离即为输入框底部到键盘的距离
int reSizeLayoutHeight = viewBottom - keyboardTop;
reSizeLayoutHeight -= getStatusBarHeight();
reSizeLayoutHeight += offset;
if (viewGroup instanceof ScrollView) {
((ScrollView) viewGroup).smoothScrollBy(0, reSizeLayoutHeight);
} else if (viewGroup instanceof RecyclerView) {
((RecyclerView) viewGroup).smoothScrollBy(0, reSizeLayoutHeight);
}
}
}
}
}, 50);
}
});
}
最后:
更详细的参考Demo在github中,我把调整高度的方法封装在一个类中,在Activity中只需要添加一行代码即可。
InputManagerHelper.attachToActivity(this).bind(viewGroup, view).offset(1);
如果有错误也希望大家能够指出,如果觉得能帮到你的话给个Star吧。