Android输入法与表情面板切换时的界面抖动问题解决方法

昨天琢磨了下Android的输入法弹出模式,突然发现利用动态切换输入法的弹出模式可以解决输入法抖动的问题。具体是怎样的抖动呢?我们先看微博的反面教材。

【具体表现为:表情面板与输入法面板高度不一致,从而导致弹出输入法(layout被挤压)时,同时又需要隐藏表情面板(layout被拉升),最终让界面产生了高度差抖动,所以在切换时明显会有不大好的抖动体验)】

使用了解决抖动的解决方案后,效果如下:

【这样的方案明显比微博的切换更平滑】

老样子,先说思路。主要我们要用到两个输入法弹出模式,分别是:adjustResize(调整模式) 、adjustNothing(不做任何调整) 。(更多介绍请参看我的上一篇文章:输入法弹出参数分析)

1.初始情况时(键盘和表情面板都未展开):我们为表情面板设置一个默认高度(因为我们还不知道键盘有多高)并将输入发弹出模式设置为adjustResize模式。
2.当我们点击了EditText时,系统将会弹出输入法,由于之前我们设置的模式为adjustResize,因此,输入法会挤压Layout,并且挤压的高度最终会固定到一个值(键盘的高度),当我们检测到挤压后,将这个挤压差值(也就是键盘高度)记录下来,作为表情面板的新高度值。于此同时,我们将表情面板隐藏。
3.当我们点击了表情按钮时,我们需要先判断输入法是否已展开。
1)如果已经展开,那么我们的任务是将键盘平滑隐藏并显示表情面板。具体做法为:先将Activity的输入法弹出模式设置为adjustNothing,然后将上一步记录下来的键盘高度作为表情面板的高度,再将表情面板显示,此时由于键盘弹出模式为adjustNothing,所以键盘不会有任何抖动,并且由于表情面板与键盘等高,因此EditText也不会下移,最后将输入法隐藏。
2)如果输入法未展开,我们再判断表情面板是否展开,如果展开了就隐藏并将输入法弹出模式归位为adjustResize,如果未展开就直接显示并将输入法弹出模式设置为adjustNothing。
大致的实现思路就是上面说到的,但是,既然都准备动手做帮助类了,就顺便将点击空白处折叠键盘和表情面板一起做了。具体实现思路为:在Activity的DecorView上面遮罩一层FrameLayout,用于监听触摸的Aciton_Down事件,如果在输入范围之外,则折叠表情面板和键盘。示意图如下:

该说的说完了,开动。

1、创建InputMethodUtils类,构造方法需要传递Activity参数,并申明所需要的成员变量,并实现View.OnClickListener接口(因为我们要监听表情按钮的点击事件)。代码如下:

public class InputMethodUtils implements View.OnClickListener {
  // 键盘是否展开的标志位
  private boolean sIsKeyboardShowing;
  // 键盘高度变量
  private int sKeyBoardHeight = 0;
  // 绑定的Activity
  private Activity activity;
  /**
   * 构造函数
   *
   * @param activity
   *      需要处理输入法的当前的Activity
   */
  public InputMethodUtils(Activity activity) {
    this.activity = activity;
    //DisplayUtils为屏幕尺寸工具类
    DisplayUtils.init(activity);
    // 默认键盘高度为267dp
    setKeyBoardHeight(DisplayUtils.dp2px(267));
  }
  @Override
  public void onClick(View v) {
  }
}

//DisplayUtils的实现代码为:

/**
 * 屏幕参数的辅助工具类。例如:获取屏幕高度,宽度,statusBar的高度,px和dp互相转换等
 * 【注意,使用之前一定要初始化!一次初始化就OK(建议APP启动时进行初始化)。 初始化代码 DisplayUtils.init(context)】
 * @author 蓝亭书序
 */
private static class DisplayUtils {
  // 四舍五入的偏移值
  private static final float ROUND_CEIL = 0.5f;
  // 屏幕矩阵对象
  private static DisplayMetrics sDisplayMetrics;
  // 资源对象(用于获取屏幕矩阵)
  private static Resources sResources;
  // statusBar的高度(由于这里获取statusBar的高度使用的反射,比较耗时,所以用变量记录)
  private static int statusBarHeight = -1;
  /**
   * 初始化操作
   *
   * @param context
   *      context上下文对象
   */
  public static void init(Context context) {
    sDisplayMetrics = context.getResources().getDisplayMetrics();
    sResources = context.getResources();
  }

  /**
   * 获取屏幕高度 单位:像素
   *
   * @return 屏幕高度
   */
  public static int getScreenHeight() {
    return sDisplayMetrics.heightPixels;
  }

  /**
   * 获取屏幕宽度 单位:像素
   *
   * @return 屏幕宽度
   */
  public static float getDensity() {
    return sDisplayMetrics.density;
  }

  /**
   * dp 转 px
   *
   * @param dp
   *      dp值
   * @return 转换后的像素值
   */
  public static int dp2px(int dp) {
    return (int) (dp * getDensity() + ROUND_CEIL);
  }

