Android軟鍵盤的監聽與高度控制的幾種方案及常用效果
theme: smartblue highlight: agate
持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第5天,點擊查看活動詳情
前言
本文我們會一起復習一下軟鍵盤高度獲取的幾種方式,佈局貼在軟鍵盤上效果的實現與優化。
事情是這樣的,有一天我逛PDD的時候,發現這樣一個效果,
在搜索頁面中,如果軟件彈起了就會有一個語音搜索的佈局,當我們隱藏軟鍵盤之後就隱藏這個佈局,
然後我又看了一下TB的搜索頁面,都是類似的效果,但是我發現他們的效果都有優化的空間。
他們的做法是獲取到軟鍵盤彈起之後的高度,然後把佈局設置到軟鍵盤上面,這個大家都會,但是佈局在添加到軟鍵盤之後,軟鍵盤才會慢慢的做一個平移動畫展示到指定的位置,如果把動畫效果放慢就可以很明顯的看到效果。
能不能讓我們的佈局附着在軟鍵盤上面,隨着軟鍵盤的平移動畫而動呢?這樣的話效果是不是會更流暢一點?
下面我們舉例説明一下之前的老方法直接獲取到軟鍵盤高度,把佈局放上去的做法,和隨着軟鍵盤一起動的做法,這兩種做法的區別。
一、獲取軟鍵盤高度-方式一
要説獲取軟鍵盤的高度,那麼肯定離不開 getViewTreeObserver().addOnGlobalLayoutListener 的方式。
只是使用起來又分不同的做法,最簡單的是拿到Activity的ContentView,設置
contentView.getViewTreeObserver()
.addOnGlobalLayoutListener(onGlobalLayoutListener);
然後在監聽內部再通過 decorView.getWindowVisibleDisplayFrame
來獲取顯示的Rect,在通過 decorView.getBottom() - outRect.bottom
的方式來獲取高度。
完整示例如下:
```java public final class Keyboard1Utils {
public static int sDecorViewInvisibleHeightPre;
private static ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener;
private Keyboard1Utils() {
}
private static int sDecorViewDelta = 0;
private static int getDecorViewInvisibleHeight(final Activity activity) {
final View decorView = activity.getWindow().getDecorView();
if (decorView == null) return sDecorViewInvisibleHeightPre;
final Rect outRect = new Rect();
decorView.getWindowVisibleDisplayFrame(outRect);
int delta = Math.abs(decorView.getBottom() - outRect.bottom);
if (delta <= getNavBarHeight()) {
sDecorViewDelta = delta;
return 0;
}
return delta - sDecorViewDelta;
}
public static void registerKeyboardHeightListener(final Activity activity, final KeyboardHeightListener listener) {
final int flags = activity.getWindow().getAttributes().flags;
if ((flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) != 0) {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
final FrameLayout contentView = activity.findViewById(android.R.id.content);
sDecorViewInvisibleHeightPre = getDecorViewInvisibleHeight(activity);
ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int height = getDecorViewInvisibleHeight(activity);
if (sDecorViewInvisibleHeightPre != height) {
listener.onKeyboardHeightChanged(height);
sDecorViewInvisibleHeightPre = height;
}
}
};
contentView.getViewTreeObserver()
.addOnGlobalLayoutListener(onGlobalLayoutListener);
}
public static void unregisterKeyboardHeightListener(Activity activity) {
onGlobalLayoutListener = null;
View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
if (contentView == null) return;
contentView.getViewTreeObserver().removeGlobalOnLayoutListener(onGlobalLayoutListener);
}
private static int getNavBarHeight() {
Resources res = Resources.getSystem();
int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId != 0) {
return res.getDimensionPixelSize(resourceId);
} else {
return 0;
}
}
public interface KeyboardHeightListener {
void onKeyboardHeightChanged(int height);
}
} ```
使用: ```kotlin
override fun init() {
Keyboard1Utils.registerKeyboardHeightListener(this) {
YYLogUtils.w("當前的軟鍵盤高度:$it")
}
} ```
Log如下:
需要注意的是方法內部獲取導航欄的方法是過時的,部分手機會有問題,但是並沒有用它做計算,只是用於一個Flag,終歸還是能用,經過我的測試也並不會影響效果。
二、獲取軟鍵盤高度-方式二
獲取軟鍵盤高度的第二種方式也是使用 getViewTreeObserver().addOnGlobalLayoutListener 的方式,不過不同的是,它是在Activity添加了一個PopupWindow,然後讓軟鍵盤彈起的時候,計算PopopWindow移動了多少範圍,從而計算軟鍵盤的高度。
這個是網上用的比較多的一種開源方案,別的不説這個思路就是清奇,真是和尚的房子-秒啊
它創建一個看不見的彈窗,即寬為0,高為全屏,併為彈窗設置全局佈局監聽器。當佈局有變化,比如有輸入法彈窗出現或消失時, 監聽器回調函數就會被調用。而其中的關鍵就是當輸入法彈出時, 它會把之前我們創建的那個看不見的彈窗往上擠, 這樣我們創建的那個彈窗的位置就變化了,只要獲取它底部高度的變化值就可以間接的獲取輸入法的高度了。
這裏我對源碼做了一點修改
```java public class KeyboardHeightUtils extends PopupWindow {
private KeyboardHeightListener mListener;
private View popupView;
private View parentView;
private Activity activity;
public KeyboardHeightUtils(Activity activity) {
super(activity);
this.activity = activity;
LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
this.popupView = inflator.inflate(R.layout.keyboard_popup_window, null, false);
setContentView(popupView);
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
parentView = activity.findViewById(android.R.id.content);
setWidth(0);
setHeight(WindowManager.LayoutParams.MATCH_PARENT);
popupView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (popupView != null) {
handleOnGlobalLayout();
}
}
});
}
public void start() {
parentView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
if (!isShowing() && parentView.getWindowToken() != null) {
setBackgroundDrawable(new ColorDrawable(0));
showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0);
}
}
@Override
public void onViewDetachedFromWindow(View view) {
}
});
}
public void close() {
this.mListener = null;
dismiss();
}
public void registerKeyboardHeightListener(KeyboardHeightListener listener) {
this.mListener = listener;
}
private void handleOnGlobalLayout() {
Point screenSize = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(screenSize);
Rect rect = new Rect();
popupView.getWindowVisibleDisplayFrame(rect);
int keyboardHeight = screenSize.y - rect.bottom;
notifyKeyboardHeightChanged(keyboardHeight);
}
private void notifyKeyboardHeightChanged(int height) {
if (mListener != null) {
mListener.onKeyboardHeightChanged(height);
}
}
public interface KeyboardHeightListener {
void onKeyboardHeightChanged(int height);
}
} ```
使用的方式:
```kotlin override fun init() {
keyboardHeightUtils = KeyboardHeightUtils(this)
keyboardHeightUtils.registerKeyboardHeightListener {
YYLogUtils.w("第二種方式:當前的軟鍵盤高度:$it")
}
keyboardHeightUtils.start()
}
override fun onDestroy() {
super.onDestroy()
Keyboard1Utils.unregisterKeyboardHeightListener(this)
keyboardHeightUtils.close();
}
```
Log如下:
和第一種方案有異曲同工之妙,都是一個方法,但是思路有所不同,但是這種方法也有一個坑點,就是需要計算狀態欄的高度。可以看到第二種方案和第一種方案有一個狀態欄高度的偏差,大家記得處理即可。
三、獲取軟鍵盤高度-方式三
之前的文章我們講過 WindowInsets 的方案,這裏我們進一步説一下使用 WindowInsets 獲取軟鍵盤高度的坑點。
如果能直接使用兼容方案,那肯定是完美的:
```kotlin
ViewCompat.setWindowInsetsAnimationCallback(window.decorView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList
val isVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
//當前是否展示
YYLogUtils.w("isVisible = $isVisible")
//當前的高度進度回調
YYLogUtils.w("keyboardHeight = $keyboardHeight")
return insets
}
})
ViewCompat.getWindowInsetsController(findViewById(android.R.id.content))?.apply {
show(WindowInsetsCompat.Type.ime())
}
```
可惜想法很好,實際上也只有在Android R 以上才好用,低版本要麼就只觸發一次,要麼就乾脆不觸發。兼容性的方案也有兼容性問題!
具體可以參考我之前的文章,按照我們之前的説法,我們需要在Android11上使用動畫監聽的方案,而Android11一下使用 setOnApplyWindowInsetsListener 的方式來獲取。
代碼大概如下
kotlin
fun addKeyBordHeightChangeCallBack(view: View, onAction: (height: Int) -> Unit) {
var posBottom: Int
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val cb = object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
override fun onProgress(
insets: WindowInsets,
animations: MutableList<WindowInsetsAnimation>
): WindowInsets {
posBottom = insets.getInsets(WindowInsets.Type.ime()).bottom +
insets.getInsets(WindowInsets.Type.systemBars()).bottom
onAction.invoke(posBottom)
return insets
}
}
view.setWindowInsetsAnimationCallback(cb)
} else {
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
posBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom +
insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
onAction.invoke(posBottom)
insets
}
}
}
但是實測之後發現,就算是兼容版本的 setOnApplyWindowInsetsListener 方法,獲取狀態欄和導航欄沒有問題,但是當軟鍵盤彈起和收起的時候並不會再次回調,也就是部分設備和版本只能調用一次,再次彈軟鍵盤的時候就不觸發了。
這... 又是一個坑。
所以我們如果想兼容版本的話,那沒辦法了,只能出絕招了,我們就把Android11以下的機型使用 getViewTreeObserver().addOnGlobalLayoutListener 的方式,而Android11以上的我們使用 WindowInsets 的方案。
具體的兼容方案如下:
```java public final class Keyboard4Utils {
public static int sDecorViewInvisibleHeightPre;
private static ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener;
private Keyboard4Utils() {
}
private static int sDecorViewDelta = 0;
private static int getDecorViewInvisibleHeight(final Activity activity) {
final View decorView = activity.getWindow().getDecorView();
if (decorView == null) return sDecorViewInvisibleHeightPre;
final Rect outRect = new Rect();
decorView.getWindowVisibleDisplayFrame(outRect);
int delta = Math.abs(decorView.getBottom() - outRect.bottom);
if (delta <= getNavBarHeight()) {
sDecorViewDelta = delta;
return 0;
}
return delta - sDecorViewDelta;
}
public static void registerKeyboardHeightListener(final Activity activity, final KeyboardHeightListener listener) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
invokeAbove31(activity, listener);
} else {
invokeBelow31(activity, listener);
}
}
@RequiresApi(api = Build.VERSION_CODES.R)
private static void invokeAbove31(Activity activity, KeyboardHeightListener listener) {
activity.getWindow().getDecorView().setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
@NonNull
@Override
public WindowInsets onProgress(@NonNull WindowInsets windowInsets, @NonNull List<WindowInsetsAnimation> list) {
int height = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
listener.onKeyboardHeightChanged(height);
return windowInsets;
}
});
}
private static void invokeBelow31(Activity activity, KeyboardHeightListener listener) {
final int flags = activity.getWindow().getAttributes().flags;
if ((flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) != 0) {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
final FrameLayout contentView = activity.findViewById(android.R.id.content);
sDecorViewInvisibleHeightPre = getDecorViewInvisibleHeight(activity);
onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int height = getDecorViewInvisibleHeight(activity);
if (sDecorViewInvisibleHeightPre != height) {
listener.onKeyboardHeightChanged(height);
sDecorViewInvisibleHeightPre = height;
}
}
};
contentView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
}
public static void unregisterKeyboardHeightListener(Activity activity) {
onGlobalLayoutListener = null;
View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
if (contentView == null) return;
contentView.getViewTreeObserver().removeGlobalOnLayoutListener(onGlobalLayoutListener);
}
private static int getNavBarHeight() {
Resources res = Resources.getSystem();
int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId != 0) {
return res.getDimensionPixelSize(resourceId);
} else {
return 0;
}
}
public interface KeyboardHeightListener {
void onKeyboardHeightChanged(int height);
}
} ```
運行的Log如下:
通過這樣的方式我們就能實現在 Android R 以上的設備可以有當前的軟鍵盤高度回調,而低版本的會直接回調當前的軟鍵盤需要展示的直接高度。
四、實現佈局懸停在軟鍵盤上面
做好了軟鍵盤的高度計算之後,我們就能實現對應的佈局了,這裏我們以非滾動的固定佈局為例子。
我們在底部加入一個ImageView,當軟鍵盤彈起的時候我們顯示到軟鍵盤上面,彈出軟鍵盤試試!
哎?怎麼沒效果??別慌,還沒開始呢!下面開始上方案。
這裏我們使用方案一來看看效果:
```kotlin Keyboard1Utils.registerKeyboardHeightListener(this) {
YYLogUtils.w("當前的軟鍵盤高度:$it")
updateVoiceIcon(it)
}
//更新語音圖標的位置
private fun updateVoiceIcon(height: Int) {
mIvVoice.updateLayoutParams<FrameLayout.LayoutParams> {
bottomMargin = height
}
}
```
我們簡單的做一個增加間距的屬性。效果如下:
嗯,就是PDD和TB的應用效果了,那之前我們説的隨着軟鍵盤的動畫而動畫的那種效果呢?
其實就是使用第三種方案,不過只有在Android11以上才能生效,其實目前Android11的佔有率還可以。接下來我們換一個手機試試。
沒什麼效果?是的,我還沒換呢,鬧個眼子。先發一個效果一的圖來做一下對比嘛。
接下來我們使用方案三來試試:
```kotlin Keyboard3Utils.registerKeyboardHeightListener(this) { YYLogUtils.w("第三種方式:當前的軟鍵盤高度:$it") updateVoiceIcon(it) }
//更新語音圖標的位置
private fun updateVoiceIcon(height: Int) {
mIvVoice.updateLayoutParams<FrameLayout.LayoutParams> {
bottomMargin = height
}
}
```
效果三的運行效果如下:
這麼看能看出效果一和效果三之間的區別嗎,沿着軟鍵盤做的位移,由於我是手機錄屏MP4轉碼GIF,所以是渣渣畫質,實際效果比GIF要流暢。
就一個字,絲滑!
總結
本文的示例都是基於固定佈局下的一些軟鍵盤的操作,而如果是ScrollView類似的一些滾動佈局下,那麼又是另外一種做法,這裏沒有做對比。由於篇幅原因,後期可能會單獨出各種佈局下軟鍵盤的與EidtText的位置相關設置。
其實這種把佈局貼在軟鍵盤上面的做法,其實在應用開發中還是相對常見的,比如把輸入框的Dialog貼在軟鍵盤上面,比如語言搜索的佈局放在軟鍵盤上面等等。
對這樣的方案來説,其實我們可以儘量的優化一下展示的方式,高版本的手機會更加的絲滑,總的來説使用第三種方案還是不錯的,兼容性還可以。
本文用到的一些測試機型為5.0 6.0 7.0 12這些機型,由於時間精力等原因並沒有覆蓋全版本和機型,如果大家有其他的兼容性問題也能評論區交流一下。如果有其他或更好的方案也可以評論區交流哦。
好了,本文的全部代碼與Demo都已經開源。有興趣可以看這裏。項目會持續更新,大家可以關注一下。
如果感覺本文對你有一點點的啟發,還望你能點贊
支持一下,你的支持是我最大的動力。
Ok,這一期就此完結。
- Android操作文件也太難了趴,File vs DocumentFile 以及 DocumentsProvider vs FileProvider 的異同
- findViewById不香嗎?為什麼要把簡單的問題複雜化?為什麼要用DataBinding?
- Android自定義View繪製進階-水波浪温度刻度表
- Android自定義ViewGroup佈局進階,完整的九宮格實現
- 記錄仿抖音的視頻播放並緩存預加載視頻的效果實現
- Kotlin對象的懶加載方式?by lazy 與 lateinit 的異同
- 定位都得集成第三方?Android原生定位服務LocationManager不行嗎?
- 還用第三方庫管理狀態欄嗎?Android關於狀態欄管理的幾種方案實現!
- 下載需要集成第三方?Android原生下載服務DownloadManager不行嗎?
- Android陰影實現的幾種方案-自定義圓角ViewGroup加入陰影效果
- 操作Android窗口的幾種方式?WindowInsets與其兼容庫的使用與踩坑
- Android軟鍵盤與佈局的協調-不同的效果與實現方案的探討
- ViewPager2:ViewPager都能自動嵌套滾動了,我不行?我麻了!該怎麼做?
- Android軟鍵盤的監聽與高度控制的幾種方案及常用效果
- 圓角升級啦,來手把手一起實現自定義ViewGroup的各種圓角與背景
- Android導航欄的處理-HostStatusLayout加入底部的導航欄適配
- 一次搞懂怎麼設置圓角圖片,ImageView的各種圓角設置
- 一看就會 Android框架DataBinding的使用與封裝
- 別濫用FileProvider了,Android中FileProvider的各種場景應用
- Android登錄攔截的場景-基於攔截器模式實現