Android弹出键盘布局闪动原理和解决

弹出键盘布局闪动原理和解决

  在开发中,遇到一个问题:做一个微信一样,表情输入和软键盘在切换的时候,聊天界面不闪动的问题。为了解决这个问题,需要知道一下Android的软键盘弹出的时候发生的几个变化。

  当AndroidMainfest.xml 中配置android:windowSoftInputMode="adjustResize|stateHidden" 属性后,如果弹出软键盘,那么会重绘界面。基本流程如下(API 10):

    1.  Android 收到打开软键盘命令

    2.  Android 打开软键盘后,调用App 注册在AWM 中的接口,告知它,界面需要进行变化.

      2.1 调用ViewRoot.java#W#resized(w,h)

      2.2 调用viewRoot.dispatchResized()

      2.3 构造Message msg = obtainMessage(reportDraw ? RESIZED_REPORT :RESIZED),然后post过去

    3. 在RootView的handleMessage的case RESIZED_REPORT: 收到具体的大小,配置App Window的大小,特别是 bottom 的大小, 最后调用requestLayout进行重绘

   在上述的过程中,我们可以发现,其实弹出键盘之后的界面闪动的核心是在于app 的window botton 属性进行变化了,从而导致整个ViewTree的高度变化。那么我们只要表情PANEL在父布局onMeasure之前,进行VISIBLE或者GONE处理,使得最终的所有子View的高度满足window height,既可以实现不闪动聊天页面。

  其核心思想为:在父布局onMeasure之前,隐藏/显示 合适高度的VIEW,既可以使得其他子VIEW高度不变化,从而避免界面闪动。引用自http://blog.dreamtobe.cn/2015/09/01/keyboard-panel-switch/。

  代码如下:

  Layout:

<?xml version="1.0" encoding="utf-8"?>
<com.test.MyActivity.MyLineLayout xmlns:android="http://schemas.android.com/apk/res/android"
                       android:orientation="vertical"
                       android:id="@+id/mll_main"
                       android:layout_width="match_parent"
                       android:layout_height="match_parent">
    <FrameLayout
            android:background="#f3f3f3"
            android:id="@+id/fl_list"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1">
        <TextView
                android:gravity="center"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="DARKGEM"/>
    </FrameLayout>
    <LinearLayout
            android:id="@+id/ll_edit"
            android:layout_width="match_parent"
            android:orientation="horizontal"
            android:layout_height="50dp">
        <EditText
                android:id="@+id/et_input"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="match_parent"/>
        <Button
                android:id="@+id/btn_trigger"
                android:text="trigger"
                android:layout_width="100dp"
                android:layout_height="match_parent"/>
    </LinearLayout>
    <FrameLayout
            android:id="@+id/fl_panel"
            android:background="#CCCCCC"
            android:layout_width="match_parent"
            android:layout_height="0dp"/>
</com.test.MyActivity.MyLineLayout>

  Activity:

  

package com.test;

import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

/**
 * <pre>
 * 聊天界面布局闪动处理, 基本原理如下:
 *          1. 弹出键盘的时候,会导致 RootView 的bottom 变小,直到容纳 键盘+虚拟按键
 *          2. 收回键盘的时候,会导致 RootView的bottom 变大,直到容纳 虚拟键盘
 *          3. 因为RootView bottom的变化,会导致整个布局高度(bottom - top)的变化,所以就会发生布局闪动的情况. 而为了
 *          避免这种情况,只需要在发生变动的父布局调用 onMeasure() 之前,将子View的高度和配置为最终高度,既可以实现弹
 *          出/收回键盘 不闪动<strong>特定部分布局</strong>的效果(如微信聊天界面)。
 * </pre>
 */
public class MyActivity extends Activity {
    MyLineLayout mll_main;
    FrameLayout fl_list;
    LinearLayout ll_edit;
    EditText et_input;
    Button btn_trigger;
    FrameLayout fl_panel;
    Rect rect = new Rect();

    enum State {
        //空状态
        NONE,
        //打开输入法状态
        KEYBOARD,
        //打开面板状态
        PANEL,
    }