  /**
   * 获取状态栏高度
   *
   * @return 状态栏高度
   */
  public static int getStatusBarHeight() {
    // 如果之前计算过,直接使用上次的计算结果
    if (statusBarHeight == -1) {
      final int defaultHeightInDp = 19;// statusBar默认19dp的高度
      statusBarHeight = DisplayUtils.dp2px(defaultHeightInDp);
      try {
        Class<?> c = Class.forName("com.android.internal.R$dimen");
        Object obj = c.newInstance();
        Field field = c.getField("status_bar_height");
        statusBarHeight = sResources.getDimensionPixelSize(Integer
            .parseInt(field.get(obj).toString()));
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    return statusBarHeight;
  }
}

【搬砖去了,等会继续写… … 】好了,继续写… …

2、在继续往下写之前,我们得考虑如何设计表情按钮、表情按钮点击事件、表情面板之间的问题。我的做法是创建一个ViewBinder内部类。(因为在逻辑上来说,这三个属于一体的)
ViewBinder的实现代码如下:

/**
 * 用于控制点击某个按钮显示或者隐藏“表情面板”的绑定bean对象。<br/>
 * 例如:我想点击“表情”按钮显示“表情面板”,我就可以这样做:<br/>
 * ViewBinder viewBinder = new ViewBinder(btn_emotion,emotionPanel);<br/>
 * 这样就创建出了一个ViewBinder对象<br/>
 * <font color='red'>【注意事项,使用此类时,千万不要使用trigger的setOnClickListener来监听事件(
 * 使用OnTriggerClickListener来代替),也不要使用setTag来设置Tag,否则会导致使用异常】</font>
 * @author 蓝亭书序
 */
public static class ViewBinder {
  private View trigger;//表情按钮对象
  private View panel;//表情面板对象
  //替代的监听器
  private OnTriggerClickListener listener;

  /**
   * 创建ViewBinder对象<br/>
   * 例如:我想点击“表情”按钮显示“表情面板”,我就可以这样做:<br/>
   * ViewBinder viewBinder = new
   * ViewBinder(btn_emotion,emotionPanel,listener);<br/>
   * 这样就创建出了一个ViewBinder对象
   *
   * @param trigger
   *      触发对象
   * @param panel
   *      点击触发对象需要显示/隐藏的面板对象
   * @param listener
   *      Trigger点击的监听器(千万不要使用setOnClickListener,否则会覆盖本工具类的监听器)
   */
  public ViewBinder(View trigger, View panel,
      OnTriggerClickListener listener) {
    this.trigger = trigger;
    this.panel = panel;
    this.listener = listener;
    trigger.setClickable(true);
  }
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    ViewBinder other = (ViewBinder) obj;
    if (panel == null) {
      if (other.panel != null)
        return false;
    } else if (!panel.equals(other.panel))
      return false;
    if (trigger == null) {
      if (other.trigger != null)
        return false;
    } else if (!trigger.equals(other.trigger))
      return false;
    return true;
  }
  public OnTriggerClickListener getListener() {
    return listener;
  }
  public void setListener(OnTriggerClickListener listener) {
    this.listener = listener;
  }
  public View getTrigger() {
    return trigger;
  }
  public void setTrigger(View trigger) {
    this.trigger = trigger;
  }
  public View getPanel() {
    return panel;
  }
  public void setPanel(View panel) {
    this.panel = panel;
  }
}

其中OnTriggerClickListener是为了解决trigger占用监听器的问题(我们内部逻辑需要占用监听器,如果外部想实现额外的点击逻辑不能再为trigger添加监听器,所以使用OnTriggerClickListener来代替原原声的OnClickListener)。OnTriggerClickListener为一个接口,实现代码如下:

/**
 * ViewBinder的触发按钮点击的监听器
 * @author 蓝亭书序
 */
public static interface OnTriggerClickListener {
  /**
   * 点击事件的回调函数
   * @param v 被点击的按钮对象
   */
  public void onClick(View v);
}

3、实现了ViewBinder后,我们还需要实现一个遮罩View,用于监听ACTION_DOWN事件。代码如下:

/**
 * 点击软键盘区域以外自动关闭软键盘的遮罩View
 * @author 蓝亭书序
 */
private class CloseKeyboardOnOutsideContainer extends FrameLayout {

  public CloseKeyboardOnOutsideContainer(Context context) {
    this(context, null);
  }

