一般我們?yōu)榱俗屾I盤自動(dòng)將界面彈起,會(huì)在清單文件中配置windowSoftInputMode,配置為adjustResize或adjustPan。
- adjustResize,鍵盤彈起時(shí),將界面Layout高度壓縮,留出空間顯示軟鍵盤。
- adjustPan,需要存在滾動(dòng)控件,鍵盤彈起時(shí),滾動(dòng)列表,留出控件顯示軟鍵盤,如果沒有滾動(dòng)控件,則將全部控件上移,而不會(huì)壓縮界面Layout。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hule.dashi.live">
<application>
<activity
android:name=".AudioLiveRoomActivity"
android:windowSoftInputMode="adjustResize" />
</application>
</manifest>
遇到的問題
在直播模塊開發(fā)中,需要將狀態(tài)欄透明。然而透明后,卻發(fā)現(xiàn)一個(gè)大坑,軟鍵盤模式失效了。無論windowSoftInputMode設(shè)置adjustResize還是adjustPan,鍵盤都會(huì)將界面覆蓋,而不會(huì)將輸入框頂起。
搜索了一番,結(jié)果發(fā)現(xiàn)是谷歌在2.2年代就留下的坑,卻一直沒修復(fù)...那么我們是不是可以監(jiān)聽軟鍵盤彈起事件,獲取軟鍵盤高度,手動(dòng)壓縮布局Layout高度不就可以了?
結(jié)果Android并沒有提供軟鍵盤彈起的監(jiān)聽事件,擦,這么坑的?那么還有什么辦法么?
辦法還是有的
搜索了一下,原來已經(jīng)有大神提供了處理類,名叫AndroidBug5497Workaround,大概原理就是給Activity的根View注冊(cè)ViewTreeObserver.OnGlobalLayoutListener。鍵盤彈起時(shí),監(jiān)聽會(huì)回調(diào),我們拿取原Activity始布局高度和Activity可見高度進(jìn)行相減,即可計(jì)算出鍵盤高度,再將布局高度重新設(shè)置,就可以將布局上移。同時(shí)可以將鍵盤改變作為回調(diào)監(jiān)聽提供給外部。
- 結(jié)果又發(fā)現(xiàn)問題,在劉海屏上有問題,底部會(huì)空出一大片,而我修改后的做法是:計(jì)算出鍵盤高度后,給布局底部控件設(shè)置一個(gè)MarginBottom,自然會(huì)有一種頂出輸入框的效果。
源碼解析
首先是生成軟鍵盤的監(jiān)聽。提供給外部監(jiān)聽軟鍵盤監(jiān)聽。
- 首先我們傳入rootView構(gòu)造KeyboardFix。
- 給rootView設(shè)置addOnGlobalLayoutListener回調(diào)。
- addOnGlobalLayoutListener的onGlobalLayout回調(diào)時(shí),就是鍵盤彈起和下降。
- possiblyResizeChildOfContent(),computeUsableHeight()計(jì)算可見高度。
- 可見高度小于原始高度一定閥值,則當(dāng)做為鍵盤彈起。否則為下降軟鍵盤。
- 最后回調(diào)監(jiān)聽者。
public class KeyboardFix implements ViewTreeObserver.OnGlobalLayoutListener {
/**
* 父容器
*/
private View vRootView;
/**
* 父容器的高度
*/
private int mContentHeight;
/**
* 標(biāo)記,第一次回調(diào)時(shí)保存父容器的高度
*/
private boolean isFirst = true;
/**
* 最后一次顯示父容器的高度,用于判斷是否顯示虛擬鍵盤
*/
private int mLastShowHeight;
//1.首先我們傳入rootView構(gòu)造KeyboardFix。
public KeyboardFix(View rootView) {
if (rootView != null) {
//2. 給rootView設(shè)置addOnGlobalLayoutListener回調(diào)。
rootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
}
@Override
public void onGlobalLayout() {
//3. addOnGlobalLayoutListener的onGlobalLayout回調(diào)時(shí),就是鍵盤彈起和下降。
possiblyResizeChildOfContent();
}
/**
* 重新調(diào)整跟布局的高度
*/
private void possiblyResizeChildOfContent() {
//4. possiblyResizeChildOfContent(),computeUsableHeight()計(jì)算可見高度。
//計(jì)算內(nèi)容的可見高度
int usableHeightNow = computeUsableHeight();
if (isFirst) {
//兼容華為等機(jī)型
mContentHeight = usableHeightNow;
isFirst = false;
}
//沒有改變,忽略
if (usableHeightNow == mLastShowHeight) {
return;
}
mLastShowHeight = usableHeightNow;
//5. 可見高度小于原始高度一定閥值,則當(dāng)做為鍵盤彈起。否則為下降軟鍵盤。
boolean isShowKeyboard = usableHeightNow + 200 < mContentHeight;
//6. 最后回調(diào)監(jiān)聽者。
for (OnKeyboardChangeCallback listener : mOnKeyboardChangeCallbacks) {
listener.onChange(isShowKeyboard, mContentHeight, usableHeightNow);
}
}
/**
* 計(jì)算內(nèi)容的可見高度
*
* @return 父容器的可見高度
*/
private int computeUsableHeight() {
Rect rect = new Rect();
vRootView.getWindowVisibleDisplayFrame(rect);
return (rect.bottom - rect.top);
}
/**
* 虛擬鍵盤顯示隱藏的監(jiān)聽
*/
public interface OnKeyboardChangeCallback {
/**
* 虛擬鍵盤顯示發(fā)生變化時(shí)調(diào)用
*
* @param isVisible true為可見,false為不可見
* @param contentHeight 原始內(nèi)容高度
* @param usableHeight 當(dāng)前可用高度
*/
void onChange(boolean isVisible, int contentHeight, int usableHeight);
/**
* 界面暫時(shí)回調(diào)
*/
void onPause();
/**
* 界面銷毀時(shí)回調(diào)
*/
void onDestroy();
}
}
拓展KeyboardFix,處理輸入框彈起
平時(shí)我們使用軟鍵盤模式,最多就是將輸入框上移,而透明狀態(tài)欄讓鍵盤模式失效,那么讓輸入框上移的活,就只能我們自己做了。
原理:
例如我們給輸入框設(shè)置一個(gè)父容器,點(diǎn)擊輸入框彈起軟鍵盤,這時(shí)我們的鍵盤回調(diào)會(huì)回調(diào),計(jì)算軟鍵盤高度,我們將軟鍵盤高度作為輸入框父容器的MarginBottom,就形成了輸入框被軟鍵盤彈起的情況。
由于這種情況很通用,所以可以抽取為一個(gè)通用的Callback。同時(shí)為Callback提供生命周期回調(diào)。
/**
* 回調(diào)空實(shí)現(xiàn)
*/
public static class OnKeyboardChangeAdapter implements OnKeyboardChangeCallback {
@Override
public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
}
@Override
public void onPause() {
}
@Override
public void onDestroy() {
}
}
/**
* 存在輸入框場(chǎng)景的監(jiān)聽,鍵盤彈起時(shí),設(shè)置輸入框距離底部一個(gè)鍵盤高度,鍵盤下降時(shí)取消距離
*/
public static class CallbackToInput extends OnKeyboardChangeAdapter {
/**
* 輸入框容器
*/
private View vInputContainer;
public CallbackToInput(View inputContainer) {
vInputContainer = inputContainer;
}
@Override
public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
super.onChange(isVisible, contentHeight, usableHeight);
if (isVisible) {
showInputView(usableHeight, contentHeight);
} else {
setBottomMargin(0);
}
}
@Override
public void onPause() {
super.onPause();
//界面暫停時(shí),下降輸入框
setBottomMargin(0);
}
/**
* 顯示虛擬鍵盤時(shí)調(diào)用,顯示輸入框,如果綁定了列表控件則滾動(dòng)到后一個(gè)item
*
* @param height 用于計(jì)算虛擬鍵盤的高度
*/
private void showInputView(int height, int contentHeight) {
if (vInputContainer == null) {
return;
}
//虛擬鍵盤的高度
int keyboardHeight = contentHeight - height;
setBottomMargin(keyboardHeight);
}
/**
* 重新設(shè)置inputContainer的底部邊距
*/
private void setBottomMargin(int bottomMargin) {
if (vInputContainer == null) {
return;
}
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) vInputContainer.getLayoutParams();
if (params.bottomMargin != bottomMargin) {
params.bottomMargin = bottomMargin;
vInputContainer.requestLayout();
}
}
}
- 使用監(jiān)聽
mKeyboardFix.addOnKeyboardChangeListener(new KeyboardFix.CallbackToInput(inputContainer));
拓展KeyboardFix,鍵盤彈起,自動(dòng)滾動(dòng)RecyclerView
像QQ、微信,軟鍵盤彈起時(shí),滾動(dòng)列表控件到底部,由于我們已經(jīng)有了軟鍵盤回調(diào)了,所以處理起來就很簡(jiǎn)單了,一樣我們將這種通用的行為封裝為Callback即可。
/**
* 存在列表場(chǎng)景的監(jiān)聽,一般鍵盤彈起時(shí),列表需要滾動(dòng)到底部,就可以添加該類型監(jiān)聽
*/
public static class CallbackToList extends OnKeyboardChangeAdapter {
private RecyclerView vRecyclerView;
private boolean mIsReverse;
/**
* 是否反轉(zhuǎn),反轉(zhuǎn)則滾動(dòng)到第0位,非反轉(zhuǎn)則滾動(dòng)到列表的最后1位
*
* @param isReverse true為反轉(zhuǎn)
*/
public CallbackToList(RecyclerView recyclerView, boolean isReverse) {
vRecyclerView = recyclerView;
mIsReverse = isReverse;
}
@Override
public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
super.onChange(isVisible, contentHeight, usableHeight);
//鍵盤彈起時(shí)滾動(dòng)到底部
if (isVisible) {
int position;
if (mIsReverse) {
position = 0;
} else {
if (vRecyclerView.getAdapter() == null) {
return;
}
position = vRecyclerView.getAdapter().getItemCount() - 1;
}
if (vRecyclerView != null && vRecyclerView.getAdapter() != null) {
vRecyclerView.scrollToPosition(position);
}
}
}
}
- 使用監(jiān)聽
mKeyboardFix.addOnKeyboardChangeListener(new KeyboardFix.CallbackToList(recyclerView, false));
- 分發(fā)生命周期事件
最后要記得在Activity或Fragment分發(fā)生命周期事件給KeyboardFix。
@Override
protected void onPause() {
super.onPause;
if (mKeyboardFix != null) {
mKeyboardFix.onPause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mKeyboardFix != null) {
mKeyboardFix.onDestroy();
}
}
完整代碼
public class KeyboardFix implements ViewTreeObserver.OnGlobalLayoutListener {
/**
* 父容器
*/
private View vRootView;
/**
* 父容器的高度
*/
private int mContentHeight;
/**
* 標(biāo)記,第一次回調(diào)時(shí)保存父容器的高度
*/
private boolean isFirst = true;
/**
* 最后一次顯示父容器的高度,用于判斷是否顯示虛擬鍵盤
*/
private int mLastShowHeight;
/**
* 鍵盤監(jiān)聽
*/
private List<OnKeyboardChangeCallback> mOnKeyboardChangeCallbacks;
/**
* 根容器
*/
public KeyboardFix(View rootView) {
mOnKeyboardChangeCallbacks = new CopyOnWriteArrayList<>();
vRootView = rootView;
if (rootView != null) {
rootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
}
@Override
public void onGlobalLayout() {
possiblyResizeChildOfContent();
}
public void addOnKeyboardChangeListener(OnKeyboardChangeCallback onKeyboardChangeCallback) {
mOnKeyboardChangeCallbacks.add(onKeyboardChangeCallback);
}
/**
* 重新調(diào)整跟布局的高度
*/
private void possiblyResizeChildOfContent() {
//計(jì)算內(nèi)容的可見高度
int usableHeightNow = computeUsableHeight();
if (isFirst) {
//兼容華為等機(jī)型
mContentHeight = usableHeightNow;
isFirst = false;
}
//沒有改變,忽略
if (usableHeightNow == mLastShowHeight) {
return;
}
mLastShowHeight = usableHeightNow;
//判斷是否彈起軟鍵盤
boolean isShowKeyboard = usableHeightNow + 200 < mContentHeight;
for (OnKeyboardChangeCallback listener : mOnKeyboardChangeCallbacks) {
listener.onChange(isShowKeyboard, mContentHeight, usableHeightNow);
}
}
/**
* 計(jì)算內(nèi)容的可見高度
*
* @return 父容器的可見高度
*/
private int computeUsableHeight() {
Rect rect = new Rect();
vRootView.getWindowVisibleDisplayFrame(rect);
return (rect.bottom - rect.top);
}
/**
* 界面暫時(shí)時(shí)調(diào)用
*/
public void onPause() {
if (mOnKeyboardChangeCallbacks != null) {
for (OnKeyboardChangeCallback callback : mOnKeyboardChangeCallbacks) {
callback.onPause();
}
}
}
/**
* 界面銷毀時(shí)調(diào)用,取消注冊(cè),防止內(nèi)存泄漏
*/
public void onDestroy() {
if (vRootView != null) {
vRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
vRootView = null;
}
if (mOnKeyboardChangeCallbacks != null) {
for (OnKeyboardChangeCallback callback : mOnKeyboardChangeCallbacks) {
callback.onDestroy();
}
mOnKeyboardChangeCallbacks.clear();
mOnKeyboardChangeCallbacks = null;
}
}
/**
* 虛擬鍵盤顯示隱藏的監(jiān)聽
*/
public interface OnKeyboardChangeCallback {
/**
* 虛擬鍵盤顯示發(fā)生變化時(shí)調(diào)用
*
* @param isVisible true為可見,false為不可見
* @param contentHeight 原始內(nèi)容高度
* @param usableHeight 當(dāng)前可用高度
*/
void onChange(boolean isVisible, int contentHeight, int usableHeight);
/**
* 界面暫時(shí)回調(diào)
*/
void onPause();
/**
* 界面銷毀時(shí)回調(diào)
*/
void onDestroy();
}
/**
* 回調(diào)空實(shí)現(xiàn)
*/
public static class OnKeyboardChangeAdapter implements OnKeyboardChangeCallback {
@Override
public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
}
@Override
public void onPause() {
}
@Override
public void onDestroy() {
}
}
/**
* 顯示軟鍵盤
*/
public void showSoftInput(final View view) {
InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (manager == null) {
return;
}
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.requestFocus();
manager.showSoftInput(view, InputMethodManager.SHOW_FORCED);
}
/**
* 存在輸入框場(chǎng)景的監(jiān)聽,鍵盤彈起時(shí),設(shè)置輸入框距離底部一個(gè)鍵盤高度,鍵盤下降時(shí)取消距離
*/
public static class CallbackToInput extends OnKeyboardChangeAdapter {
/**
* 輸入框容器
*/
private View vInputContainer;
public CallbackToInput(View inputContainer) {
vInputContainer = inputContainer;
}
@Override
public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
super.onChange(isVisible, contentHeight, usableHeight);
if (isVisible) {
showInputView(usableHeight, contentHeight);
} else {
setBottomMargin(0);
}
}
@Override
public void onPause() {
super.onPause();
//界面暫停時(shí),下降輸入框
setBottomMargin(0);
}
/**
* 顯示虛擬鍵盤時(shí)調(diào)用,顯示輸入框,如果綁定了列表控件則滾動(dòng)到后一個(gè)item
*
* @param height 用于計(jì)算虛擬鍵盤的高度
*/
private void showInputView(int height, int contentHeight) {
if (vInputContainer == null) {
return;
}
//虛擬鍵盤的高度
int keyboardHeight = contentHeight - height;
setBottomMargin(keyboardHeight);
}
/**
* 重新設(shè)置inputContainer的底部邊距
*/
private void setBottomMargin(int bottomMargin) {
if (vInputContainer == null) {
return;
}
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) vInputContainer.getLayoutParams();
if (params.bottomMargin != bottomMargin) {
params.bottomMargin = bottomMargin;
vInputContainer.requestLayout();
}
}
}
/**
* 存在列表場(chǎng)景的監(jiān)聽,一般鍵盤彈起時(shí),列表需要滾動(dòng)到底部,就可以添加該類型監(jiān)聽
*/
public static class CallbackToList extends OnKeyboardChangeAdapter {
private RecyclerView vRecyclerView;
private boolean mIsReverse;
/**
* 是否反轉(zhuǎn),反轉(zhuǎn)則滾動(dòng)到第0位,非反轉(zhuǎn)則滾動(dòng)到列表的最后1位
*
* @param isReverse true為反轉(zhuǎn)
*/
public CallbackToList(RecyclerView recyclerView, boolean isReverse) {
vRecyclerView = recyclerView;
mIsReverse = isReverse;
}
@Override
public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
super.onChange(isVisible, contentHeight, usableHeight);
//鍵盤彈起時(shí)滾動(dòng)到底部
if (isVisible) {
int position;
if (mIsReverse) {
position = 0;
} else {
if (vRecyclerView.getAdapter() == null) {
return;
}
position = vRecyclerView.getAdapter().getItemCount() - 1;
}
if (vRecyclerView != null && vRecyclerView.getAdapter() != null) {
vRecyclerView.scrollToPosition(position);
}
}
}
}
}
總結(jié)
雖然谷歌沒有提供鍵盤監(jiān)聽,但是我們可以曲線救國(guó),當(dāng)然希望官方可以修復(fù)這個(gè)bug,并且提供回調(diào)為最好,本篇作為記錄而寫。