網站首頁 編程語言 正文
前言
前段時間出現了webview的輸入框被軟鍵盤擋住的問題,處理之后順便對一些列的輸入框被擋住的情況進行一個總結。
正常情況下的輸入框被擋
正常情況下,輸入框被輸入法擋住,一般給window設softInputMode就能解決。
window.getAttributes().softInputMode = WindowManager.LayoutParams.XXX
有3種情況:
(1)SOFT_INPUT_ADJUST_RESIZE: 布局會被軟鍵盤頂上去
(2)SOFT_INPUT_ADJUST_PAN:只會把輸入框給頂上去(就是只頂一部分距離)
(3)SOFT_INPUT_ADJUST_NOTHING:不做任何操作(就是不頂)
SOFT_INPUT_ADJUST_PAN和SOFT_INPUT_ADJUST_RESIZE的不同在于SOFT_INPUT_ADJUST_PAN只是把輸入框,而SOFT_INPUT_ADJUST_RESIZE會把整個布局頂上去,這就會有種布局高度在輸入框展示和隱藏時高度動態變化的視覺效果。
如果你是出現了輸入框被擋的情況,一般設置SOFT_INPUT_ADJUST_PAN就能解決。如果你是輸入框沒被擋,但是軟鍵盤彈出的時候會把布局往上頂,如果你不希望往上頂,可以設置SOFT_INPUT_ADJUST_NOTHING。
softInputMode是window的屬性,你給在Mainifest給Activity設置,也是設給window,你如果是Dialog或者popupwindow這種,就直接getWindow()來設置就行。正常情況下設置這個屬性就能解決問題。
Webview的輸入框被擋
但是Webview的輸入框被擋的情況下,設這個屬性有可能會失效。
Webview的情況下,SOFT_INPUT_ADJUST_PAN會沒效果,然后,如果是Webview并且你還開沉浸模式的情況的話,SOFT_INPUT_ADJUST_RESIZE和SOFT_INPUT_ADJUST_PAN都會不起作用。
我去查看資料,發現這就是經典的issue 5497, 網上很多的解決方案就是通過AndroidBug5497Workaround,這個方案很容易能查到,我就不貼出來了,原理就是監聽View樹的變化,然后再計算高度,再去動態設置。這個方案的確能解決問題,但是我覺得這個操作不可控的因素比較多,說白了就是會不會某種機型或者情況下使用會出現其它的BUG,導致你需要寫一些判斷邏輯來處理特殊的情況。
解法就是不用沉浸模式然后使用SOFT_INPUT_ADJUST_RESIZE就能解決。但是有時候這個window顯示的時候就需要沉浸模式,特別是一些適配劉海屏、水滴屏這些場景。
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
那我的第一反應就是改變布局
window. setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
這樣是能正常把彈框頂上去,但是控件內部用的也是WRAP_CONTENT導致SOFT_INPUT_ADJUST_RESIZE改變布局之后就恢復不了原樣,也就是會變形。而不用WRAP_CONTENT用固定高度的話,SOFT_INPUT_ADJUST_RESIZE也是失效的。
沒事,還要辦法,在MATCH_PARENT的情況下我們去設置fitSystemWindows為true,但是這個屬性會讓出一個頂部的安全距離,效果就是向下偏移了一個狀態欄的高度。
這種情況下你可以去設置margin來解決這個頂部偏移的問題。
params.topMargin = statusHeight == 0 ? -120 : -statusHeight;
view.setLayoutParams(params);
這樣的操作是能解除頂部偏移的問題,但是布局有可能被縱向壓縮,這個我沒完全測試過,我覺得如果你布局高度是固定的,可能不會受到影響,但我的webview是自適應的,webview里面的內容也是自適應的,所以我這出現了布局縱向壓縮的情況。
舉個例子,你的view的高度是800,狀態欄高度是100,那設fitSystemWindows之后的效果就是view顯示700,paddingTop 100,這樣的效果,設置params.topMargin =-100,之后,view顯示700,paddingTop 100。大概是這個意思:能從視覺上消除頂部偏移,但是布局縱向被壓縮的問題沒得到處理
所以最終的解決方法是改WindowInsets的Rect (這個我等下會再解釋是什么意思)
具體的操作就是在你的自定義view中加入下面兩個方法
@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
fitSystemWindows = true;
super.setFitsSystemWindows(fitSystemWindows);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Log.v("mmp", "測試頂部偏移量: "+insets.top);
insets.top = 0;
return super.fitSystemWindows(insets);
}
小結
解決WebView+沉浸模式下輸入框被軟鍵盤擋住的步驟:
- window.getAttributes().softInputMode設置成SOFT_INPUT_ADJUST_RESIZE
- 設置view的fitSystemWindows為true,我這里是webview里面的輸入框被擋住,設的就是webview而不是父View
- 重寫fitSystemWindows方法,把insets的top設為0
WindowInsets
根據上面的3步操作,你就能處理webview輸入框被擋的問題,但是如果你想知道為什么,這是什么原理。你就需要去了解WindowInsets。我們的沉浸模式的操作setSystemUiVisibility和設置fitSystemWindows屬性,還有重寫fitSystemWindows方法,都和WindowInsets有關。
WindowInsets是應用于窗口的系統視圖的插入。例如狀態欄STATUS_BAR和導航欄NAVIGATION_BAR。它會被view引用,所以我們要做具體的操作,是對view進行操作。
還有一個比較重要的問題,WindowInsets的不同版本都是有一定的差別,Android28、Android29、Android30都有一定的差別,例如29中有個android.graphics.Insets類,這是28里面沒有的,我們可以在29中拿到它然后查看top、left等4個屬性,但是只能查看,它是final的,不能直接拿出來修改。
但是WindowInsets這塊其實能講的內容比較多,以后可以拿出來單獨做一篇文章,這里就簡單介紹下,你只需要指定我們解決上面那些問題的原理,就是這個東西。
源碼解析
大概對WindowInsets有個了解之后,我再帶大家簡單過一遍setFitsSystemWindows的源碼,相信大家會印象更深。
public void setFitsSystemWindows(boolean fitSystemWindows) {
setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}
它這里只是設置一個flag而已,如果你看它的注釋(我這里就不帖出來了),他會把你引導到protected boolean fitSystemWindows(Rect insets)這個方法(我之后會說為什么會到這個方法)
@Deprecated
protected boolean fitSystemWindows(Rect insets) {
if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
if (insets == null) {
// Null insets by definition have already been consumed.
// This call cannot apply insets since there are none to apply,
// so return false.
return false;
}
// If we're not in the process of dispatching the newer apply insets call,
// that means we're not in the compatibility path. Dispatch into the newer
// apply insets path and take things from there.
try {
mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;
return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();
} finally {
mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;
}
} else {
// We're being called from the newer apply insets path.
// Perform the standard fallback behavior.
return fitSystemWindowsInt(insets);
}
}
(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0 這個判斷后面會簡單講,你只需要知道正常情況是執行fitSystemWindowsInt(insets)
而fitSystemWindows又是哪里調用的?往前跳,能看到是onApplyWindowInsets調用的,而onApplyWindowInsets又是由dispatchApplyWindowInsets調用的。其實到這里已經沒必要往前找了,能看出這個就是個分發機制,沒錯,這里就是WindowInsets的分發機制,和View的事件分發機制類似,再往前找就是viewgroup調用的。前面說了WindowInsets在這里不會詳細說,所以WindowInsets分發機制這里也不會去展開,你只需要先知道有那么一回事就行。
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
try {
mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
} else {
return onApplyWindowInsets(insets);
}
} finally {
mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
}
}
假設mPrivateFlags3是0,PFLAG3_APPLYING_INSETS是20,0和20做或運算,就是20。然后判斷是否有mOnApplyWindowInsetsListener,這個Listener就是我們有沒有在外面做
setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
......
return insets;
}
});
假設沒有,調用onApplyWindowInsets
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
// We weren't called from within a direct call to fitSystemWindows,
// call into it as a fallback in case we're in a class that overrides it
// and has logic to perform.
if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
} else {
// We were called from within a direct call to fitSystemWindows.
if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
}
return insets;
}
mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS就是20和40做與運算,那就是0,所以調用fitSystemWindows。
而fitSystemWindows的(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0)就是20和20做與運算,不為0,所以調用fitSystemWindowsInt。
分析到這里,就需要結合我們上面解決BUG的思路了,我們其實是要拿到Rect insets這個參數,并且修改它的top。
setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
......
return insets;
}
});
setOnApplyWindowInsetsListener回調中的insets可以拿到android.graphics.Insets這個類,但是你只能看到top是多少,沒辦法修改。當然你可以看到top是多少,然后按我上面的做法Margin設置一下
params.topMargin = -top;
如果你的布局不發生縱向變形,那倒沒有多大關系,如果有變形,那就不能用這個做法。從源碼看,這個過程主要涉及3個方法。我們能看出最好下手的地方就是fitSystemWindows。因為onApplyWindowInsets和dispatchApplyWindowInsets是分發機制的方法,你要在這里下手的話可能會出現流程混亂等問題。
所以我們這樣做來解決fitSystemWindows = true出現的頂部偏移。
@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
fitSystemWindows = true;
super.setFitsSystemWindows(fitSystemWindows);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Log.v("mmp", "測試頂部偏移量: "+insets.top);
insets.top = 0;
return super.fitSystemWindows(insets);
}
擴展
上面已經解決問題了,這里是為了擴展一下思路。
fitSystemWindows方法是protected,導致你能重寫它,但是如果這個過程我們沒辦法用繼承來實現呢?
其實這就是一個解決問題的思路,我們要知道為什么會出現這種情況,原理是什么,比如這里我們知道這個fitSystemWindows導致的頂部偏移是insets的top導致的。你得先知道這一點,不然你不知道怎么去解決這個問題,你只能去網上找別人的方法一個一個試。那我怎么知道是insets的top導致的呢?這就需要有一定的源碼閱讀能力,還要知道這個東西設計的思想是怎樣的。當你知道有這么一個東西之后,再想辦法去拿到它然后改變數據。
這里我我們是利用繼承protected方法這個特性去獲取到insets,那如果這個過程沒辦法通過繼承實現怎么辦?比如這里是因為fitSystemWindows是view的方法,而我們自定義view正好繼承view。如果它是內部自己寫的一個類去實現這個操作呢?
這種情況下一般兩種操作比較萬金油:
- 你寫一個類去繼承它那個類,然后在你寫的類里面去改insets,然后通過反射的方式把它注入給View
- 動態代理
我其實一開始改這個的想法就是用動態代理,所以馬上把代碼擼出來。
public class WebViewProxy implements InvocationHandler {
private Object relObj;
public Object newProxyInstance(Object object){
this.relObj = object;
return Proxy.newProxyInstance(relObj.getClass().getClassLoader(), relObj.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if ("fitSystemWindows".equals(method.getName()) && args != null && args.length == 1){
Log.v("mmp", "測試代理效果 "+args);
}
}catch (Exception e){
e.printStackTrace();
}
return proxy;
}
}
WebViewProxy proxy = new WebViewProxy();
View viewproxy = (View) proxy.newProxyInstance(mWebView);
然后才發現fitSystemWindows不是接口方法,白忙活一場,但是如果fitSystemWindows是接口方法的話,我這里就可以用通過動態代理加反射的操作去修改這個insets,雖然用不上,但也是個思路。最后發現可以直接重寫這個方法就行,我反倒還把問題想復雜了。
原文鏈接:https://juejin.cn/post/7154378005035352100
相關推薦
- 2022-10-09 python將寫好的程序打包成exe可執行文件_python
- 2022-04-25 C#用NPOI導出導入Excel幫助類_C#教程
- 2022-08-13 beginInvoke加回調函數lamad
- 2022-05-12 Echarts x軸標簽太長解決方案
- 2022-10-01 使用C++實現插件模式時的避坑要點(推薦)_C 語言
- 2022-03-18 C語言計算字符串最后一個單詞的長度_C 語言
- 2022-03-30 Docker搭建RabbitMQ集群的方法步驟_docker
- 2023-04-02 GoLang中的timer定時器實現原理分析_Golang
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支