  public CloseKeyboardOnOutsideContainer(Context context,
      AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public CloseKeyboardOnOutsideContainer(Context context,
      AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
  }

  /*如果不知道这个方法的作用的话,需要了解下Android的事件分发机制哈,如果有时间我也可以写个文章介绍下。dispatchTouchEvent方法主要是ViewGroup在事件分发之前进行事件进行判断,如果返回true表示此ViewGroup拦截此事件,这个事件将不会传递给他的子View,如果返回false,反之。*/
  @Override
  public boolean dispatchTouchEvent(MotionEvent event) {
  //这段逻辑不复杂,看一遍应该就懂
    boolean isKeyboardShowing = isKeyboardShowing();
    boolean isEmotionPanelShowing = hasPanelShowing();
    if ((isKeyboardShowing || isEmotionPanelShowing)
        && event.getAction() == MotionEvent.ACTION_DOWN) {
      int touchY = (int) (event.getY());
      int touchX = (int) (event.getX());
      if (isTouchKeyboardOutside(touchY)) {
        if (isKeyboardShowing) {
          hideKeyBordAndSetFlag(activity.getCurrentFocus());
        }
        if (isEmotionPanelShowing) {
          closeAllPanels();
        }
      }
      if (isTouchedFoucusView(touchX, touchY)) {
        // 如果点击的是输入框(会弹出输入框),那么延时折叠表情面板
        postDelayed(new Runnable() {
          @Override
          public void run() {
            setKeyboardShowing(true);
          }
        }, 500);
      }
    }
    return super.onTouchEvent(event);
  }
}

/**
 * 是否点击软键盘和输入法外面区域
 * @param activity
 *      当前activity
 * @param touchY
 *      点击y坐标(不包括statusBar的高度)
 */
private boolean isTouchKeyboardOutside(int touchY) {
  View foucusView = activity.getCurrentFocus();
  if (foucusView == null) {
    return false;
  }
  int[] location = new int[2];
  foucusView.getLocationOnScreen(location);
  int editY = location[1] - DisplayUtils.getStatusBarHeight();
  int offset = touchY - editY;
  if (offset > 0 && offset < foucusView.getMeasuredHeight()) {
    return false;
  }
  return true;
}
/**
 * 是否点击的是当前焦点View的范围
 * @param x
 *      x方向坐标
 * @param y
 *      y方向坐标(不包括statusBar的高度)
 * @return true表示点击的焦点View,false反之
 */
private boolean isTouchedFoucusView(int x, int y) {
  View foucusView = activity.getCurrentFocus();
  if (foucusView == null) {
    return false;
  }
  int[] location = new int[2];
  foucusView.getLocationOnScreen(location);
  int foucusViewTop = location[1] - DisplayUtils.getStatusBarHeight();
  int offsetY = y - foucusViewTop;
  if (offsetY > 0 && offsetY < foucusView.getMeasuredHeight()) {
    int foucusViewLeft = location[0];
    int foucusViewLength = foucusView.getWidth();
    int offsetX = x - foucusViewLeft;
    if (offsetX >= 0 && offsetX <= foucusViewLength) {
      return true;
    }
  }
  return false;
}

4、准备工作做完,我们可以继续完善InputMethodUtils类了,由于我们需要存储ViewBinder对象(主要用于控制按钮和面板之间的关联关系),所以,我们还需要在InputMethodUtils中申明一个集合。代码如下:

// 触发与面板对象集合(使用set可以自动过滤相同的ViewBinder)
private Set<ViewBinder> viewBinders = new HashSet<ViewBinder>();

5、与viewBinders 随之而来的一些常用方法有必要写一下(例如折叠所有表情面板、获取当前哪个表情面板展开着等),代码如下:

/**
 * 添加ViewBinder
 * @param viewBinder
 *      变长参数
 */
public void setViewBinders(ViewBinder... viewBinder) {
  for (ViewBinder vBinder : viewBinder) {
    if (vBinder != null) {
      viewBinders.add(vBinder);
      vBinder.trigger.setTag(vBinder);
      vBinder.trigger.setOnClickListener(this);
    }
  }
  updateAllPanelHeight(sKeyBoardHeight);
}
/**
 * 重置所有面板
 * @param dstPanel
 *      重置操作例外的对象
 */
private void resetOtherPanels(View dstPanel) {
  for (ViewBinder vBinder : viewBinders) {
    if (dstPanel != vBinder.panel) {
      vBinder.panel.setVisibility(View.GONE);
    }
  }
}
/**
 * 关闭所有的面板
 */
public void closeAllPanels() {
  resetOtherPanels(null);
  //重置面板后,需要将输入法弹出模式一并重置
  updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
/**
 * 判断是否存在正在显示的面板
 * @return true表示存在,false表示不存在
 */
public boolean hasPanelShowing() {
  for (ViewBinder viewBinder : viewBinders) {
    if (viewBinder.panel.isShown()) {
      return true;
    }
  }
  return false;
}
/**
 * 更新所有面板的高度
 * @param height
 *      具体高度(单位px)
 */
private void updateAllPanelHeight(int height) {
  for (ViewBinder vBinder : viewBinders) {
    ViewGroup.LayoutParams params = vBinder.panel.getLayoutParams();
    params.height = height;
    vBinder.panel.setLayoutParams(params);
  }
}

6、通过监听Layout的变化来判断输入法是否已经展开。代码如下:

/**
 * 设置View树监听,以便判断键盘是否弹出。<br/>
 * 【只有当Activity的windowSoftInputMode设置为adjustResize时才有效!所以我们要处理adjustNoting(不会引起Layout的形变)的情况键盘监听(后文会提到)】
 */
private void detectKeyboard() {
  final View activityRootView = ((ViewGroup) activity
      .findViewById(android.R.id.content)).getChildAt(0);
  if (activityRootView != null) {
    ViewTreeObserver observer = activityRootView.getViewTreeObserver();
    if (observer == null) {
      return;
    }
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {
        final Rect r = new Rect();
        activityRootView.getWindowVisibleDisplayFrame(r);
        int heightDiff = DisplayUtils.getScreenHeight()
            - (r.bottom - r.top);
        //Layout形变超过键盘的一半表示键盘已经展开了
        boolean show = heightDiff >= sKeyBoardHeight / 2;
        setKeyboardShowing(show);// 设置键盘是否展开状态
        if (show) {
          int keyboardHeight = heightDiff
              - DisplayUtils.getStatusBarHeight();
          // 设置新的键盘高度
          setKeyBoardHeight(keyboardHeight);
        }
      }
    });
  }
}

7、完成键盘的显示/隐藏和动态控制输入法弹出模式的常用方法。代码如下:

/**
 * 隐藏输入法
 * @param currentFocusView
 *      当前焦点view
 */
public static void hideKeyboard(View currentFocusView) {
  if (currentFocusView != null) {
    IBinder token = currentFocusView.getWindowToken();
    if (token != null) {
      InputMethodManager im = (InputMethodManager) currentFocusView
          .getContext().getSystemService(
              Context.INPUT_METHOD_SERVICE);
      im.hideSoftInputFromWindow(token, 0);
    }
  }
}
/**
 * 更新输入法的弹出模式(注意这是静态方法,可以直接当做工具方法使用)
 * @param activity 对应的Activity
 * @param softInputMode
 * <br/>
 *      键盘弹出模式:WindowManager.LayoutParams的参数有:<br/>
 *          可见状态: SOFT_INPUT_STATE_UNSPECIFIED,
 *      SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
 *      SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
 *          适配选项有: SOFT_INPUT_ADJUST_UNSPECIFIED,
 *      SOFT_INPUT_ADJUST_RESIZE, or SOFT_INPUT_ADJUST_PAN.
 */
public static void updateSoftInputMethod(Activity activity,
    int softInputMode) {
  if (!activity.isFinishing()) {
    WindowManager.LayoutParams params = activity.getWindow()
        .getAttributes();
    if (params.softInputMode != softInputMode) {
      params.softInputMode = softInputMode;
      activity.getWindow().setAttributes(params);
    }
  }
}

/**
 * 更新输入法的弹出模式(遇上面的静态方法的区别是直接使用的是绑定的activity对象)
 *
 * @param softInputMode
 * <br/>
 *      键盘弹出模式:WindowManager.LayoutParams的参数有:<br/>
 *          可见状态: SOFT_INPUT_STATE_UNSPECIFIED,
 *      SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
 *      SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
 *          适配选项有: SOFT_INPUT_ADJUST_UNSPECIFIED,
 *      SOFT_INPUT_ADJUST_RESIZE, or SOFT_INPUT_ADJUST_PAN.
 */
public void updateSoftInputMethod(int softInputMode) {
  updateSoftInputMethod(activity, softInputMode);
}

8、在构造方法中将这些组件都初始化,并做相关设置,代码如下:

/**
 * 构造函数
 *
 * @param activity
 *      需要处理输入法的当前的Activity
 */
public InputMethodUtils(Activity activity) {
  this.activity = activity;
  DisplayUtils.init(activity);
  // 默认键盘高度为267dp
  setKeyBoardHeight(DisplayUtils.dp2px(267));
  updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
  detectKeyboard();// 监听View树变化,以便监听键盘是否弹出
  enableCloseKeyboardOnTouchOutside(activity);
}

/**
 1. 设置键盘的高度
 2.
 3. @param keyBoardHeight
 4.      键盘的高度(px单位)
 */
private void setKeyBoardHeight(int keyBoardHeight) {
  sKeyBoardHeight = keyBoardHeight;
  updateAllPanelHeight(keyBoardHeight);
}
/**
 5. 开启点击外部关闭键盘的功能(其实就是将遮罩View添加到Decorview)
 6.
 7. @param activity
 */
private void enableCloseKeyboardOnTouchOutside(Activity activity) {
  CloseKeyboardOnOutsideContainer frameLayout = new CloseKeyboardOnOutsideContainer(
      activity);
  activity.addContentView(frameLayout, new ViewGroup.LayoutParams(
      ViewGroup.LayoutParams.MATCH_PARENT,
      ViewGroup.LayoutParams.MATCH_PARENT));
}

【突然有事,先写到这,等会来完善…】回来了,接着写。
上面的代码基本完成需求,需要重点说的是如何检测键盘弹出/隐藏状态的问题(有人可能会说用InputMethodManager.isActive()啊,恩…反正我用有这个方法问题,他永远都给我返回true),下面简单介绍下如何实现的键盘的弹出和隐藏状态的检测。

1、如果当前输入法是adjustResize模式,那么我们直接可以用Layout的形变监听即可实现,也就是之前detectKeyboard()实现的代码。

2、如果当前输入法是adjustNoting模式,这个就有点难处理了,因为没有形变可以监听。我的实现方式是:通过遮罩View判断ACTION_DOWN的坐标,如果该坐标落在输入框内(就是用户点击了输入框,此时系统将会弹出输入框),那么我们就可以认为键盘为弹出模式。代码体现在CloseKeyboardOnOutsideContainer的dispatchTouchEvent()方法中。

到此,开发就告一段落了。按照惯例,完整代码如下:

/**
 * 解决输入法与表情面板之间切换时抖动冲突的控制辅助工具类(能做到将面板与输入法之间平滑切换).另外,具备点击空白处自动收起面板和输入法的功能.<br/>
 * 使用方法介绍如下:
 * <hr/>
 * <font color= 'red'>申明:【此类中,我们将表情面板选项、显示表情面板的按钮、表情面板按钮的点击事件
 * 作为一个整体,包装在ViewBinder类中(点击表情面板按钮时,将会展开表情面 板 ) 】</font> <br/>
 * 因此,第一步,我们将需要操作的表情面板、按钮、事件绑定在一起,创建ViewBinder类(可以是很多个)代码示例如下:<br/>
 * //如果不想监听按钮点击事件,之间将listener参数替换成null即可<br/>
 * ViewBinder viewBinder1 = new ViewBinder(btn_1,panel1,listener1);<br/>
 * ViewBinder viewBinder2 = new ViewBinder(btn_2,panel2,listener2);<br/>
 * ...<br/>
 * 第二步:创建InputMethodUtils类<br/>
 * InputMethodUtils inputMethodUtils = new InputMethodUtils(this);<br/>
 * 第三部:将ViewBinder传递给InputMethodUtils。<br/>
 * inputMethodUtils.setViewBinders(viewBinder1,viewBinder2);//这个参数为动态参数,
 * 支持多个参数传递进来
 * <hr/>
 * 本类还提供两个常用的工具方法:<br/>
 * InputMethodUtils.hideKeyboard();//用于隐藏输入法<br/>
 * InputMethodUtils.updateSoftInputMethod();//用于将当前Activity的输入法模式切换成指定的输入法模式
 * <br/>
 *
 * @author 李长军 2016.11.26
 */
public class InputMethodUtils implements View.OnClickListener {

