【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;
    // 記錄是否獲取了鍵盤高度
    private boolean haskownKeyboardHeight = false;
    // 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) {
        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/>
 *            &nbsp;&nbsp;&nbsp;&nbsp;可見狀態: SOFT_INPUT_STATE_UNSPECIFIED,
 *            SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
 *            SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
 *            &nbsp;&nbsp;&nbsp;&nbsp;適配選項有: 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/>
 *            &nbsp;&nbsp;&nbsp;&nbsp;可見狀態: SOFT_INPUT_STATE_UNSPECIFIED,
 *            SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
 *            SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
 *            &nbsp;&nbsp;&nbsp;&nbsp;適配選項有: 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 boolean haskownKeyboardHeight = false;
    // 觸發與面板對象集合
    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 if (haskownKeyboardHeight) {
            updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
            panel.setVisibility(View.VISIBLE);
        } 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/>
     *            &nbsp;&nbsp;&nbsp;&nbsp;可見狀態: SOFT_INPUT_STATE_UNSPECIFIED,
     *            SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
     *            SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
     *            &nbsp;&nbsp;&nbsp;&nbsp;適配選項有: 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/>
     *            &nbsp;&nbsp;&nbsp;&nbsp;可見狀態: SOFT_INPUT_STATE_UNSPECIFIED,
     *            SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
     *            SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
     *            &nbsp;&nbsp;&nbsp;&nbsp;適配選項有: 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) {// 輸入框一下的有可能是表情面板,所以,我們算作範圍內
            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 = activity
                .findViewById(android.R.id.content);
        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 / 3;
                    setKeyboardShowing(show);// 設置鍵盤是否展開狀態
                    if (show) {
                        int keyboardHeight = heightDiff
                                - DisplayUtils.getStatusBarHeight();
                        // 設置新的鍵盤高度
                        setKeyBoardHeight(keyboardHeight);
                        haskownKeyboardHeight = true;
                    }
                }
            });
        }
    }

    /**
     * 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;
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章