    State state = State.NONE;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mll_main = (MyLineLayout) findViewById(R.id.mll_main);
        fl_list = (FrameLayout) findViewById(R.id.fl_list);
        ll_edit = (LinearLayout) findViewById(R.id.ll_edit);
        et_input = (EditText) findViewById(R.id.et_input);
        btn_trigger = (Button) findViewById(R.id.btn_trigger);
        fl_panel = (FrameLayout) findViewById(R.id.fl_panel);
        mll_main.onMeasureListener = new MyLineLayout.OnMeasureListener() {
            /**
             * 可能会发生多次 调用的情况,因为存在 layout_weight 属性,需要2次测试,给定最终大小
             * */
            @Override
            public void onMeasure(int maxHeight, int oldHeight, int nowHeight) {
                switch (state) {
                    case NONE: {

                    }
                    break;
                    case PANEL: {
                        //state 处于 panel 状态只有一种可能,就是主动点击切换到panel,
                        //1.如果之前是keyboard状态,则在本次onMeasure的时候,一定要把panel显示出来
                        //避免 mll 刷动
                        //2. 如果之前处于 none状态,那么本次触发来自于 postDelay,可以忽略
                        fl_panel.setVisibility(View.VISIBLE);
                    }
                    break;
                    case KEYBOARD: {
                        //state = KEYBOARD 状态,只有一种可能,就是主动点击了 EditText
                        //1. 如果之前是panel状态,则一般已经有了固有高度,这个高度刚刚好满足键盘的高度,那么只用隐藏掉
                        //panel 既可以实现页面不进行刷新
                        //2. 如果之前为none状态,则可以忽略
                        fl_panel.setVisibility(View.GONE);
                        //处于键盘状态,需要更新键盘高度为面板的高度
                        if (oldHeight >= nowHeight) {
                            //记录当前的缩放大小为键盘大小
                            int h = maxHeight - nowHeight;
                            //避免 输入法 悬浮状态, 保留一个最低高度
                            if (h < 500) {
                                h = 500;
                            }
                            fl_panel.getLayoutParams().height = h;
                        }
                    }
                    break;
                }
                Log.d("SC_SIZE", String.format("onMeasure %d %d %d", maxHeight, nowHeight, oldHeight));
            }
        };
        fl_list.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                hideSoftInputView();
                fl_panel.setVisibility(View.GONE);
                state = State.NONE;
                return false;
            }
        });
        et_input.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                state = State.KEYBOARD;
            }
        });
        btn_trigger.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                switch (state) {
                    case NONE:
                    case KEYBOARD: {
                        hideSoftInputView();
                        state = State.PANEL;
                        //无论App 处于什么状态,都追加一个 显示 panel 的方法,避免处于非正常状态无法打开panel
                        getWindow().getDecorView().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                fl_panel.setVisibility(View.VISIBLE);
                            }
                        }, 100);
                    }
                    break;
                    case PANEL: {
                        state = State.NONE;
                        fl_panel.setVisibility(View.GONE);
                    }
                    break;
                }
            }
        });
        //设置基本panel 高度,以使得第一次能正常打开panel
        getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
        fl_panel.getLayoutParams().height = rect.height() / 2;
        fl_panel.setVisibility(View.GONE);
    }

    /**
     * 隐藏软键盘输入
     */
    public void hideSoftInputView() {
        InputMethodManager manager = ((InputMethodManager) this.getSystemService(Activity.INPUT_METHOD_SERVICE));

        if (getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
            if (getCurrentFocus() != null && manager != null)
                manager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
        }
    }

    /**
     * Created by Administrator on 2015/11/20.
     */
    public static class MyLineLayout extends LinearLayout {
        OnMeasureListener onMeasureListener;
        int maxHeight = 0;
        int oldHeight;

        public MyLineLayout(Context context) {
            super(context);
        }

        public MyLineLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int height = MeasureSpec.getSize(heightMeasureSpec);
            if (onMeasureListener != null) {
                onMeasureListener.onMeasure(maxHeight, oldHeight, height);
            }
            oldHeight = height;
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            //之所以,在这里记录 maxHeight的大小,是因为 onMeasure 中可能多次调用,中间可能会逐步出现 ActionBar,BottomVirtualKeyboard,
            //所以 onMeasure中获取的maxHeight存在误差
            if (h > maxHeight) {
                maxHeight = h;
            }
            Log.d("SC_SIZE", String.format("Size Change %d %d", h, oldh));
        }

        interface OnMeasureListener {
            void onMeasure(int maxHeight, int oldHeight, int nowHeight);
        }
    }
}

测试效果图:

                      

 项目源码:

    https://git.oschina.net/darkgem/KeyboardSwitch.git

 APK:

    https://git.oschina.net/darkgem/KeyboardSwitch/raw/master/out/production/myapp/myapp.apk

 参考:

    https://github.com/angeldevil/KeyboardAwareDemo

      https://github.com/Jacksgong/JKeyboardPanelSwitch