  // 键盘是否展开的标志位
  private boolean sIsKeyboardShowing;
  // 键盘高度
  private int sKeyBoardHeight = 0;
  // 绑定的Activity
  private Activity activity;
  // 触发与面板对象集合
  private Set<ViewBinder> viewBinders = new HashSet<ViewBinder>();

  /**
   * 构造函数
   *
   * @param activity
   *      需要处理输入法的当前的Activity
   */
  public InputMethodUtils(Activity activity) {
    this.activity = activity;
    DisplayUtils.init(activity);
    // 默认键盘高度为267dp
    setKeyBoardHeight(DisplayUtils.dp2px(267));
    updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
    detectKeyboard();// 监听View树变化,以便监听键盘是否弹出
    enableCloseKeyboardOnTouchOutside(activity);
  }

  /**
   * 添加ViewBinder
   *
   * @param viewBinder
   *      变长参数
   */
  public void setViewBinders(ViewBinder... viewBinder) {
    for (ViewBinder vBinder : viewBinder) {
      if (vBinder != null) {
        viewBinders.add(vBinder);
        vBinder.trigger.setTag(vBinder);
        vBinder.trigger.setOnClickListener(this);
      }
    }
    updateAllPanelHeight(sKeyBoardHeight);
  }

  @Override
  public void onClick(View v) {
    ViewBinder viewBinder = (ViewBinder) v.getTag();
    View panel = viewBinder.panel;
    resetOtherPanels(panel);// 重置所有面板
    if (isKeyboardShowing()) {
      updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
      panel.setVisibility(View.VISIBLE);
      hideKeyBordAndSetFlag(activity.getCurrentFocus());
    } else if (panel.isShown()) {
      updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
      panel.setVisibility(View.GONE);
    } else {
      updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
      panel.setVisibility(View.VISIBLE);
    }
    if (viewBinder.listener != null) {
      viewBinder.listener.onClick(v);
    }
  }

