Android輸入法與表情面板切換時(shí)的界面抖動(dòng)問題解決方法
昨天琢磨了下Android的輸入法彈出模式,突然發(fā)現(xiàn)利用動(dòng)態(tài)切換輸入法的彈出模式可以解決輸入法抖動(dòng)的問題。具體是怎樣的抖動(dòng)呢?我們先看微博的反面教材。

【具體表現(xiàn)為:表情面板與輸入法面板高度不一致,從而導(dǎo)致彈出輸入法(layout被擠壓)時(shí),同時(shí)又需要隱藏表情面板(layout被拉升),最終讓界面產(chǎn)生了高度差抖動(dòng),所以在切換時(shí)明顯會(huì)有不大好的抖動(dòng)體驗(yàn))】
使用了解決抖動(dòng)的解決方案后,效果如下:

【這樣的方案明顯比微博的切換更平滑】
老樣子,先說思路。主要我們要用到兩個(gè)輸入法彈出模式,分別是:adjustResize(調(diào)整模式) 、adjustNothing(不做任何調(diào)整) 。(更多介紹請參看我的上一篇文章:輸入法彈出參數(shù)分析)
1.初始情況時(shí)(鍵盤和表情面板都未展開):我們?yōu)楸砬槊姘逶O(shè)置一個(gè)默認(rèn)高度(因?yàn)槲覀冞€不知道鍵盤有多高)并將輸入發(fā)彈出模式設(shè)置為adjustResize模式。
2.當(dāng)我們點(diǎn)擊了EditText時(shí),系統(tǒng)將會(huì)彈出輸入法,由于之前我們設(shè)置的模式為adjustResize,因此,輸入法會(huì)擠壓Layout,并且擠壓的高度最終會(huì)固定到一個(gè)值(鍵盤的高度),當(dāng)我們檢測到擠壓后,將這個(gè)擠壓差值(也就是鍵盤高度)記錄下來,作為表情面板的新高度值。于此同時(shí),我們將表情面板隱藏。
3.當(dāng)我們點(diǎn)擊了表情按鈕時(shí),我們需要先判斷輸入法是否已展開。
1)如果已經(jīng)展開,那么我們的任務(wù)是將鍵盤平滑隱藏并顯示表情面板。具體做法為:先將Activity的輸入法彈出模式設(shè)置為adjustNothing,然后將上一步記錄下來的鍵盤高度作為表情面板的高度,再將表情面板顯示,此時(shí)由于鍵盤彈出模式為adjustNothing,所以鍵盤不會(huì)有任何抖動(dòng),并且由于表情面板與鍵盤等高,因此EditText也不會(huì)下移,最后將輸入法隱藏。
2)如果輸入法未展開,我們再判斷表情面板是否展開,如果展開了就隱藏并將輸入法彈出模式歸位為adjustResize,如果未展開就直接顯示并將輸入法彈出模式設(shè)置為adjustNothing。
大致的實(shí)現(xiàn)思路就是上面說到的,但是,既然都準(zhǔn)備動(dòng)手做幫助類了,就順便將點(diǎn)擊空白處折疊鍵盤和表情面板一起做了。具體實(shí)現(xiàn)思路為:在Activity的DecorView上面遮罩一層FrameLayout,用于監(jiān)聽觸摸的Aciton_Down事件,如果在輸入范圍之外,則折疊表情面板和鍵盤。示意圖如下:

該說的說完了,開動(dòng)。
1、創(chuàng)建InputMethodUtils類,構(gòu)造方法需要傳遞Activity參數(shù),并申明所需要的成員變量,并實(shí)現(xiàn)View.OnClickListener接口(因?yàn)槲覀円O(jiān)聽表情按鈕的點(diǎn)擊事件)。代碼如下:
public class InputMethodUtils implements View.OnClickListener {
// 鍵盤是否展開的標(biāo)志位
private boolean sIsKeyboardShowing;
// 鍵盤高度變量
private int sKeyBoardHeight = 0;
// 綁定的Activity
private Activity activity;
/**
* 構(gòu)造函數(shù)
*
* @param activity
* 需要處理輸入法的當(dāng)前的Activity
*/
public InputMethodUtils(Activity activity) {
this.activity = activity;
//DisplayUtils為屏幕尺寸工具類
DisplayUtils.init(activity);
// 默認(rèn)鍵盤高度為267dp
setKeyBoardHeight(DisplayUtils.dp2px(267));
}
@Override
public void onClick(View v) {
}
}
//DisplayUtils的實(shí)現(xiàn)代碼為:
/**
* 屏幕參數(shù)的輔助工具類。例如:獲取屏幕高度,寬度,statusBar的高度,px和dp互相轉(zhuǎn)換等
* 【注意,使用之前一定要初始化!一次初始化就OK(建議APP啟動(dòng)時(shí)進(jìn)行初始化)。 初始化代碼 DisplayUtils.init(context)】
* @author 藍(lán)亭書序
*/
private static class DisplayUtils {
// 四舍五入的偏移值
private static final float ROUND_CEIL = 0.5f;
// 屏幕矩陣對象
private static DisplayMetrics sDisplayMetrics;
// 資源對象(用于獲取屏幕矩陣)
private static Resources sResources;
// statusBar的高度(由于這里獲取statusBar的高度使用的反射,比較耗時(shí),所以用變量記錄)
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 轉(zhuǎn) px
*
* @param dp
* dp值
* @return 轉(zhuǎn)換后的像素值
*/
public static int dp2px(int dp) {
return (int) (dp * getDensity() + ROUND_CEIL);
}
/**
* 獲取狀態(tài)欄高度
*
* @return 狀態(tài)欄高度
*/
public static int getStatusBarHeight() {
// 如果之前計(jì)算過,直接使用上次的計(jì)算結(jié)果
if (statusBarHeight == -1) {
final int defaultHeightInDp = 19;// statusBar默認(rèn)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、在繼續(xù)往下寫之前,我們得考慮如何設(shè)計(jì)表情按鈕、表情按鈕點(diǎn)擊事件、表情面板之間的問題。我的做法是創(chuàng)建一個(gè)ViewBinder內(nèi)部類。(因?yàn)樵谶壿嬌蟻碚f,這三個(gè)屬于一體的)
ViewBinder的實(shí)現(xiàn)代碼如下:
/**
* 用于控制點(diǎn)擊某個(gè)按鈕顯示或者隱藏“表情面板”的綁定bean對象。<br/>
* 例如:我想點(diǎn)擊“表情”按鈕顯示“表情面板”,我就可以這樣做:<br/>
* ViewBinder viewBinder = new ViewBinder(btn_emotion,emotionPanel);<br/>
* 這樣就創(chuàng)建出了一個(gè)ViewBinder對象<br/>
* <font color='red'>【注意事項(xiàng),使用此類時(shí),千萬不要使用trigger的setOnClickListener來監(jiān)聽事件(
* 使用OnTriggerClickListener來代替),也不要使用setTag來設(shè)置Tag,否則會(huì)導(dǎo)致使用異?!?lt;/font>
* @author 藍(lán)亭書序
*/
public static class ViewBinder {
private View trigger;//表情按鈕對象
private View panel;//表情面板對象
//替代的監(jiān)聽器
private OnTriggerClickListener listener;
/**
* 創(chuàng)建ViewBinder對象<br/>
* 例如:我想點(diǎn)擊“表情”按鈕顯示“表情面板”,我就可以這樣做:<br/>
* ViewBinder viewBinder = new
* ViewBinder(btn_emotion,emotionPanel,listener);<br/>
* 這樣就創(chuàng)建出了一個(gè)ViewBinder對象
*
* @param trigger
* 觸發(fā)對象
* @param panel
* 點(diǎn)擊觸發(fā)對象需要顯示/隱藏的面板對象
* @param listener
* Trigger點(diǎn)擊的監(jiān)聽器(千萬不要使用setOnClickListener,否則會(huì)覆蓋本工具類的監(jiān)聽器)
*/
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占用監(jiān)聽器的問題(我們內(nèi)部邏輯需要占用監(jiān)聽器,如果外部想實(shí)現(xiàn)額外的點(diǎn)擊邏輯不能再為trigger添加監(jiān)聽器,所以使用OnTriggerClickListener來代替原原聲的OnClickListener)。OnTriggerClickListener為一個(gè)接口,實(shí)現(xiàn)代碼如下:
/**
* ViewBinder的觸發(fā)按鈕點(diǎn)擊的監(jiān)聽器
* @author 藍(lán)亭書序
*/
public static interface OnTriggerClickListener {
/**
* 點(diǎn)擊事件的回調(diào)函數(shù)
* @param v 被點(diǎn)擊的按鈕對象
*/
public void onClick(View v);
}
3、實(shí)現(xiàn)了ViewBinder后,我們還需要實(shí)現(xiàn)一個(gè)遮罩View,用于監(jiān)聽ACTION_DOWN事件。代碼如下:
/**
* 點(diǎn)擊軟鍵盤區(qū)域以外自動(dòng)關(guān)閉軟鍵盤的遮罩View
* @author 藍(lán)亭書序
*/
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);
}
/*如果不知道這個(gè)方法的作用的話,需要了解下Android的事件分發(fā)機(jī)制哈,如果有時(shí)間我也可以寫個(gè)文章介紹下。dispatchTouchEvent方法主要是ViewGroup在事件分發(fā)之前進(jìn)行事件進(jìn)行判斷,如果返回true表示此ViewGroup攔截此事件,這個(gè)事件將不會(huì)傳遞給他的子View,如果返回false,反之。*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//這段邏輯不復(fù)雜,看一遍應(yīng)該就懂
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)) {
// 如果點(diǎn)擊的是輸入框(會(huì)彈出輸入框),那么延時(shí)折疊表情面板
postDelayed(new Runnable() {
@Override
public void run() {
setKeyboardShowing(true);
}
}, 500);
}
}
return super.onTouchEvent(event);
}
}
/**
* 是否點(diǎn)擊軟鍵盤和輸入法外面區(qū)域
* @param activity
* 當(dāng)前activity
* @param touchY
* 點(diǎn)擊y坐標(biāo)(不包括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;
}
/**
* 是否點(diǎn)擊的是當(dāng)前焦點(diǎn)View的范圍
* @param x
* x方向坐標(biāo)
* @param y
* y方向坐標(biāo)(不包括statusBar的高度)
* @return true表示點(diǎn)擊的焦點(diǎn)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、準(zhǔn)備工作做完,我們可以繼續(xù)完善InputMethodUtils類了,由于我們需要存儲(chǔ)ViewBinder對象(主要用于控制按鈕和面板之間的關(guān)聯(lián)關(guān)系),所以,我們還需要在InputMethodUtils中申明一個(gè)集合。代碼如下:
// 觸發(fā)與面板對象集合(使用set可以自動(dòng)過濾相同的ViewBinder) private Set<ViewBinder> viewBinders = new HashSet<ViewBinder>();
5、與viewBinders 隨之而來的一些常用方法有必要寫一下(例如折疊所有表情面板、獲取當(dāng)前哪個(gè)表情面板展開著等),代碼如下:
/**
* 添加ViewBinder
* @param viewBinder
* 變長參數(shù)
*/
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);
}
}
}
/**
* 關(guān)閉所有的面板
*/
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、通過監(jiān)聽Layout的變化來判斷輸入法是否已經(jīng)展開。代碼如下:
/**
* 設(shè)置View樹監(jiān)聽,以便判斷鍵盤是否彈出。<br/>
* 【只有當(dāng)Activity的windowSoftInputMode設(shè)置為adjustResize時(shí)才有效!所以我們要處理adjustNoting(不會(huì)引起Layout的形變)的情況鍵盤監(jiān)聽(后文會(huì)提到)】
*/
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形變超過鍵盤的一半表示鍵盤已經(jīng)展開了
boolean show = heightDiff >= sKeyBoardHeight / 2;
setKeyboardShowing(show);// 設(shè)置鍵盤是否展開狀態(tài)
if (show) {
int keyboardHeight = heightDiff
- DisplayUtils.getStatusBarHeight();
// 設(shè)置新的鍵盤高度
setKeyBoardHeight(keyboardHeight);
}
}
});
}
}
7、完成鍵盤的顯示/隱藏和動(dòng)態(tài)控制輸入法彈出模式的常用方法。代碼如下:
/**
* 隱藏輸入法
* @param currentFocusView
* 當(dāng)前焦點(diǎn)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);
}
}
}
/**
* 更新輸入法的彈出模式(注意這是靜態(tài)方法,可以直接當(dāng)做工具方法使用)
* @param activity 對應(yīng)的Activity
* @param softInputMode
* <br/>
* 鍵盤彈出模式:WindowManager.LayoutParams的參數(shù)有:<br/>
* 可見狀態(tài): SOFT_INPUT_STATE_UNSPECIFIED,
* SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
* SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
* 適配選項(xiàng)有: 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);
}
}
}
/**
* 更新輸入法的彈出模式(遇上面的靜態(tài)方法的區(qū)別是直接使用的是綁定的activity對象)
*
* @param softInputMode
* <br/>
* 鍵盤彈出模式:WindowManager.LayoutParams的參數(shù)有:<br/>
* 可見狀態(tài): SOFT_INPUT_STATE_UNSPECIFIED,
* SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
* SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
* 適配選項(xiàng)有: SOFT_INPUT_ADJUST_UNSPECIFIED,
* SOFT_INPUT_ADJUST_RESIZE, or SOFT_INPUT_ADJUST_PAN.
*/
public void updateSoftInputMethod(int softInputMode) {
updateSoftInputMethod(activity, softInputMode);
}
8、在構(gòu)造方法中將這些組件都初始化,并做相關(guān)設(shè)置,代碼如下:
/**
* 構(gòu)造函數(shù)
*
* @param activity
* 需要處理輸入法的當(dāng)前的Activity
*/
public InputMethodUtils(Activity activity) {
this.activity = activity;
DisplayUtils.init(activity);
// 默認(rèn)鍵盤高度為267dp
setKeyBoardHeight(DisplayUtils.dp2px(267));
updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
detectKeyboard();// 監(jiān)聽View樹變化,以便監(jiān)聽鍵盤是否彈出
enableCloseKeyboardOnTouchOutside(activity);
}
/**
1. 設(shè)置鍵盤的高度
2.
3. @param keyBoardHeight
4. 鍵盤的高度(px單位)
*/
private void setKeyBoardHeight(int keyBoardHeight) {
sKeyBoardHeight = keyBoardHeight;
updateAllPanelHeight(keyBoardHeight);
}
/**
5. 開啟點(diǎn)擊外部關(guān)閉鍵盤的功能(其實(shí)就是將遮罩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));
}
【突然有事,先寫到這,等會(huì)來完善…】回來了,接著寫。
上面的代碼基本完成需求,需要重點(diǎn)說的是如何檢測鍵盤彈出/隱藏狀態(tài)的問題(有人可能會(huì)說用InputMethodManager.isActive()啊,恩…反正我用有這個(gè)方法問題,他永遠(yuǎn)都給我返回true),下面簡單介紹下如何實(shí)現(xiàn)的鍵盤的彈出和隱藏狀態(tài)的檢測。
1、如果當(dāng)前輸入法是adjustResize模式,那么我們直接可以用Layout的形變監(jiān)聽即可實(shí)現(xiàn),也就是之前detectKeyboard()實(shí)現(xiàn)的代碼。
2、如果當(dāng)前輸入法是adjustNoting模式,這個(gè)就有點(diǎn)難處理了,因?yàn)闆]有形變可以監(jiān)聽。我的實(shí)現(xiàn)方式是:通過遮罩View判斷ACTION_DOWN的坐標(biāo),如果該坐標(biāo)落在輸入框內(nèi)(就是用戶點(diǎn)擊了輸入框,此時(shí)系統(tǒng)將會(huì)彈出輸入框),那么我們就可以認(rèn)為鍵盤為彈出模式。代碼體現(xiàn)在CloseKeyboardOnOutsideContainer的dispatchTouchEvent()方法中。
到此,開發(fā)就告一段落了。按照慣例,完整代碼如下:
/**
* 解決輸入法與表情面板之間切換時(shí)抖動(dòng)沖突的控制輔助工具類(能做到將面板與輸入法之間平滑切換).另外,具備點(diǎn)擊空白處自動(dòng)收起面板和輸入法的功能.<br/>
* 使用方法介紹如下:
* <hr/>
* <font color= 'red'>申明:【此類中,我們將表情面板選項(xiàng)、顯示表情面板的按鈕、表情面板按鈕的點(diǎn)擊事件
* 作為一個(gè)整體,包裝在ViewBinder類中(點(diǎn)擊表情面板按鈕時(shí),將會(huì)展開表情面 板 ) 】</font> <br/>
* 因此,第一步,我們將需要操作的表情面板、按鈕、事件綁定在一起,創(chuàng)建ViewBinder類(可以是很多個(gè))代碼示例如下:<br/>
* //如果不想監(jiān)聽按鈕點(diǎn)擊事件,之間將listener參數(shù)替換成null即可<br/>
* ViewBinder viewBinder1 = new ViewBinder(btn_1,panel1,listener1);<br/>
* ViewBinder viewBinder2 = new ViewBinder(btn_2,panel2,listener2);<br/>
* ...<br/>
* 第二步:創(chuàng)建InputMethodUtils類<br/>
* InputMethodUtils inputMethodUtils = new InputMethodUtils(this);<br/>
* 第三部:將ViewBinder傳遞給InputMethodUtils。<br/>
* inputMethodUtils.setViewBinders(viewBinder1,viewBinder2);//這個(gè)參數(shù)為動(dòng)態(tài)參數(shù),
* 支持多個(gè)參數(shù)傳遞進(jìn)來
* <hr/>
* 本類還提供兩個(gè)常用的工具方法:<br/>
* InputMethodUtils.hideKeyboard();//用于隱藏輸入法<br/>
* InputMethodUtils.updateSoftInputMethod();//用于將當(dāng)前Activity的輸入法模式切換成指定的輸入法模式
* <br/>
*
* @author 李長軍 2016.11.26
*/
public class InputMethodUtils implements View.OnClickListener {
// 鍵盤是否展開的標(biāo)志位
private boolean sIsKeyboardShowing;
// 鍵盤高度
private int sKeyBoardHeight = 0;
// 綁定的Activity
private Activity activity;
// 觸發(fā)與面板對象集合
private Set<ViewBinder> viewBinders = new HashSet<ViewBinder>();
/**
* 構(gòu)造函數(shù)
*
* @param activity
* 需要處理輸入法的當(dāng)前的Activity
*/
public InputMethodUtils(Activity activity) {
this.activity = activity;
DisplayUtils.init(activity);
// 默認(rèn)鍵盤高度為267dp
setKeyBoardHeight(DisplayUtils.dp2px(267));
updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
detectKeyboard();// 監(jiān)聽View樹變化,以便監(jiān)聽鍵盤是否彈出
enableCloseKeyboardOnTouchOutside(activity);
}
/**
* 添加ViewBinder
*
* @param viewBinder
* 變長參數(shù)
*/
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;
}
/**
* 關(guān)閉所有的面板
*/
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的參數(shù)有:<br/>
* 可見狀態(tài): SOFT_INPUT_STATE_UNSPECIFIED,
* SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
* SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
* 適配選項(xiàng)有: SOFT_INPUT_ADJUST_UNSPECIFIED,
* SOFT_INPUT_ADJUST_RESIZE, or SOFT_INPUT_ADJUST_PAN.
*/
public void updateSoftInputMethod(int softInputMode) {
updateSoftInputMethod(activity, softInputMode);
}
/**
* 隱藏輸入法
*
* @param currentFocusView
* 當(dāng)前焦點(diǎn)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的參數(shù)有:<br/>
* 可見狀態(tài): SOFT_INPUT_STATE_UNSPECIFIED,
* SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN,
* SOFT_INPUT_STATE_ALWAYS_VISIBLE, or SOFT_INPUT_STATE_VISIBLE.<br/>
* 適配選項(xiàng)有: 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);
}
}
}
/**
* 隱藏鍵盤,并維護(hù)顯示或不顯示的邏輯
*
* @param currentFocusView
* 當(dāng)前的焦點(diǎn)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);
}
}
/**
* 設(shè)置鍵盤彈出與否狀態(tài)
*
* @param show
* true表示彈出,false表示未彈出
*/
private void setKeyboardShowing(boolean show) {
sIsKeyboardShowing = show;
if (show) {
resetOtherPanels(null);
updateSoftInputMethod(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
}
/**
* 設(shè)置鍵盤的高度
*
* @param keyBoardHeight
* 鍵盤的高度(px單位)
*/
private void setKeyBoardHeight(int keyBoardHeight) {
sKeyBoardHeight = keyBoardHeight;
updateAllPanelHeight(keyBoardHeight);
}
/**
* 是否點(diǎn)擊軟鍵盤和輸入法外面區(qū)域
*
* @param activity
* 當(dāng)前activity
* @param touchY
* 點(diǎn)擊y坐標(biāo)(不包括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;
}
/**
* 是否點(diǎn)擊的是當(dāng)前焦點(diǎn)View的范圍
*
* @param x
* x方向坐標(biāo)
* @param y
* y方向坐標(biāo)(不包括statusBar的高度)
* @return true表示點(diǎn)擊的焦點(diǎn)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;
}
/**
* 開啟點(diǎn)擊外部關(guān)閉鍵盤的功能
*
* @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));
}
/**
* 設(shè)置View樹監(jiān)聽,以便判斷鍵盤是否彈出。<br/>
* 【只有當(dāng)Activity的windowSoftInputMode設(shè)置為adjustResize時(shí)才有效】
*/
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);// 設(shè)置鍵盤是否展開狀態(tài)
if (show) {
int keyboardHeight = heightDiff
- DisplayUtils.getStatusBarHeight();
// 設(shè)置新的鍵盤高度
setKeyBoardHeight(keyboardHeight);
}
}
});
}
}
/**
* ViewBinder的觸發(fā)按鈕點(diǎn)擊的監(jiān)聽器
*
* @author 李長軍
*
*/
public static interface OnTriggerClickListener {
/**
*
* @param v
*/
public void onClick(View v);
}
/**
* 用于控制點(diǎn)擊某個(gè)按鈕顯示或者隱藏“表情面板”的綁定bean對象。<br/>
* 例如:我想點(diǎn)擊“表情”按鈕顯示“表情面板”,我就可以這樣做:<br/>
* ViewBinder viewBinder = new ViewBinder(btn_emotion,emotionPanel);<br/>
* 這樣就創(chuàng)建出了一個(gè)ViewBinder對象<br/>
* <font color='red'>【注意事項(xiàng),使用此類時(shí),千萬不要使用trigger的setOnClickListener來監(jiān)聽事件(
* 使用OnTriggerClickListener來代替),也不要使用setTag來設(shè)置Tag,否則會(huì)導(dǎo)致使用異?!?lt;/font>
*
* @author 李長軍
*
*/
public static class ViewBinder {
private View trigger;
private View panel;
private OnTriggerClickListener listener;
/**
* 創(chuàng)建ViewBinder對象<br/>
* 例如:我想點(diǎn)擊“表情”按鈕顯示“表情面板”,我就可以這樣做:<br/>
* ViewBinder viewBinder = new
* ViewBinder(btn_emotion,emotionPanel,listener);<br/>
* 這樣就創(chuàng)建出了一個(gè)ViewBinder對象
*
* @param trigger
* 觸發(fā)對象
* @param panel
* 點(diǎn)擊觸發(fā)對象需要顯示/隱藏的面板對象
* @param listener
* Trigger點(diǎn)擊的監(jiān)聽器(千萬不要使用setOnClickListener,否則會(huì)覆蓋本工具類的監(jiān)聽器)
*/
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;
}
}
/**
* 點(diǎn)擊軟鍵盤區(qū)域以外自動(dòng)關(guān)閉軟鍵盤的遮罩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)) {
// 如果點(diǎn)擊的是輸入框,那么延時(shí)折疊表情面板
postDelayed(new Runnable() {
@Override
public void run() {
setKeyboardShowing(true);
}
}, 500);
}
}
return super.onTouchEvent(event);
}
}
/**
* 屏幕參數(shù)的輔助工具類。例如:獲取屏幕高度,寬度,statusBar的高度,px和dp互相轉(zhuǎn)換等
* 【注意,使用之前一定要初始化!一次初始化就OK(建議APP啟動(dòng)時(shí)進(jìn)行初始化)。 初始化代碼 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的高度使用的反射,比較耗時(shí),所以用變量記錄)
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 轉(zhuǎn) px
*
* @param dp
* dp值
* @return 轉(zhuǎn)換后的像素值
*/
public static int dp2px(int dp) {
return (int) (dp * getDensity() + ROUND_CEIL);
}
/**
* 獲取狀態(tài)欄高度
*
* @return 狀態(tài)欄高度
*/
public static int getStatusBarHeight() {
// 如果之前計(jì)算過,直接使用上次的計(jì)算結(jié)果
if (statusBarHeight == -1) {
final int defaultHeightInDp = 19;// statusBar默認(rèn)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;
}
}
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android使用ViewPager實(shí)現(xiàn)頂部tabbar切換界面
- Android實(shí)現(xiàn)界面左右滑動(dòng)切換功能
- Android實(shí)現(xiàn)閃屏及注冊和登錄界面之間的切換效果
- PagerSlidingTabStrip制作Android帶標(biāo)簽的多界面滑動(dòng)切換
- Android App仿微信界面切換時(shí)Tab圖標(biāo)變色效果的制作方法
- Android應(yīng)用中使用ViewPager實(shí)現(xiàn)類似QQ的界面切換效果
- android編程實(shí)現(xiàn)局部界面動(dòng)態(tài)切換的方法
- Android界面切換出現(xiàn)短暫黑屏的解決方法
- Android實(shí)現(xiàn)Activity界面切換添加動(dòng)畫特效的方法
- Android studio實(shí)現(xiàn)兩個(gè)界面間的切換
相關(guān)文章
Android通過手勢實(shí)現(xiàn)的縮放處理實(shí)例代碼
Android通過手勢實(shí)現(xiàn)的縮放處理實(shí)例代碼,需要的朋友可以參考一下2013-05-05
Android Flutter實(shí)現(xiàn)3D動(dòng)畫效果示例詳解
在Flutter中提供了AnimatedWidget組件用于構(gòu)建可復(fù)用的動(dòng)畫組件。本文我們用AnimatedWidget來實(shí)現(xiàn)組件的3D旋轉(zhuǎn)效果,感興趣的可以了解一下2022-03-03
詳解Android控件之DatePicker、TimePicker探究
本篇文章主要介紹了Android控件之DatePicker、TimePicker探究,非常具有實(shí)用價(jià)值,需要的朋友可以參考下。2016-12-12
Android RecyclerView 滾動(dòng)到中間位置的方法示例
這篇文章主要介紹了Android RecyclerView 滾動(dòng)到中間位置的方法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-03-03
Android實(shí)現(xiàn)手機(jī)聯(lián)系人分欄效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)手機(jī)聯(lián)系人分欄效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動(dòng)控件
這篇文章主要介紹了Android自定義view實(shí)現(xiàn)有header和footer的滾動(dòng)控件,可以在XML中當(dāng)Layout使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-11-11
Android 連接藍(lán)牙掃碼器無輸入框的實(shí)現(xiàn)
這篇文章主要介紹了Android 連接藍(lán)牙掃碼器無輸入框的實(shí)現(xiàn),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02