时间: 2024-10-15 14:49:04

Android弹出键盘布局闪动原理和解决的相关文章

Android软键盘弹出,布局移动

在项目的androidmanifest.xml文件中界面对应的<activity>里加入 android:windowsoftinputmode="adjustpan"这样键盘就会覆盖屏幕.. 如果不想键盘覆盖屏幕,想让屏幕整体上移,就加入属性android:windowsoftinputmode="statevisible|adjustresize" Android软键盘弹出,布局移动,布布扣,bubuko.com

android 弹出的软键盘遮挡住EditText文本框的解决方案

1.android 弹出的软键盘遮挡住EditText文本框的解决方案:把Activit对应的布局文件filename.xml文件里的控件用比重设置布局.(例如:android:layout_weight="31")并且尽可能把高度设置成自适应的:android:layout_height="wrap_content",也就是没有设置高度的控件可压缩度的总和,如果比软键盘的高度要大,在EditText文本输入的时候,弹出的软键盘就不会遮挡住文本输入框.2.设置默认软

关于android中EditText自动获取焦点并弹出键盘的相关设置

在android开发中,关于EditText自动获取焦点弹出键盘,我们可能又是会有让键盘自动弹出的需求,有时可能又会有不想让键盘自动弹出的需求,下面是我所总结的两种方法: 需求:EditText自动获取焦点并弹出键盘,代码: EditText.setFocusable(true); EditText.setFocusableInTouchMode(true); EditText.requestFocus(); 需求:EditText不会自动获取焦点并且不会弹出键盘,代码:  将其父控件设置: P

弹出键盘windowsoftinputmode属性设置值

windowSoftInputMode属性设置值 2012-08-30 16:49 1592人阅读 评论(0) 收藏 举报 androidattributes活动 (1).AndroidManifest.xml文件中界面对应的<activity>里加入            android:windowSoftInputMode="adjustPan"   键盘就会覆盖屏幕            android:windowSoftInputMode="state

IPhone手机页面中点击文本输入框,弹出键盘,网页会放大,如何解决

在head标签中加入以上meta声明.具体属性可以谷歌/百度. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 我查了下viewport,有几个属性:width - viewport的宽度 height - viewport的高度initial-scale - 初始的缩放比例minim

IOS开发之自定义系统弹出键盘上方的view

IOS开发之自定义系统弹出键盘上方的view 分类: IOS 2014-11-18 09:26 1304人阅读 评论(0) 收藏 举报 目录(?)[+] 这篇文章解决的一个开发中的实际问题就是:当弹出键盘时,自定义键盘上方的view.目前就我的经验来看,有两种解决方法.一个就是利用 UITextField或者UITextView的inputAccessoryView属性,另一种,就是监听键盘弹出的notification来自 己解决相关视图的位置问题. 第一种解决方法相对比较简单,第二种的方法中

Android中修改键盘布局或者按键映射时的注意点

在Android中修改键盘布局或者按键映射时,处理在inputdevice中修改上报的SCANCODE之外,还需要修改相应的kl文件. 具体原理: 当一个inputdevice的driver将按键的SCANCODE上报给EventHub之后,EventHub还会用SCANCODE去获取具体的Keycode,这一步就是根据相应的inputdevice的kl文件中的配置得到的,所以如果仅仅上报SCANCODE,不修改kl文件,则会导致SCANCODE是正确的,但是找不到相应的mapkey,从而上报的

经常使用的android弹出对话框

我们在平时做开发的时候,免不了会用到各种各样的对话框,相信有过其它平台开发经验的朋友都会知道,大部分的平台都仅仅提供了几个最简单的实现,假设我们想实现自己特定需求的对话框,大家可能首先会想到,通过继承等方式,重写我们自己的对话框.当然,这也是不失为一个不错的解决方式,可是一般的情况却是这样,我们重写的对话框,或许仅仅在一个特定的地方会用到,为了这一次的使用,而去创建一个新类,往往有点杀鸡用牛刀的感觉,甚至会对我们的程序添加不必要的复杂性,对于这样的情形的对话框有没有更优雅的解决方式呢?    

UI弹出键盘和收回键盘

点击textfield,会自动弹出键盘 要让键盘收回来,先设置个代理:[field setTextFieldDelegate:self];  可设置成自己,也可设置成其他对象,只要在对应的类中,遵循UITextFieldDelegate协议 在UITextFieldDelegate协议中,有一些可选的方法: //点击return回收键盘 - (BOOL)textFieldShouldReturn:(UITextField *)textField{ [textField resignFirstRe