  /**
   * 获取键盘是否弹出
   *
   * @return true表示弹出
   */
  public boolean isKeyboardShowing() {
    return sIsKeyboardShowing;
  }

  /**
   * 获取键盘的高度
   *
   * @return 键盘的高度(px单位)
   */
  public int getKeyBoardHeight() {
    return sKeyBoardHeight;
  }

  /**
   * 关闭所有的面板
   */
  public void closeAllPanels() {
    resetOtherPanels(null);
    updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
  }

  /**
   * 判断是否存在正在显示的面板
   *
   * @return true表示存在,false表示不存在
   */
  public boolean hasPanelShowing() {
    for (ViewBinder viewBinder : viewBinders) {
      if (viewBinder.panel.isShown()) {
        return true;
      }
    }
    return false;
  }

  /**
   * 更新输入法的弹出模式
   *
   * @param softInputMode
   * <br/>
   *      键盘弹出模式:WindowManager.LayoutParams的参数有:<br/>
   *          可见状态: SOFT_INPUT_STATE_UNSPECIFIED,
   *      SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
   *      SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
   *          适配选项有: SOFT_INPUT_ADJUST_UNSPECIFIED,
   *      SOFT_INPUT_ADJUST_RESIZE, or SOFT_INPUT_ADJUST_PAN.
   */
  public void updateSoftInputMethod(int softInputMode) {
    updateSoftInputMethod(activity, softInputMode);
  }

  /**
   * 隐藏输入法
   *
   * @param currentFocusView
   *      当前焦点view
   */
  public static void hideKeyboard(View currentFocusView) {
    if (currentFocusView != null) {
      IBinder token = currentFocusView.getWindowToken();
      if (token != null) {
        InputMethodManager im = (InputMethodManager) currentFocusView
            .getContext().getSystemService(
                Context.INPUT_METHOD_SERVICE);
        im.hideSoftInputFromWindow(token, 0);
      }
    }
  }

  /**
   * 更新输入法的弹出模式
   *
   * @param activity
   * @param softInputMode
   * <br/>
   *      键盘弹出模式:WindowManager.LayoutParams的参数有:<br/>
   *          可见状态: SOFT_INPUT_STATE_UNSPECIFIED,
   *      SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
   *      SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
   *          适配选项有: SOFT_INPUT_ADJUST_UNSPECIFIED,
   *      SOFT_INPUT_ADJUST_RESIZE, or SOFT_INPUT_ADJUST_PAN.
   */
  public static void updateSoftInputMethod(Activity activity,
      int softInputMode) {
    if (!activity.isFinishing()) {
      WindowManager.LayoutParams params = activity.getWindow()
          .getAttributes();
      if (params.softInputMode != softInputMode) {
        params.softInputMode = softInputMode;
        activity.getWindow().setAttributes(params);
      }
    }
  }

  /**
   * 隐藏键盘,并维护显示或不显示的逻辑
   *
   * @param currentFocusView
   *      当前的焦点View
   */
  private void hideKeyBordAndSetFlag(View currentFocusView) {
    sIsKeyboardShowing = false;
    hideKeyboard(currentFocusView);
  }

  /**
   * 重置所有面板
   */
  private void resetOtherPanels(View dstPanel) {
    for (ViewBinder vBinder : viewBinders) {
      if (dstPanel != vBinder.panel) {
        vBinder.panel.setVisibility(View.GONE);
      }
    }
  }

  /**
   * 更新所有面板的高度
   *
   * @param height
   *      具体高度
   */
  private void updateAllPanelHeight(int height) {
    for (ViewBinder vBinder : viewBinders) {
      ViewGroup.LayoutParams params = vBinder.panel.getLayoutParams();
      params.height = height;
      vBinder.panel.setLayoutParams(params);
    }
  }

  /**
   * 设置键盘弹出与否状态
   *
   * @param show
   *      true表示弹出,false表示未弹出
   */
  private void setKeyboardShowing(boolean show) {
    sIsKeyboardShowing = show;
    if (show) {
      resetOtherPanels(null);
      updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
    }
  }

  /**
   * 设置键盘的高度
   *
   * @param keyBoardHeight
   *      键盘的高度(px单位)
   */
  private void setKeyBoardHeight(int keyBoardHeight) {
    sKeyBoardHeight = keyBoardHeight;
    updateAllPanelHeight(keyBoardHeight);
  }

  /**
   * 是否点击软键盘和输入法外面区域
   *
   * @param activity
   *      当前activity
   * @param touchY
   *      点击y坐标(不包括statusBar的高度)
   */
  private boolean isTouchKeyboardOutside(int touchY) {
    View foucusView = activity.getCurrentFocus();
    if (foucusView == null) {
      return false;
    }
    int[] location = new int[2];
    foucusView.getLocationOnScreen(location);
    int editY = location[1] - DisplayUtils.getStatusBarHeight();
    int offset = touchY - editY;
    if (offset > 0 && offset < foucusView.getMeasuredHeight()) {
      return false;
    }
    return true;
  }

