Keyboard 软键盘阻挡输入框爬坑指南

来源:互联网 发布:2016年网络群体性事件 编辑:程序博客网 时间:2024/06/16 08:21

导读:

日常开发中我们经常会用到EditText输入框,但有时我们的输入框会出现被软键盘界面阻挡,那么我们就会想到设置android:windowSoftInputMode属性

但是,当我们用的正爽的时候,又会出现什么布局上移,ba..ba..ba的bug

因此,本篇将针对个人在开发中遇到的”软键盘阻挡输入框”问题介绍


android:windowSoftInputMode属性说明

属性 说明 stateUnspecified 软键盘的状态并没有指定,系统将选择一个合适的状态或依赖于主题的设置 stateUnchanged 当这个activity出现时,软键盘将一直保持在上一个activity里的状态,无论是隐藏还是显示 stateHidden 用户选择activity时,软键盘总是被隐藏 stateAlwaysHidden 当该Activity主窗口获取焦点时,软键盘也总是被隐藏的 stateVisible 软键盘通常是可见的 stateAlwaysVisible 用户选择activity时,软键盘总是显示的状态 adjustUnspecified 默认设置,通常由系统自行决定是隐藏还是显示 adjustResize 该Activity总是调整屏幕的大小以便留出软键盘的空间 adjustPan 当前窗口的内容将自动移动以便当前焦点从不被键盘覆盖和用户能总是看到输入内容的部分 adjustNothing 不调整窗口大小或平移窗口,软键盘就默认显示,会覆盖后面内容
  • 这个属性能影响两个事情:

    1. 当有焦点产生时,软键盘是隐藏还是显示
    2. 是否减少活动主窗口大小以便腾出空间放软键盘
  • 它的设置必须是下面的一个值,或者”state…| adjust..”

  • 正常情况来说,如果要防止软键盘挡住输入框如上表设置属性就能解决…

代码实现:

//adjustPangetWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);//adjustResizegetWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);

清单文件对应节点实现:

        <!-- 静态设置输入模式 -->        <!-- android:windowSoftInputMode="adjustResize" -->        <activity            android:name=".activity.AdjustResize_KeyboardActivity"            android:windowSoftInputMode="adjustResize"/>        <!-- android:windowSoftInputMode="adjustPan" -->        <activity            android:name=".activity.AdjustPan_KeyboardActivity"            android:windowSoftInputMode="adjustPan"/>

部分属性应用场景说明:

  1. 使用adjustPan,如果有几个EditText,当点击第一个EditText时,下方的EditText会被软键盘界面覆盖
  2. 使用adjustPan,如果输入框在软键盘后面,软键盘会把输入框以上的布局(包括ToolBar)挤出屏幕外
  3. 使用adjustResize,当使用LinearLayout作为输入框的父布局时,软键盘会覆盖输入框
  4. 使用adjustResize,当控件设置了background属性,或设置了权重的imageView会发生变形,该使用adjustPan

开发中遇到的Bug,以及解决方案:

设置了沉浸式/透明状态栏后,adjustResize失效问题

在根布局或者父布局设置android:fitsSystemWindows=”true”即可解决


“全屏模式”使用adjustResize无效,而且会类似adjustPan把上方布局挤出屏幕外

官方认为”全屏模式”指的是App自己接管了状态栏的控制,如使用了Fullscreen主题、使用了『状态色着色』、『沉浸式状态栏』、『Immersive Mode』等等

