Android输入法遮挡问题的解决思路

作者: imflyn | 来源:发表于2017-03-22 15:19 被阅读1921次

    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吧。

    相关文章

      网友评论

      • FeelRelus:多谢!
      • cwzqf:大神是用的setPadding来达到效果,学到了,我之前用scrollTo实现,导致editText会失去焦点
        78983551cc90:scrollTo我尝试并不会导致Edit失去焦点
        cwzqf:光标会出现闪烁吗
      • silence_jjj:大神的这个思路真好,如果每次弹出或收起加上平移动画就更好了,小弟先拿去锦上添花了:stuck_out_tongue:
        imflyn:@silence_jjj 是个不错的建议😃
        silence_jjj:@imflyn 赞,比我自己写的属性动画简洁省力,但是还有个小小问题addOnGlobalLayoutListener的监听最好在onstop()时候回收掉。
        imflyn:@silence_jjj 可以在setpadding方法之前加上这句TransitionManager.beginDelayedTransition(viewGroup) 就会有平移的动画
      • 皇马船长:Github Demo中 TranslucentLayoutActivity ,如果EditText 设置了android:inputType="textPassword" , 效果就不那么友好了,登录按钮还是会被输入法遮挡,有什么办法吗
        imflyn:哥们,是存在这个问题,谢谢提醒。当焦点切换到第二个输入框时,有些手机键盘会重新弹出。但是获取登录按钮的位置是获取的焦点在第一个输入框时候的位置,这就导致后面计算布局需要改变多少高度就有问题了。解决方法是在布局文件中加上这行“android:fitsSystemWindows="true"。 代码已经提交了。你可以看下。 为什么fitsSystemWindows这个属性计算高度和对布局产生的作用还在调查中。
      • 0a70bbb8e40e:请问一下,你写的这个支持自定义的键盘吗?如果是在一个自定义的Dialog里面弹出来一个自定义的键盘,布局里面的内容还会往上移动吗?
        0a70bbb8e40e:好的,,谢谢
        imflyn:@罗曼_克里斯汀 自定义键盘和dailog中还不支持
      • 相互交流:楼主用一下最外层加一个scrollView
        imflyn:在最外层加一个ScrollView是一个不错的方法:grin: 。但是如果要将登录按钮处于可见范围内需要手动去滑动,否则还是需要代码去判断软键盘是否弹起从而调整登录按钮的位置。
      • wenzhihao123:我设置沉浸栏就不好使了~😬
        wenzhihao123:@imflyn 楼主用addOnGlobalLayoutListener不会出现问题么,万一让edittext一旦输入文字后面显示一个小叉号清空,这个方法也会调用,也就是说这个方法调用太频繁,会出问题~:grin:
        wenzhihao123:@imflyn 我没用toolbar,根布局android:fitsSystemWindows="true",然后设置style的属性<item name="android:windowTranslucentStatus">true</item> ~
        imflyn:我更新了下Demo,在ToolBarActivity里状态栏是沉浸式,是有效果的。能否把你说的设置沉浸式在描述的具体些。
      • Hidetag:安卓有个issue专门解决这个问题,是个类
        imflyn:@S_H_I_E_L_D 谢谢。AndroidBug5497Workaround这个类是一个解决办法。原理都是为ViewTreeObserver添加监听。不过我之前用这个方法试的时候,其中计算布局需要改变的高度还是有些偏差,没法达到我想要的效果。
        Hidetag:@imflyn https://code.google.com/p/android/issues/detail?id=5497
        imflyn: @S_H_I_E_L_D 嗯,我之前应该看到过, 我也是受那个类的启发。不知道和你说的是不是同一个。

      本文标题:Android输入法遮挡问题的解决思路

      本文链接:https://www.haomeiwen.com/subject/eoponttx.html