  /**
   * 是否点击的是当前焦点View的范围
   *
   * @param x
   *      x方向坐标
   * @param y
   *      y方向坐标(不包括statusBar的高度)
   * @return true表示点击的焦点View,false反之
   */
  private boolean isTouchedFoucusView(int x, int y) {
    View foucusView = activity.getCurrentFocus();
    if (foucusView == null) {
      return false;
    }
    int[] location = new int[2];
    foucusView.getLocationOnScreen(location);
    int foucusViewTop = location[1] - DisplayUtils.getStatusBarHeight();
    int offsetY = y - foucusViewTop;
    if (offsetY > 0 && offsetY < foucusView.getMeasuredHeight()) {
      int foucusViewLeft = location[0];
      int foucusViewLength = foucusView.getWidth();
      int offsetX = x - foucusViewLeft;
      if (offsetX >= 0 && offsetX <= foucusViewLength) {
        return true;
      }
    }
    return false;
  }

  /**
   * 开启点击外部关闭键盘的功能
   *
   * @param activity
   */
  private void enableCloseKeyboardOnTouchOutside(Activity activity) {
    CloseKeyboardOnOutsideContainer frameLayout = new CloseKeyboardOnOutsideContainer(
        activity);
    activity.addContentView(frameLayout, new ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT));
  }