个人开发也有一段时间,由于很少遇到需要全屏(状态栏也隐藏)的应用,真说有的话,那就是游戏里的,不过一旦设置Fullscreen主题一类的全屏,点击EditText输入框会跳转到一个全屏模式的输入界面,因此这里就不作处理了,有兴趣的同学可以根据以下代码深入研究下,这里就不赘述了.使用:(最终效果类似adjustResize)1.把AndroidBug5497Workaround类复制到项目中2.在需要填坑的activity的onCreate方法中添加一句AndroidBug5497Workaround.assistActivity(this)即可。----------------------------------------------------/* * @本类描述   网上收集的,解决"全屏模式"下,adjustResize失效方案    *             * @内容说明   解决: *             1.非全屏模式下使用adjustPan无效问题 *             2.全屏模式下使用adjustPan和adjustResize无效问题 *  * @补充内容   有兴趣的同学可以学下里面的思路,自己写一个解决方案(*^__^*) 嘻嘻……) * * ---------------------------------      * @更新时间    * @新增内容    * */public class AndroidBug5497Workaround {    // For more information, see https://code.google.com/p/android/issues/detail?id=5497    // To use this class, simply invoke assistActivity() on an Activity that already has its content view set.    public static void assistActivity(Activity activity) {        new AndroidBug5497Workaround(activity);    }    private View                     mChildOfContent;    private int                      usableHeightPrevious;    private FrameLayout.LayoutParams frameLayoutParams;    private AndroidBug5497Workaround(Activity activity) {        //拿到当前XML文件的根布局        FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);        //监听当前View的状态,进行通知回调,即"软键盘弹出""        mChildOfContent = content.getChildAt(0);        mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {            public void onGlobalLayout() {                possiblyResizeChildOfContent();            }        });        frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();    }  /**     * 重新设置高度     * <p>     * 把界面高度设置为可用高度     */    private void possiblyResizeChildOfContent() {        int usableHeightNow = computeUsableHeight();        if (usableHeightNow != usableHeightPrevious) {            // int usableHeightSansKeyboard = activity.getWindowManager().getDefaultDisplay().getHeight();//获取屏幕尺寸,不包括虚拟功能高度 用这个可以完美解决            //findViewById(android.R.id.content).getMeasuredHeight() 也可以解决虚拟按键问题            int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();            int heightDifference = usableHeightSansKeyboard - usableHeightNow;            //排除其他View引起的变化,专注软键盘变化            if (heightDifference > (usableHeightSansKeyboard / 4)) {                // keyboard probably just became visible                frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;            } else {                // keyboard probably just became hidden                frameLayoutParams.height = usableHeightSansKeyboard;            }            mChildOfContent.requestLayout();            usableHeightPrevious = usableHeightNow;        }    }    /**     * 软键盘弹出后,可以显示内容的高度     *     * @return     */    private int computeUsableHeight() {        Rect r = new Rect();        //这行代码能够获取到去除标题栏和被软键盘挡住的部分,所剩下的矩形区域        mChildOfContent.getWindowVisibleDisplayFrame(r);        //r.top : 标题栏的高度        //屏幕高度-r.bottom : 软键盘的高度        //可用高度(全屏模式) : rect.bottom        //可用高度(非全屏模式) : rect.bottom - rect.top        return (r.bottom - r.top);// 全屏模式下: return r.bottom    }}

工具类图解

这里写图片描述


一些”奇淫”技巧

有时候,我们需要将想要的Button按钮也显示在软键盘上面,如登录页面,如果我们只设置了adjustPan或adjustResize,那么Button按钮就会被挡住,体验不好.下面介绍几种解决方案

其实最简单的方式是将布局嵌套在ScrollView里,但体验一样不好,操作多了一步嘛

一、工具类方法(监听软键盘弹出位置,让我们的布局滑动到合适的位置)

Utils类

public class NestedScrollView_KeyboardUtils {    private static final String TAG = "NestedScrollView_KeyboardUtils";    /**     * @param activity 上下文     * @param viewId   NestedScrollView_ID     */    public static void assistActivity(Activity activity, int viewId) {        new NestedScrollView_KeyboardUtils(activity, viewId);    }    private View             mChildOfContent;    /**android.support.v4包中,新版的ScrollView**/    private NestedScrollView mScrollView;    private NestedScrollView_KeyboardUtils(Activity activity, int viewId) {        //拿到当前XML文件的根布局        FrameLayout content = (FrameLayout) activity                .findViewById(android.R.id.content);        //监听当前View的状态,进行通知回调,即"软键盘弹出""        mChildOfContent = content.getChildAt(0);        mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(                new ViewTreeObserver.OnGlobalLayoutListener() {                    @Override                    public void onGlobalLayout() {                        possiblyResizeChildOfContent();                    }                });        mScrollView = (NestedScrollView) content.findViewById(viewId);    }    private void possiblyResizeChildOfContent() {        int contentHeight = mChildOfContent.getRootView().getHeight();        int curDisplayHeight = computeUsableHeight();        if (contentHeight - curDisplayHeight > contentHeight / 4) {            Log.e(TAG, "possiblyResizeChildOfContent: 1");            mScrollView.scrollTo(0, 600);            //                mScrollView.fullScroll(ScrollView.FOCUS_DOWN);        } else {            Log.e(TAG, "possiblyResizeChildOfContent: 2");        }    }    /**     * 软键盘弹出后,获取屏幕可显示区域高度     *     * @return     */    private int computeUsableHeight() {        Rect r = new Rect();        //这行代码能够获取到去除标题栏和被软键盘挡住的部分,所剩下的矩形区域        mChildOfContent.getWindowVisibleDisplayFrame(r);        //r.top : 标题栏的高度        //屏幕高度-r.bottom : 软键盘的高度        //可用高度(全屏模式) : rect.bottom        //可用高度(非全屏模式) : rect.bottom - rect.top        return r.height();    }}

布局文件

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:layout_width="match_parent"              android:layout_height="match_parent"              android:fitsSystemWindows="true"              android:clipToPadding="true"    >    <android.support.v4.widget.NestedScrollView        android:id="@+id/scroll_view"        android:layout_width="match_parent"        android:layout_height="match_parent">        <LinearLayout            android:id="@+id/ll_parent"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:orientation="vertical">            <TextView                android:layout_width="match_parent"                android:layout_height="200dp"                android:background="#0000ff"/>            <EditText                android:id="@+id/account"                android:layout_width="match_parent"                android:layout_height="match_parent"/>            <EditText                android:layout_width="match_parent"                android:layout_height="match_parent"/>            <Button                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:text="确定"/>        </LinearLayout>    </android.support.v4.widget.NestedScrollView></LinearLayout>

主页面

// 改动过状态栏的状态,记得在XML布局文件根布局设置android:fitsSystemWindows="true"public class ScrollViewByClassActivity extends AppCompatActivity {    private LinearLayout     mParent;    private EditText         account;    private NestedScrollView scroll;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_scroll_class);        //getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // 设置全屏        StatusBar.setStatusBarColor(this, Color.BLUE);        mParent = (LinearLayout) findViewById(R.id.ll_parent);        account = (EditText) findViewById(R.id.account);        scroll = (NestedScrollView) findViewById(R.id.scroll_view);        setScroll();    }    //键盘不遮挡按钮    private void setScroll() {        NestedScrollView_KeyboardUtils.assistActivity(this, R.id.scroll_view);       //这个是别人给我的工具类,只用这个会有        hideKeyboard();        scroll.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {         //scroll为parent外面那层布局()最好用NestedScrollView,ScrollView会有版本问题            @Override            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {                v.smoothScrollTo(0, 450);     //这个是滑动距离,随便大一点就好            }        });    }    /**     * EditText失去焦点,隐藏软键盘     */    private void hideKeyboard() {        mParent.setOnTouchListener(new View.OnTouchListener() {                 //parent为Editext外面那层布局            @Override            public boolean onTouch(View v, MotionEvent event) {                mParent.setFocusable(true);                mParent.setFocusableInTouchMode(true);                mParent.requestFocus();                InputMethodManager imm = (InputMethodManager) ScrollViewByClassActivity.this                        .getSystemService(Context.INPUT_METHOD_SERVICE);                imm.hideSoftInputFromWindow(account.getWindowToken(), 0);  //隐藏键盘,account为Editext,随便一个就好                return false;            }        });    }}

效果图

这里写图片描述


二、自定义View作为我们的根布局,监听软键盘弹出状态,将我们的布局滑动到合适位置

自定义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;    }    /**     * 测试弹出输入法时,系统做了什么     **/    private static int count = 0;    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        Log.e("onSizeChanged " + count++, "=>onResize called! w=" + w + ",h=" + h + ",oldw=" + oldw + ",oldh=" + oldh);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        Log.e("onLayout " + count++, "=>OnLayout called! l=" + l + ", t=" + t + ",r=" + r + ",b=" + b);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        Log.e("onMeasure " + count++, "=>onMeasure called! widthMeasureSpec=" + widthMeasureSpec + ", heightMeasureSpec=" + heightMeasureSpec);    }    /**     * 获取输入法高度     *     * @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);            }        }    }}

布局文件

<?xml version="1.0" encoding="utf-8"?><zs.xmx.view.KeyboardLayout xmlns:android="http://schemas.android.com/apk/res/android"                            android:id="@+id/keyboardview"                            android:layout_width="match_parent"                            android:layout_height="match_parent"                            android:fitsSystemWindows="true"    >    <ScrollView        android:id="@+id/scroll_view"        android:layout_width="match_parent"        android:layout_height="match_parent">        <LinearLayout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:orientation="vertical">            <TextView                android:layout_width="match_parent"                android:layout_height="200dp"                android:background="#00f0ff"/>            <EditText                android:id="@+id/account"                android:layout_width="match_parent"                android:layout_height="match_parent"/>            <EditText                android:layout_width="match_parent"                android:layout_height="match_parent"/>            <Button                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:text="确定"/>        </LinearLayout>    </ScrollView></zs.xmx.view.KeyboardLayout>

主页面

public class ScrollViewByLayoutActivity extends AppCompatActivity {    private ScrollView     scroll;    private KeyboardLayout mKeyboardLayout;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        // requestWindowFeature(Window.FEATURE_NO_TITLE); // 去除标题  必须在setContentView()方法之前调用        // getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // 设置全屏        setContentView(R.layout.activity_scroll_layout);        StatusBar.setStatusBarColor(this, Color.GRAY);        mKeyboardLayout = (KeyboardLayout) findViewById(R.id.keyboardview);        scroll = (ScrollView) findViewById(R.id.scroll_view);        addLayoutListener();    }    /**     * 监听键盘状态,布局有变化时,靠scrollView去滚动界面     */    public void addLayoutListener() {        mKeyboardLayout.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() {        scroll.postDelayed(new Runnable() {            @Override            public void run() {                scroll.smoothScrollTo(0, scroll.getBottom() + getStatusBarHeight(ScrollViewByLayoutActivity.this));            }        }, 100);    }    /**     * 获取状态栏高度     *     * @param activity     * @return     */    public static int getStatusBarHeight(Activity activity) {        //获取状态栏的高度        int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");        return activity.getResources().getDimensionPixelSize(resourceId);    }}

效果图:

这里写图片描述


总结:

  1. 如果EditText的位置写死在软键盘下方,会被覆盖.同理如果不想它移动可以写死,但是要注意屏幕适配问题
  2. 有些定制的键盘有精简模式,还有全屏/数字键盘大小不一样,可能有问题(目前没遇到)
  3. 若像我们登录界面,需要把登录按钮也上移,用scrollTo + 布局监听软键盘(即上面两种方案)
  4. 若有多个输入框,没什么特殊要求,使用ScrollView + adjustResize,基本能够满足开发需求
  5. 设置了沉浸式/透明状态栏,记得在布局文件设置android:fitsSystemWindows=”true”属性

本篇测试Demo:

这里写图片描述

Demo源码: keyboard_Demo

Keyboard 相关文章链接

Keyboard 输入框与软键盘联动

keyboard 动态启动或关闭软键盘

本篇文章到此结束,欢迎关注,后续有补充的会即时更新,有问题也欢迎评论,共同成长

待续:深入学下测量原理,写一个适配全屏的工具类