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
-3
发布时间 2016-12-09 14:47:10
0 条回复(回复会通过微信通知作者)
点击加载更多评论
登录 后再进行评论
(微信扫码即可登录,无需注册)