  /**
   * 设置View树监听,以便判断键盘是否弹出。<br/>
   * 【只有当Activity的windowSoftInputMode设置为adjustResize时才有效】
   */
  private void detectKeyboard() {
    final View activityRootView = ((ViewGroup) activity
        .findViewById(android.R.id.content)).getChildAt(0);
    if (activityRootView != null) {
      ViewTreeObserver observer = activityRootView.getViewTreeObserver();
      if (observer == null) {
        return;
      }
      observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
          final Rect r = new Rect();
          activityRootView.getWindowVisibleDisplayFrame(r);
          int heightDiff = DisplayUtils.getScreenHeight()
              - (r.bottom - r.top);
          boolean show = heightDiff >= sKeyBoardHeight / 2;
          setKeyboardShowing(show);// 设置键盘是否展开状态
          if (show) {
            int keyboardHeight = heightDiff
                - DisplayUtils.getStatusBarHeight();
            // 设置新的键盘高度
            setKeyBoardHeight(keyboardHeight);
          }
        }
      });
    }
  }

  /**
   * ViewBinder的触发按钮点击的监听器
   *
   * @author 李长军
   *
   */
  public static interface OnTriggerClickListener {
    /**
     *
     * @param v
     */
    public void onClick(View v);
  }

  /**
   * 用于控制点击某个按钮显示或者隐藏“表情面板”的绑定bean对象。<br/>
   * 例如:我想点击“表情”按钮显示“表情面板”,我就可以这样做:<br/>
   * ViewBinder viewBinder = new ViewBinder(btn_emotion,emotionPanel);<br/>
   * 这样就创建出了一个ViewBinder对象<br/>
   * <font color='red'>【注意事项,使用此类时,千万不要使用trigger的setOnClickListener来监听事件(
   * 使用OnTriggerClickListener来代替),也不要使用setTag来设置Tag,否则会导致使用异常】</font>
   *
   * @author 李长军
   *
   */
  public static class ViewBinder {
    private View trigger;
    private View panel;
    private OnTriggerClickListener listener;

    /**
     * 创建ViewBinder对象<br/>
     * 例如:我想点击“表情”按钮显示“表情面板”,我就可以这样做:<br/>
     * ViewBinder viewBinder = new
     * ViewBinder(btn_emotion,emotionPanel,listener);<br/>
     * 这样就创建出了一个ViewBinder对象
     *
     * @param trigger
     *      触发对象
     * @param panel
     *      点击触发对象需要显示/隐藏的面板对象
     * @param listener
     *      Trigger点击的监听器(千万不要使用setOnClickListener,否则会覆盖本工具类的监听器)
     */
    public ViewBinder(View trigger, View panel,
        OnTriggerClickListener listener) {
      this.trigger = trigger;
      this.panel = panel;
      this.listener = listener;
      trigger.setClickable(true);
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      ViewBinder other = (ViewBinder) obj;
      if (panel == null) {
        if (other.panel != null)
          return false;
      } else if (!panel.equals(other.panel))
        return false;
      if (trigger == null) {
        if (other.trigger != null)
          return false;
      } else if (!trigger.equals(other.trigger))
        return false;
      return true;
    }

    public OnTriggerClickListener getListener() {
      return listener;
    }

    public void setListener(OnTriggerClickListener listener) {
      this.listener = listener;
    }

    public View getTrigger() {
      return trigger;
    }

    public void setTrigger(View trigger) {
      this.trigger = trigger;
    }

    public View getPanel() {
      return panel;
    }

    public void setPanel(View panel) {
      this.panel = panel;
    }

  }

  /**
   * 点击软键盘区域以外自动关闭软键盘的遮罩View
   *
   * @author 李长军
   */
  private class CloseKeyboardOnOutsideContainer extends FrameLayout {

    public CloseKeyboardOnOutsideContainer(Context context) {
      this(context, null);
    }

    public CloseKeyboardOnOutsideContainer(Context context,
        AttributeSet attrs) {
      this(context, attrs, 0);
    }

    public CloseKeyboardOnOutsideContainer(Context context,
        AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
      boolean isKeyboardShowing = isKeyboardShowing();
      boolean isEmotionPanelShowing = hasPanelShowing();
      if ((isKeyboardShowing || isEmotionPanelShowing)
          && event.getAction() == MotionEvent.ACTION_DOWN) {
        int touchY = (int) (event.getY());
        int touchX = (int) (event.getX());
        if (isTouchKeyboardOutside(touchY)) {
          if (isKeyboardShowing) {
            hideKeyBordAndSetFlag(activity.getCurrentFocus());
          }
          if (isEmotionPanelShowing) {
            closeAllPanels();
          }
        }
        if (isTouchedFoucusView(touchX, touchY)) {
          // 如果点击的是输入框,那么延时折叠表情面板
          postDelayed(new Runnable() {
            @Override
            public void run() {
              setKeyboardShowing(true);
            }
          }, 500);
        }
      }
      return super.onTouchEvent(event);
    }
  }

  /**
   * 屏幕参数的辅助工具类。例如:获取屏幕高度,宽度,statusBar的高度,px和dp互相转换等
   * 【注意,使用之前一定要初始化!一次初始化就OK(建议APP启动时进行初始化)。 初始化代码 DisplayUtils.init(context)】
   *
   * @author 李长军 2016.11.25
   */
  private static class DisplayUtils {

    // 四舍五入的偏移值
    private static final float ROUND_CEIL = 0.5f;
    // 屏幕矩阵对象
    private static DisplayMetrics sDisplayMetrics;
    // 资源对象(用于获取屏幕矩阵)
    private static Resources sResources;
    // statusBar的高度(由于这里获取statusBar的高度使用的反射,比较耗时,所以用变量记录)
    private static int statusBarHeight = -1;

    /**
     * 初始化操作
     *
     * @param context
     *      context上下文对象
     */
    public static void init(Context context) {
      sDisplayMetrics = context.getResources().getDisplayMetrics();
      sResources = context.getResources();
    }

    /**
     * 获取屏幕高度 单位:像素
     *
     * @return 屏幕高度
     */
    public static int getScreenHeight() {
      return sDisplayMetrics.heightPixels;
    }

    /**
     * 获取屏幕宽度 单位:像素
     *
     * @return 屏幕宽度
     */
    public static float getDensity() {
      return sDisplayMetrics.density;
    }

    /**
     * dp 转 px
     *
     * @param dp
     *      dp值
     * @return 转换后的像素值
     */
    public static int dp2px(int dp) {
      return (int) (dp * getDensity() + ROUND_CEIL);
    }

    /**
     * 获取状态栏高度
     *
     * @return 状态栏高度
     */
    public static int getStatusBarHeight() {
      // 如果之前计算过,直接使用上次的计算结果
      if (statusBarHeight == -1) {
        final int defaultHeightInDp = 19;// statusBar默认19dp的高度
        statusBarHeight = DisplayUtils.dp2px(defaultHeightInDp);
        try {
          Class<?> c = Class.forName("com.android.internal.R$dimen");
          Object obj = c.newInstance();
          Field field = c.getField("status_bar_height");
          statusBarHeight = sResources.getDimensionPixelSize(Integer
              .parseInt(field.get(obj).toString()));
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
      return statusBarHeight;
    }
  }

}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android开发软键盘遮挡登陆按钮的完美解决方案

    在应用登陆页面我们需要填写用户名和密码.当填写这些信息的时候,软键盘会遮挡登陆按钮,这使得用户体验较差,所以今天就来解决这个问题 1:登陆布局界面如下 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="

  • Android判断软键盘弹出并隐藏的简单完美解决方法(推荐)

    最近项目中有一个编辑框,下面是个ListView.在触发编辑框弹出软键盘后,ListView还能滑动,并且ListView的item还能响应单击.这样的体验效果很不好.于是便想在滑动或单击item时判断键盘是否弹出,若弹出,则把它隐藏. 网上一搜,发现Android并没有直接提供软键盘的弹出与隐藏判断,一些解决方案诸如判断父控件的高度或者判断 if(getWindow().getAttributes().softInputMode==WindowManager.LayoutParams.SOFT

  • Android 表情面板和软键盘切换时跳闪问题的解决方法

    现在很多应用都会在让用户输入各种文本信息的时候同时多提供一个表情面板,这样就会出现一个问题,即表情面板的跳闪问题要输入文本信息,那固然是需要弹出软键盘,在软键盘显示的情况下,此时如果要切换显示出表情面板,由于表情面板不可能和用户的软键盘高度恰好一样,此外由于控件的上下移位,就会出现表情面板的跳闪现象 在点击切换按钮的时候,表情面板会先向上跳,然后再往下移,这样就会带来很差的用户体验,效果如下图所示: 这里提供一个解决方案,使软键盘和表情面板可以很自然地切换,效果如下图所示: 解决思路主要是这样:

  • Android 软键盘弹出时把原来布局顶上去的解决方法

    键盘弹出时,会将布局底部的导航条顶上去. 解决办法: 在mainfest.xml中,在和导航栏相关的activity中加: <activity android:name=".filing.MainActivity" android:windowSoftInputMode="adjustResize|stateHidden" /> windowSoftInputMode 属性解释: 活动的主窗口如何与包含屏幕上的软键盘窗口交互.这个属性的设置将会影响两件事

  • Android软键盘弹出时的界面控制方法

    本文实例讲述了Android软键盘弹出时的界面控制方法.分享给大家供大家参考,具体如下: 有时候androidactivity弹出软键盘后布局改变 下面有三种模式可以改变软键盘弹出以后的显示形式 模式一:压缩模式软键盘弹出以后,会压缩原先的大小 我们可以在AndroidManifet.xml中对Activity进行设置.如: android:windowSoftInputMode="stateUnchanged|adjustResize" 模式二:平移模式 软键盘弹出以后,不会压缩原先

  • Android判断软键盘的状态和隐藏软键盘的简单实例

    之前本人也遇到一个关于获取软键盘的状态的问题,在网上找了很多资料,基本上回答都是用getWindow().getAttributes().softInputMode==WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED来判断软键盘是否打开,若相等则为打开,然后你就可以根据这段代码进行后续操作了.但是我试了好久,不管是软键盘弹出还是关闭getWindow().getAttributes().softInputMode的值一直是0,至于为什

  • Android 点击屏幕空白处收起输入法软键盘(手动打开)

    很多时候,我们在使用应用时,会出现输入法软键盘弹出的问题,通常情况下,我们默认会使用户点击返回键或者下一步对软键盘进行隐藏.为了更好的体验,我们可以实现当用户使用完毕软键盘时.点击屏幕空白区域即可实现收起输入法软键盘功能.下面给大家介绍下实现方法. 1.//隐藏软键盘   在Java文件: InputMethodManager m = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); m .

  • Android输入法与表情面板切换时的界面抖动问题解决方法

    昨天琢磨了下Android的输入法弹出模式,突然发现利用动态切换输入法的弹出模式可以解决输入法抖动的问题.具体是怎样的抖动呢?我们先看微博的反面教材. [具体表现为:表情面板与输入法面板高度不一致,从而导致弹出输入法(layout被挤压)时,同时又需要隐藏表情面板(layout被拉升),最终让界面产生了高度差抖动,所以在切换时明显会有不大好的抖动体验)] 使用了解决抖动的解决方案后,效果如下: [这样的方案明显比微博的切换更平滑] 老样子,先说思路.主要我们要用到两个输入法弹出模式,分别是:ad

  • Android编程实现横竖屏切换时不销毁当前activity和锁定屏幕的方法

    本文实例讲述了Android编程实现横竖屏切换时不销毁当前activity和锁定屏幕的方法.分享给大家供大家参考,具体如下: 首先在Mainifest.xml的Activity元素中加入android:configChanges="orientation|keyboardHidden"属性 <activityandroid:name=".FileBrowser"android:label="@string/app_name"android:

  • Android Fragment中使用SurfaceView切换时闪一下黑屏的解决办法

    重构了下之前自己的一个新闻客户端,全部使用了Fragment来进行页面切换,只有一个入口Activity作为程序的启动Activity,其中有一个界面需要调用摄像头识别二维码,于是就会用到SurfaceView进行预览,那么问题来了,当切换到对应的Fragment时,屏幕会黑一下,黑了1秒左右就显示出正常的界面,而且这种现象只有第一次进入该Fragment才会出现,之后进入都不会出现,解决方法是无意在github上看到了,试了一下,可以行的通,下面贴出解决方法. 方法一.在Activity的on

  • vue使用keep-alive实现组件切换时保存原组件数据方法

    前言 最近在做一个精品课程后台管理系统,其中涉及文件上传和文件列表展示,我不想将他们写入一个组件,故分开两个组件实现. 问题:但由于上传文件需要时间,这时要是用户切换别的组件查看时,上传文件组件就销毁了,导致文件上传失败. 追求效果:想利用keep-alive实现上传组件切换时仍继续上传文件,而其他组件则不会存活. 使用keep-alive的过程 普通方法:直接使用keep-alive <keep-alive> <router-view /> </keep-alive>

  • mysql 登录时闪退的问题解决方法

    mysql 登录时闪退的问题解决方法 之前mysql用着好着,可是今天在启动mysql后输入密码出现了闪退,在任务管理器中发现mysql服务没有启动,当手动启动时提示拒绝访问.在网上查找原因发现问题所在. 问题原因:mysql服务没有安装. 解决办法: 在cmd操作下找到mysql的安装目录(注意要用管理员身份运行cmd) 在 mysql bin目录下 以管理员的权限 执行 mysqld -install命令 然后仍然以管理员的权限 net start mysql 开启Mysql服务了. 输入m

  • Android Studio升级3.6 Build窗口出现中文乱码问题解决方法

    前言 最近受疫情影响,很多互联网企业目前才开始慢慢复工,希望这次的疫情没有影响我们码代码的心情. 问题 好的一点,我们的AndroidStudio迎难而上,最近发布了3.6最新版本,不得不说最新版本还是更新了很多地方,但也有出现了一些问题,比如我最近就碰到了这么一个问题,在我升级到了Android Studio3.6以后发现,编译的时候在底部出现的Build窗口中文出现了乱码的问题,代码中没有出现乱码情况,如下图: 这种问题就很头疼了,我先是在File->Settings->File Enco

  • android实现在横竖屏切换时页面信息不被重置的示例分享

    当屏幕转动切换的时候 Android 机制是:销毁当前屏幕的 Activity ,然后重新开启一个新的适应屏幕改变的 Activity .那么,我们该如何在屏幕切换的时候页面信息不被重置呢? 解决实现:1.在 AnroidMainifest.xml 的 activity 元素中加入: 复制代码 代码如下: android:configChanges="orientation|keyboardHidden" 或 复制代码 代码如下: android:configChanges="

  • Android中的sqlite查询数据时去掉重复值的方法实例

    1.方式一: /** * 参数一:是否去重 * 参数二:表名 * 参数三:columns 表示查询的字段,new String[]{MODEL}表示查询该表当中的模式(也表示查询的结果) * 参数思:selection表示查询的条件,PHONE_NUMBER+" = ?" 表示根据手机号去查询模式 * 参数五:selectionArgs 表示查询条件对应的值,new String[]{phoneNumber}表示查询条件对应的值 * 参数六:String groupBy 分组 * 参数

  • spring+springmvc整合mabytis时mapper注入失败问题解决方法

    好久不搭架构,今天使用spring+springmvc整合mabytis的时候,坑爹的两个问题发生了. 花了一晚上解决这两个问题.现在将错误经验总结分享下,避免以后遇到坑继续往里跳. 1. 单元测试的时候,单元测试失败报错: A ServletContext is required to configure default servlet handling 十月 28, 2016 9:03:33 下午 org.springframework.context.support.GenericAppl

  • Android禁止横屏竖屏切换的有效方法

    在Android中要让一个程序的界面始终保持一个方向,不随手机方向转动而变化的办法: 只要在AndroidManifest.xml里面配置一下就可以了. 在AndroidManifest.xml的activity(需要禁止转向的activity)配置中加入android:screenOrientation="landscape"属性即可(landscape是横向,portrait是纵向).例如: <?xml version="1.0" encoding=&qu

随机推荐