Android 輸入框被擋問(wèn)題完美解決方案
前言
前段時(shí)間出現(xiàn)了webview的輸入框被軟鍵盤(pán)擋住的問(wèn)題,處理之后順便對(duì)一些列的輸入框被擋住的情況進(jìn)行一個(gè)總結(jié)。
正常情況下的輸入框被擋
正常情況下,輸入框被輸入法擋住,一般給window設(shè)softInputMode就能解決。
window.getAttributes().softInputMode = WindowManager.LayoutParams.XXX
有3種情況:
(1)SOFT_INPUT_ADJUST_RESIZE: 布局會(huì)被軟鍵盤(pán)頂上去
(2)SOFT_INPUT_ADJUST_PAN:只會(huì)把輸入框給頂上去(就是只頂一部分距離)
(3)SOFT_INPUT_ADJUST_NOTHING:不做任何操作(就是不頂)
SOFT_INPUT_ADJUST_PAN和SOFT_INPUT_ADJUST_RESIZE的不同在于SOFT_INPUT_ADJUST_PAN只是把輸入框,而SOFT_INPUT_ADJUST_RESIZE會(huì)把整個(gè)布局頂上去,這就會(huì)有種布局高度在輸入框展示和隱藏時(shí)高度動(dòng)態(tài)變化的視覺(jué)效果。
如果你是出現(xiàn)了輸入框被擋的情況,一般設(shè)置SOFT_INPUT_ADJUST_PAN就能解決。如果你是輸入框沒(méi)被擋,但是軟鍵盤(pán)彈出的時(shí)候會(huì)把布局往上頂,如果你不希望往上頂,可以設(shè)置SOFT_INPUT_ADJUST_NOTHING。
softInputMode是window的屬性,你給在Mainifest給Activity設(shè)置,也是設(shè)給window,你如果是Dialog或者popupwindow這種,就直接getWindow()來(lái)設(shè)置就行。正常情況下設(shè)置這個(gè)屬性就能解決問(wèn)題。
Webview的輸入框被擋
但是Webview的輸入框被擋的情況下,設(shè)這個(gè)屬性有可能會(huì)失效。
Webview的情況下,SOFT_INPUT_ADJUST_PAN會(huì)沒(méi)效果,然后,如果是Webview并且你還開(kāi)沉浸模式的情況的話(huà),SOFT_INPUT_ADJUST_RESIZE和SOFT_INPUT_ADJUST_PAN都會(huì)不起作用。
我去查看資料,發(fā)現(xiàn)這就是經(jīng)典的issue 5497, 網(wǎng)上很多的解決方案就是通過(guò)AndroidBug5497Workaround,這個(gè)方案很容易能查到,我就不貼出來(lái)了,原理就是監(jiān)聽(tīng)View樹(shù)的變化,然后再計(jì)算高度,再去動(dòng)態(tài)設(shè)置。這個(gè)方案的確能解決問(wèn)題,但是我覺(jué)得這個(gè)操作不可控的因素比較多,說(shuō)白了就是會(huì)不會(huì)某種機(jī)型或者情況下使用會(huì)出現(xiàn)其它的BUG,導(dǎo)致你需要寫(xiě)一些判斷邏輯來(lái)處理特殊的情況。
解法就是不用沉浸模式然后使用SOFT_INPUT_ADJUST_RESIZE就能解決。但是有時(shí)候這個(gè)window顯示的時(shí)候就需要沉浸模式,特別是一些適配劉海屏、水滴屏這些場(chǎng)景。
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
那我的第一反應(yīng)就是改變布局
window. setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
這樣是能正常把彈框頂上去,但是控件內(nèi)部用的也是WRAP_CONTENT導(dǎo)致SOFT_INPUT_ADJUST_RESIZE改變布局之后就恢復(fù)不了原樣,也就是會(huì)變形。而不用WRAP_CONTENT用固定高度的話(huà),SOFT_INPUT_ADJUST_RESIZE也是失效的。
沒(méi)事,還要辦法,在MATCH_PARENT的情況下我們?nèi)ピO(shè)置fitSystemWindows為true,但是這個(gè)屬性會(huì)讓出一個(gè)頂部的安全距離,效果就是向下偏移了一個(gè)狀態(tài)欄的高度。
這種情況下你可以去設(shè)置margin來(lái)解決這個(gè)頂部偏移的問(wèn)題。
params.topMargin = statusHeight == 0 ? -120 : -statusHeight; view.setLayoutParams(params);
這樣的操作是能解除頂部偏移的問(wèn)題,但是布局有可能被縱向壓縮,這個(gè)我沒(méi)完全測(cè)試過(guò),我覺(jué)得如果你布局高度是固定的,可能不會(huì)受到影響,但我的webview是自適應(yīng)的,webview里面的內(nèi)容也是自適應(yīng)的,所以我這出現(xiàn)了布局縱向壓縮的情況。
舉個(gè)例子,你的view的高度是800,狀態(tài)欄高度是100,那設(shè)fitSystemWindows之后的效果就是view顯示700,paddingTop 100,這樣的效果,設(shè)置params.topMargin =-100,之后,view顯示700,paddingTop 100。大概是這個(gè)意思:能從視覺(jué)上消除頂部偏移,但是布局縱向被壓縮的問(wèn)題沒(méi)得到處理
所以最終的解決方法是改WindowInsets的Rect (這個(gè)我等下會(huì)再解釋是什么意思)
具體的操作就是在你的自定義view中加入下面兩個(gè)方法
@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
fitSystemWindows = true;
super.setFitsSystemWindows(fitSystemWindows);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Log.v("mmp", "測(cè)試頂部偏移量: "+insets.top);
insets.top = 0;
return super.fitSystemWindows(insets);
}
小結(jié)
解決WebView+沉浸模式下輸入框被軟鍵盤(pán)擋住的步驟:
- window.getAttributes().softInputMode設(shè)置成SOFT_INPUT_ADJUST_RESIZE
- 設(shè)置view的fitSystemWindows為true,我這里是webview里面的輸入框被擋住,設(shè)的就是webview而不是父View
- 重寫(xiě)fitSystemWindows方法,把insets的top設(shè)為0
WindowInsets
根據(jù)上面的3步操作,你就能處理webview輸入框被擋的問(wèn)題,但是如果你想知道為什么,這是什么原理。你就需要去了解WindowInsets。我們的沉浸模式的操作setSystemUiVisibility和設(shè)置fitSystemWindows屬性,還有重寫(xiě)fitSystemWindows方法,都和WindowInsets有關(guān)。
WindowInsets是應(yīng)用于窗口的系統(tǒng)視圖的插入。例如狀態(tài)欄STATUS_BAR和導(dǎo)航欄NAVIGATION_BAR。它會(huì)被view引用,所以我們要做具體的操作,是對(duì)view進(jìn)行操作。
還有一個(gè)比較重要的問(wèn)題,WindowInsets的不同版本都是有一定的差別,Android28、Android29、Android30都有一定的差別,例如29中有個(gè)android.graphics.Insets類(lèi),這是28里面沒(méi)有的,我們可以在29中拿到它然后查看top、left等4個(gè)屬性,但是只能查看,它是final的,不能直接拿出來(lái)修改。
但是WindowInsets這塊其實(shí)能講的內(nèi)容比較多,以后可以拿出來(lái)單獨(dú)做一篇文章,這里就簡(jiǎn)單介紹下,你只需要指定我們解決上面那些問(wèn)題的原理,就是這個(gè)東西。
源碼解析
大概對(duì)WindowInsets有個(gè)了解之后,我再帶大家簡(jiǎn)單過(guò)一遍setFitsSystemWindows的源碼,相信大家會(huì)印象更深。
public void setFitsSystemWindows(boolean fitSystemWindows) {
setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}
它這里只是設(shè)置一個(gè)flag而已,如果你看它的注釋?zhuān)ㄎ疫@里就不帖出來(lái)了),他會(huì)把你引導(dǎo)到protected boolean fitSystemWindows(Rect insets)這個(gè)方法(我之后會(huì)說(shuō)為什么會(huì)到這個(gè)方法)
@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 這個(gè)判斷后面會(huì)簡(jiǎn)單講,你只需要知道正常情況是執(zhí)行fitSystemWindowsInt(insets)
而fitSystemWindows又是哪里調(diào)用的?往前跳,能看到是onApplyWindowInsets調(diào)用的,而onApplyWindowInsets又是由dispatchApplyWindowInsets調(diào)用的。其實(shí)到這里已經(jīng)沒(méi)必要往前找了,能看出這個(gè)就是個(gè)分發(fā)機(jī)制,沒(méi)錯(cuò),這里就是WindowInsets的分發(fā)機(jī)制,和View的事件分發(fā)機(jī)制類(lèi)似,再往前找就是viewgroup調(diào)用的。前面說(shuō)了WindowInsets在這里不會(huì)詳細(xì)說(shuō),所以WindowInsets分發(fā)機(jī)制這里也不會(huì)去展開(kāi),你只需要先知道有那么一回事就行。
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;
}
}
假設(shè)mPrivateFlags3是0,PFLAG3_APPLYING_INSETS是20,0和20做或運(yùn)算,就是20。然后判斷是否有mOnApplyWindowInsetsListener,這個(gè)Listener就是我們有沒(méi)有在外面做
setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
......
return insets;
}
});
假設(shè)沒(méi)有,調(diào)用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做與運(yùn)算,那就是0,所以調(diào)用fitSystemWindows。
而fitSystemWindows的(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0)就是20和20做與運(yùn)算,不為0,所以調(diào)用fitSystemWindowsInt。
分析到這里,就需要結(jié)合我們上面解決BUG的思路了,我們其實(shí)是要拿到Rect insets這個(gè)參數(shù),并且修改它的top。
setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
......
return insets;
}
});
setOnApplyWindowInsetsListener回調(diào)中的insets可以拿到android.graphics.Insets這個(gè)類(lèi),但是你只能看到top是多少,沒(méi)辦法修改。當(dāng)然你可以看到top是多少,然后按我上面的做法Margin設(shè)置一下
params.topMargin = -top;
如果你的布局不發(fā)生縱向變形,那倒沒(méi)有多大關(guān)系,如果有變形,那就不能用這個(gè)做法。從源碼看,這個(gè)過(guò)程主要涉及3個(gè)方法。我們能看出最好下手的地方就是fitSystemWindows。因?yàn)閛nApplyWindowInsets和dispatchApplyWindowInsets是分發(fā)機(jī)制的方法,你要在這里下手的話(huà)可能會(huì)出現(xiàn)流程混亂等問(wèn)題。
所以我們這樣做來(lái)解決fitSystemWindows = true出現(xiàn)的頂部偏移。
@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
fitSystemWindows = true;
super.setFitsSystemWindows(fitSystemWindows);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Log.v("mmp", "測(cè)試頂部偏移量: "+insets.top);
insets.top = 0;
return super.fitSystemWindows(insets);
}
擴(kuò)展
上面已經(jīng)解決問(wèn)題了,這里是為了擴(kuò)展一下思路。
fitSystemWindows方法是protected,導(dǎo)致你能重寫(xiě)它,但是如果這個(gè)過(guò)程我們沒(méi)辦法用繼承來(lái)實(shí)現(xiàn)呢?
其實(shí)這就是一個(gè)解決問(wèn)題的思路,我們要知道為什么會(huì)出現(xiàn)這種情況,原理是什么,比如這里我們知道這個(gè)fitSystemWindows導(dǎo)致的頂部偏移是insets的top導(dǎo)致的。你得先知道這一點(diǎn),不然你不知道怎么去解決這個(gè)問(wèn)題,你只能去網(wǎng)上找別人的方法一個(gè)一個(gè)試。那我怎么知道是insets的top導(dǎo)致的呢?這就需要有一定的源碼閱讀能力,還要知道這個(gè)東西設(shè)計(jì)的思想是怎樣的。當(dāng)你知道有這么一個(gè)東西之后,再想辦法去拿到它然后改變數(shù)據(jù)。
這里我我們是利用繼承protected方法這個(gè)特性去獲取到insets,那如果這個(gè)過(guò)程沒(méi)辦法通過(guò)繼承實(shí)現(xiàn)怎么辦?比如這里是因?yàn)閒itSystemWindows是view的方法,而我們自定義view正好繼承view。如果它是內(nèi)部自己寫(xiě)的一個(gè)類(lèi)去實(shí)現(xiàn)這個(gè)操作呢?
這種情況下一般兩種操作比較萬(wàn)金油:
- 你寫(xiě)一個(gè)類(lèi)去繼承它那個(gè)類(lèi),然后在你寫(xiě)的類(lèi)里面去改insets,然后通過(guò)反射的方式把它注入給View
- 動(dòng)態(tài)代理
我其實(shí)一開(kāi)始改這個(gè)的想法就是用動(dòng)態(tài)代理,所以馬上把代碼擼出來(lái)。
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", "測(cè)試代理效果 "+args);
}
}catch (Exception e){
e.printStackTrace();
}
return proxy;
}
}
WebViewProxy proxy = new WebViewProxy(); View viewproxy = (View) proxy.newProxyInstance(mWebView);
然后才發(fā)現(xiàn)fitSystemWindows不是接口方法,白忙活一場(chǎng),但是如果fitSystemWindows是接口方法的話(huà),我這里就可以用通過(guò)動(dòng)態(tài)代理加反射的操作去修改這個(gè)insets,雖然用不上,但也是個(gè)思路。最后發(fā)現(xiàn)可以直接重寫(xiě)這個(gè)方法就行,我反倒還把問(wèn)題想復(fù)雜了。
以上就是Android 輸入框被擋問(wèn)題完美解決方案的詳細(xì)內(nèi)容,更多關(guān)于A(yíng)ndroid 輸入框被擋的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義View葉子旋轉(zhuǎn)完整版(六)
這篇文章主要為大家詳細(xì)介紹了Android自定義View葉子旋轉(zhuǎn)完整版,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android貝塞爾曲線(xiàn)初步學(xué)習(xí)第二課 仿QQ未讀消息氣泡拖拽黏連效果
這篇文章主要為大家詳細(xì)介紹了Android貝塞爾曲線(xiàn)初步學(xué)習(xí)的第二課,仿QQ未讀消息氣泡拖拽黏連效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android實(shí)現(xiàn)超級(jí)棒的沉浸式體驗(yàn)教程
這篇文章主要給大家介紹了關(guān)于A(yíng)ndroid如何實(shí)現(xiàn)超級(jí)棒的沉浸式體驗(yàn)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Android具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11
Android控件PullRefreshViewGroup實(shí)現(xiàn)下拉刷新和上拉加載
這篇文章主要為大家詳細(xì)介紹了Android控件PullRefreshViewGroup實(shí)現(xiàn)下拉刷新和上拉加載效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android虛擬機(jī)與類(lèi)加載機(jī)制詳情
這篇文章主要介紹了Android虛擬機(jī)與類(lèi)加載機(jī)制詳情,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
Android ListView position詳解及實(shí)例代碼
這篇文章主要介紹了Android ListView position的相關(guān)資料,在開(kāi)發(fā)Android 應(yīng)用的時(shí)候你真的用對(duì)了嗎?這里給大家徹底解釋下,需要的朋友可以參考下2016-10-10
android 多線(xiàn)程技術(shù)應(yīng)用
能夠在屏幕上“實(shí)時(shí)地顯示”時(shí)間的流逝,單線(xiàn)程程序是無(wú)法實(shí)現(xiàn)的,必須要多線(xiàn)程程序才可以實(shí)現(xiàn),即便有些計(jì)算機(jī)語(yǔ)言可以通過(guò)封裝好的類(lèi)實(shí)現(xiàn)這一功能,但從本質(zhì)上講這些封裝好的類(lèi)就是封裝了一個(gè)線(xiàn)程,具體祥看本文2012-12-12
Android超詳細(xì)講解組件ScrollView的使用
本節(jié)帶來(lái)的是Android基本UI控件中的第十個(gè):ScrollView(滾動(dòng)條),或者我們應(yīng)該叫他?豎直滾動(dòng)條,對(duì)應(yīng)的另外一個(gè)水平方向上的滾動(dòng)條:HorizontalScrollView,先讓我們來(lái)了解ScrollView2022-03-03

