*本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布java
在开发中,常常会遇到键盘挡住输入框的状况,好比登陆界面或注册界面,弹出的软键盘把登陆或注册按钮挡住了,用户必须把软键盘收起,才能点击相应按钮,这样的用户体验很是很差。像微信则直接把登陆按钮作在输入框的上面,但有不少状况下,这常常知足不了需求。同时若是输入框特别多的状况下,点击输入时,当前输入框没被挡住,可是当前输入框下面的输入框却没法获取焦点,必须先把键盘收起,再去获取下面输入框焦点,这样用户体验也很是很差,那有什么办法呢?
系统的adjustResize和adjustPan有什么区别,他们使用时的注意事项,有什么系统要求及蔽端呢?android
下面对几种在开发中经常使用的方法进行总结:windows
主要实现方法:
在AndroidManifest.xml对应的Activity里添加
android:windowSoftInputMode=”adjustPan”或是android:windowSoftInputMode=”adjustResize”属性
这两种属性的区别,官方的解释是:
微信
这两个属性做用都是为了调整界面使键盘不挡住输入框 ,我这里对这两种属性使用场景、优缺点、注意事项进行了全方面总结,不知你们平时使用时是否注意到了。ide
属性 | 注意事项 | 优缺点 | 失效状况 | 适用状况 |
---|---|---|---|---|
adjustResize | 须要界面自己可调整尺寸, 如在布局添加ScrollView,或输入控件属于RecycleView/ListView某一项 |
优势:1.不会把标题栏顶出当前布局; 2.有多项输入时,当前输入框下面的输入框可上下滑动输入 缺点:1.须要界面自己可调整尺寸; 2. 全屏时失效 |
1.Activity主窗口尺寸没法调整; 2.Activity全屏 3.android5.0以上经过style设置沉浸式状态栏模式而不设置fitSystemWindow为true |
非全屏或是非沉浸式状态栏输入界面,输入框比较多 |
adjustPan | 页面不会从新布局,当前输入框和键盘会直接将当前输入框以上界面总体向上平移,这样即便界面包含标题栏,也会被顶上去 | 优势: 使用简单,不须要界面自己可调整尺寸,不会有失效状况 缺点: 会把标题栏顶出当前布局;有多项输入时,当前输入框下面的输入框没法输入,必须收起键盘显示输入框再输入 |
无 | 有少许输入项,且输入量居界面上方 |
fitsSystemWindows | 若是多个View设置了fitsSystemWindows=”true”,只有初始的view起做用,都是从第一个设置了fitsSystemWindows的view开始计算padding | 优势:使用简单,须要沉浸式状态栏的界面,不须要本身计算padding状态栏的高度 缺点:使用有限制 |
1.View 的其余 padding 值被从新改写了 2.手机系统版本>=android 4.4 |
1.界面全屏 2.设置界面主题为沉浸式状态栏 |
假设原始界面是一个LinearLayout包含若干EditText,以下图所示,在分别使用两种属性时的表现。工具
整个界面向上平移,使输入框露出,它不会改变界面的布局;界面总体可用高度仍是屏幕高度,这个能够经过下面的截图看出,如点击输入框6,输入框会被推到键盘上方,但输入框1被顶出去了,若是界面包含标题栏,也会被顶出去。布局
须要界面的高度是可变的,或者说Activity主窗口的尺寸是能够调整的,若是不能调整,则不会起做用。
例如:Activity的xml布局中只有一个LinearLayout包含若干EditText,在Activity的AndroidMainfest.xml中设置android:windowSoftInputMode=”adjustResize”属性,点击输入框6, 发现软键盘挡住了输入框6,并无调整,以下图所示:post
但使用这两种属性,咱们能够总结如下几点:
1) 使用adjustPan, 若是须要输入的项比较多时,点击输入框,当前输入项会被顶到软键盘上方,但若当前输入框下面还有输入项时,却须要先收起键盘,再点击相应的输入项才能输入。这样操做太繁琐了,对于用户体验不大好;
2) adjustResize的使用,须要界面自己可显示的窗口内容能调整,可结合scrollview使用;ui
在相应界面的xml布局中,最外层添加一个ScrollView,不在AndroidMainfest.xml中设置任何android:windowSoftInputMode属性,此时点击输入框,输入框均不会被软键盘档住。即便当前输入框下方也有输入框,在键盘显示的状况下,也能够经过上下滑动界面来输入,而不用先隐藏键盘,点击下方输入框,再显示键盘输入。
咱们能够根据Android Studio的Inspect Layout工具来查看界面真正占用的布局高度,工具在
this
经过该工具,咱们看到:
界面真正能用的高度=屏幕高度-状态栏高度-软键盘高度
界面中蓝框是真正界面所用的高度:
咱们再在该类的AndroidMainfest.xml中设置windowSoftInputMode属性为adjustPan,
<activity android:name=".TestInputActivity" android:windowSoftInputMode="adjustPan">
发现当前输入框不会被挡住,可是输入框比较多时,在有键盘显示时,界面上下滑动,但只能滑动部分,且若是输入框在界面靠下方时,点击输入框,标题栏也会被顶出去,以下图所示:
咱们借助Inspect Layout工具查看此设置布局可用高度,从下图能够看出,此时布局可用高度是屏幕的高度,上下滑动也只是此屏的高度,在输入框9如下的输入框滑不出来,向上滑动,也只能滑到输入框1。
咱们前面说过adjustResize的使用必须界面布局高度是可变的,如最外层套个ScrollView或是界面可收缩的,才起做用。这里在该类的AndroidMainfest.xml中设置windowSoftInputMode属性为adjustResize,
<activity android:name=".TestInputActivity" android:windowSoftInputMode="adjustResize">
发现效果和1不设置任何windowSoftInputMode属性相似,其使用高度也是:屏幕高度-状态栏高度-软键盘高度
咱们再来看看windowSoftInputMode默认属性值stateUnspecified:
能够看出,系统将选择合适的状态,也就是在界面最外层包含一层ScrollView时,设置默认属性值stateUnspecified其实就是adjustResize属性。
但如下两方面没法知足需求:
1) 当Activity设置成全屏fullscreen模式时或是使用沉浸式状态栏时,界面最外层包裹 ScrollView,当输入框超过一屏,当前输入框下面的输入框并不能上下滑动来输入,状况相似于ScrollView+adjustPan,只能滑动部分,经过Inspect Layout也能够看到,界面可用高度是整个屏幕高度,并不会进行调整高度。即便设置adjustResize,也不起做用。
2) 若是是相似于注册界面或是登陆界面,键盘会挡住输入框下面的登陆按钮。
自android系统4.4(API>=19)就开始支持沉浸式状态栏,当使用觉System windows(系统窗口),显示系统一些属性和操做区域,如 最上方的状态及没有实体按键的最下方的虚拟导航栏。
android:fitsSystemWindows=“true”会使得屏幕上的可布局空间位于状态栏下方与导航栏上方
使用场景:针对界面全屏或是沉浸式状态栏,输入框不会被键盘遮挡。主要用于一些登陆界面,或是须要把界面总体都顶上去的场景。
(1) 获取Activity布局xml的最外层控件,如xml文件以下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/main" tools:context="com.example.liubin1.softkeyboardhelper.MainActivity"> <EditText android:id="@+id/name" android:hint="请输入用户名:" android:layout_centerInParent="true" android:layout_width="match_parent" android:layout_height="50dp" /> <EditText android:id="@+id/pas" android:layout_below="@id/name" android:hint="请输入密码:" android:layout_centerInParent="true" android:layout_width="match_parent" android:layout_height="50dp" /> <Button android:id="@+id/login_btn" android:layout_below="@id/rpas" android:layout_centerHorizontal="true" android:text="登陆" android:layout_width="180dp" android:layout_height="50dp" /> </RelativeLayout>
先获取到最外层控件
RelativeLayout main = (RelativeLayout) findViewById(R.id.main);
(2) 获取到最后一个控件,如上面的xml文件,最后一个控件是Button
Button login_btn = (Button) findViewById(R.id.login_btn);
(3) 给最外层控件和最后一个控件添加监听事件
//在Activity的onCreate里添加以下方法 addLayoutListener(main,login_btn); /** * addLayoutListener方法以下 * @param main 根布局 * @param scroll 须要显示的最下方View */ public void addLayoutListener(final View main, final View scroll) { main.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Rect rect = new Rect(); //一、获取main在窗体的可视区域 main.getWindowVisibleDisplayFrame(rect); //二、获取main在窗体的不可视区域高度,在键盘没有弹起时,main.getRootView().getHeight()调节度应该和rect.bottom高度同样 int mainInvisibleHeight = main.getRootView().getHeight() - rect.bottom; int screenHeight = main.getRootView().getHeight();//屏幕高度 //三、不可见区域大于屏幕自己高度的1/4:说明键盘弹起了 if (mainInvisibleHeight > screenHeight / 4) { int[] location = new int[2]; scroll.getLocationInWindow(location); // 4、获取Scroll的窗体坐标,算出main须要滚动的高度 int srollHeight = (location[1] + scroll.getHeight()) - rect.bottom; //5、让界面总体上移键盘的高度 main.scrollTo(0, srollHeight); } else { //三、不可见区域小于屏幕高度1/4时,说明键盘隐藏了,把界面下移,移回到原有高度 main.scrollTo(0, 0); } } }); } }
此方法经过监听Activity最外层布局控件来检测软键盘是否弹出,而后去手动调用控件的scrollTo方法达到调整布局目的。
具体实现代码见demo中的LoginActivity类。
此种方法须要在当前界面写比较多的代码,在某些手机上,若输入时,软键盘高度是可变的,如中英文切换,高度变化时,会发现适配的不大好。以下图:
从上图能够看出,若是键盘高度变化,键盘仍是会挡住登陆按钮。
此方法主要是经过在须要移动的控件外套一层scrollView,同时最布局最外层使用自定义view监听键盘弹出状态,计算键盘高度,再进行计算须要移动的位置,这个和方法三有点相似,但能适配键盘高度变化状况。
(1) 先写自定义View,实时临听界面键盘弹起状态,计算键盘高度
public class KeyboardLayout extends FrameLayout { private KeyboardLayoutListener mListener; private boolean mIsKeyboardActive = false; //输入法是否激活 private int mKeyboardHeight = 0; // 输入法高度 public KeyboardLayout(Context context) { this(context, null, 0); } public KeyboardLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public KeyboardLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 监听布局变化 getViewTreeObserver().addOnGlobalLayoutListener(new KeyboardOnGlobalChangeListener()); } public void setKeyboardListener(KeyboardLayoutListener listener) { mListener = listener; } public KeyboardLayoutListener getKeyboardListener() { return mListener; } public boolean isKeyboardActive() { return mIsKeyboardActive; } /** * 获取输入法高度 * * @return */ public int getKeyboardHeight() { return mKeyboardHeight; } public interface KeyboardLayoutListener { /** * @param isActive 输入法是否激活 * @param keyboardHeight 输入法面板高度 */ void onKeyboardStateChanged(boolean isActive, int keyboardHeight); } private class KeyboardOnGlobalChangeListener implements ViewTreeObserver.OnGlobalLayoutListener { int mScreenHeight = 0; private int getScreenHeight() { if (mScreenHeight > 0) { return mScreenHeight; } mScreenHeight = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay().getHeight(); return mScreenHeight; } @Override public void onGlobalLayout() { Rect rect = new Rect(); // 获取当前页面窗口的显示范围 ((Activity) getContext()).getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); int screenHeight = getScreenHeight(); int keyboardHeight = screenHeight - rect.bottom; // 输入法的高度 boolean isActive = false; if (Math.abs(keyboardHeight) > screenHeight / 4) { isActive = true; // 超过屏幕五分之一则表示弹出了输入法 mKeyboardHeight = keyboardHeight; } mIsKeyboardActive = isActive; if (mListener != null) { mListener.onKeyboardStateChanged(isActive, keyboardHeight); } } } }
(2) xml文件编写,在界面最外层套上自定义view,在须要滚动的控件外层添加scrollView
<com.example.smilexie.softboradblockedittext.util.KeyboardLayout android:id="@+id/main_ll" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@mipmap/login_bg" android:orientation="vertical"> <ScrollView android:id="@+id/login_ll" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="50dp" android:layout_marginRight="50dp" android:layout_marginTop="200dp" android:background="@mipmap/login_input_field_icon" android:orientation="horizontal"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="20dp" android:background="@mipmap/login_yonghuming_icon" /> <EditText android:id="@+id/ui_username_input" style="@style/editext_input_style" android:layout_marginLeft="40dp" android:layout_marginRight="20dp" android:background="@null" android:hint="@string/login_hint_username" android:imeOptions="actionNext" android:textColor="@android:color/white" android:textColorHint="@android:color/white"> <requestFocus /> </EditText> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="50dp" android:layout_marginRight="50dp" android:layout_marginTop="20dp" android:background="@mipmap/login_input_field_icon" android:orientation="horizontal"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="20dp" android:background="@mipmap/login_mima_icon" /> <EditText android:id="@+id/ui_password_input" style="@style/editext_input_style" android:layout_marginLeft="40dp" android:layout_marginRight="20dp" android:background="@null" android:hint="@string/login_hint_pwd" android:imeOptions="actionDone" android:inputType="textPassword" android:textColor="@android:color/white" android:textColorHint="@android:color/white"></EditText> </LinearLayout> <Button android:id="@+id/login_btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="50dp" android:layout_marginRight="50dp" android:layout_marginTop="20dp" android:background="@mipmap/login_button_bg_icon" android:text="@string/login" android:textColor="@color/titlebar_main_color" android:textSize="@dimen/font_normal" /> </LinearLayout> </ScrollView> </com.example.smilexie.softboradblockedittext.util.KeyboardLayout>
(3) Activity调用,自定义view控件添加键盘响应,在键盘变化时调用scrollView的smoothScrollTo去滚动界面
/** * 监听键盘状态,布局有变化时,靠scrollView去滚动界面 */ public void addLayoutListener() { bindingView.mainLl.setKeyboardListener(new KeyboardLayout.KeyboardLayoutListener() { @Override public void onKeyboardStateChanged(boolean isActive, int keyboardHeight) { Log.e("onKeyboardStateChanged", "isActive:" + isActive + " keyboardHeight:" + keyboardHeight); if (isActive) { scrollToBottom(); } } }); } /** * 弹出软键盘时将SVContainer滑到底 */ private void scrollToBottom() { bindingView.loginLl.postDelayed(new Runnable() { @Override public void run() { bindingView.loginLl.smoothScrollTo(0, bindingView.loginLl.getBottom() + SoftKeyInputHidWidget.getStatusBarHeight(LoginActivityForDiffkeyboardHeight.this)); } }, 100); }
具体实现代码见demo中的LoginActivityForDiffkeyboardHeight类。实现效果以下:
能够看到键盘高度变化了,也不会影响界面布局
此方法的实现来自android中提出的issue 5497 https://code.google.com/p/android/issues/detail?id=5497
使用场景:针对界面全屏或是沉浸式状态栏,界面包含比较多输入框,界面即便包裹了一层ScrollView,在键盘显示时,当前输入框下面的输入不能经过上下滑动界面来输入。
感谢下面提出评论的同窗,指出此方法的不适配问题,以前写的博文在华为小米手机上确实有不适配状况,在输入时,键盘有时会错乱,如今已加入适配。
1、把SoftHideKeyBoardUtil类复制到项目中;
2、在须要使用的Activity的onCreate方法中添加:SoftHideKeyBoardUtil.assistActivity(this);便可。
SoftHideKeyBoardUtil类具体代码以下:
/** * 解决键盘档住输入框 * Created by SmileXie on 2017/4/3. */ public class SoftHideKeyBoardUtil { public static void assistActivity (Activity activity) { new SoftHideKeyBoardUtil(activity); } private View mChildOfContent; private int usableHeightPrevious; private FrameLayout.LayoutParams frameLayoutParams; //为适应华为小米等手机键盘上方出现黑条或不适配 private int contentHeight;//获取setContentView原本view的高度 private boolean isfirst = true;//只用获取一次 private int statusBarHeight;//状态栏高度 private SoftHideKeyBoardUtil(Activity activity) { //1、找到Activity的最外层布局控件,它实际上是一个DecorView,它所用的控件就是FrameLayout FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content); //2、获取到setContentView放进去的View mChildOfContent = content.getChildAt(0); //3、给Activity的xml布局设置View树监听,当布局有变化,如键盘弹出或收起时,都会回调此监听 mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { //4、软键盘弹起会使GlobalLayout发生变化 public void onGlobalLayout() { if (isfirst) { contentHeight = mChildOfContent.getHeight();//兼容华为等机型 isfirst = false; } //5、当前布局发生变化时,对Activity的xml布局进行重绘 possiblyResizeChildOfContent(); } }); //6、获取到Activity的xml布局的放置参数 frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams(); } // 获取界面可用高度,若是软键盘弹起后,Activity的xml布局可用高度须要减去键盘高度 private void possiblyResizeChildOfContent() { //1、获取当前界面可用高度,键盘弹起后,当前界面可用布局会减小键盘的高度 int usableHeightNow = computeUsableHeight(); //2、若是当前可用高度和原始值不同 if (usableHeightNow != usableHeightPrevious) { //3、获取Activity中xml中布局在当前界面显示的高度 int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight(); //4、Activity中xml布局的高度-当前可用高度 int heightDifference = usableHeightSansKeyboard - usableHeightNow; //5、高度差大于屏幕1/4时,说明键盘弹出 if (heightDifference > (usableHeightSansKeyboard/4)) { // 6、键盘弹出了,Activity的xml布局高度应当减去键盘高度 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ frameLayoutParams.height = usableHeightSansKeyboard - heightDifference + statusBarHeight; } else { frameLayoutParams.height = usableHeightSansKeyboard - heightDifference; } } else { frameLayoutParams.height = contentHeight; } //7、 重绘Activity的xml布局 mChildOfContent.requestLayout(); usableHeightPrevious = usableHeightNow; } } private int computeUsableHeight() { Rect r = new Rect(); mChildOfContent.getWindowVisibleDisplayFrame(r); // 全屏模式下:直接返回r.bottom,r.top实际上是状态栏的高度 return (r.bottom - r.top); } }
它的实现原理主要是:
(1) 找到Activity的最外层布局控件,咱们知道全部的Activity都是DecorView,它就是一个FrameLayout控件,该控件id是系统写死叫R.id.content,就是咱们setContentView时,把相应的View放在此FrameLayout控件里
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
因此content.getChildAt(0)获取到的mChildOfContent,也就是咱们用setContentView放进去的View。
(2) 给咱们的Activity的xml布局View设置一个Listener监听
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener({ possiblyResizeChildOfContent(); });
View.getViewTreeObserver()能够获取一个ViewTreeObserver对象——它是一个观察者,用以监听当前View树所发生的变化。这里所注册的addOnGlobalLayoutListener,就是会在当前的View树的全局布局(GlobalLayout)发生变化、或者其中的View可视状态有变化时,进行通知回调。『软键盘弹出/隐 』都能监听到。
(3) 获取当前界面可用高度
private int computeUsableHeight() { Rect rect = new Rect(); mChildOfContent.getWindowVisibleDisplayFrame(rect); // rect.top实际上是状态栏的高度,若是是全屏主题,直接 return rect.bottom就能够了 return (rect.bottom - rect.top); }
以下图所示:
(4) 重设高度, 咱们计算出的可用高度,是目前在视觉效果上能看到的界面高度。但当前界面的实际高度是比可用高度要多出一个软键盘的距离的。
具体实现代码见demo中的TransStatusbarWisthAssistActivity类。
注意:若是既使用了沉浸式状态栏,又加了fitSystetemWindow=true属性,就须要在AndroidMainfest.xml注册Activity的地方添加上如下属性。由于你两种都用,系统不知道用哪一种了。fitSystetemWindow已经有resize屏幕的做用。
android:windowSoftInputMode="stateHidden|adjustPan"
经过上面的这种方法,通常布局输入键盘挡住输入框的问题基本都能解决。即便界面全屏或是沉浸式状态栏状况。
下面对上面几种方法进行对比:
方法一:优势:使用简单,只需在Activity的AndroidMainfest.xml中设置windowSoftInput属性便可。
注意点:adjustResize属性必需要界面大小能够自身改变;
缺点:当输入框比较多时,当前输入框下方的输入框会初键盘挡住,须收起键盘再进入输入;使用adjustPan,输入框较多时,因它是把界面当成一个总体,只会显示一屏的高度,会把ActionBar顶上去。
方法二:优势:使用简单,只需在Activity的最外层布局包裹一个ScrollView便可。
注意点:不可以使用adjustPan属性,不然ScrollView失效;
缺点:对于全屏时,在键盘显示时,没法上下滑动界面达到输入的目的;
方法三:优势:能够解决全屏时,键盘挡入按钮问题。
缺点:只要有此需求的Activity均须要获取到最外层控件和最后一个控件,监测键盘是否弹出,再调用控件的scrollTo方法对界面总体上移或是下移。代码冗余。对于键盘高度变化时,适配很差。
方法四:优势:能够解决全屏时,键盘挡入按钮问题。
缺点:只要有此需求的Activity均须要获取到最外层控件和最后一个控件,布局多出一层。
方法五:优势:能够解决全屏时,键盘挡入输入框问题。只须要写一个全局类,其余有需求的界面直接在onCreate方法里调用此类的全局方法,便可。
缺点:多用了一个类。
综上所述: 1) 当输入框比较少时,界面只有一个输入框时,能够经过方法一设置adjustPan; 2) 若是对于非全屏/非沉浸式状态栏需求,只须要使用方法二ScrollView+adjustResize; 3) 若是对于使用沉浸式状态栏,使用fitSystemWindow=true属性,按道理android系统已经作好适配,键盘不会挡住输入框; 4) 若是全屏/沉浸式状态栏界面,相似于登陆界面,有须要把登陆键钮或是评论按钮也顶起,若是键盘没有变化需求,可使用方法三,若须要适配键盘高度变化,则须要使用方法四; 5) 若是界面使用全屏或沉浸式状态栏,没有使用fitSystemWindow=true属性,通常如须要用到抽屈并且状态栏颜色也须要跟着变化,则选择方法五更恰当。