為什么會(huì)想起聊聊這個(gè)吶?
最近公司在用Dcloud開(kāi)發(fā)單頁(yè)面應(yīng)用,原生做殼,說(shuō)沒(méi)坑你信嗎?
沉浸模式下,軟鍵盤(pán)擋住輸入框:
常規(guī)情況下,我們通過(guò)設(shè)置AndroidManifest中Activity節(jié)點(diǎn)下的android:windowSoftInputMode屬性,解決輸入框的適應(yīng)問(wèn)題。
該屬性有兩個(gè)值可供使用:
-
djustPan:把整個(gè)界面向上平移,使輸入框露出,不會(huì)改變界面的布局。 -
adjustResize:重新計(jì)算彈出軟鍵盤(pán)之后的界面大小,相當(dāng)于是用更少的界面區(qū)域去顯示內(nèi)容,輸入框一般自然也就在內(nèi)了。
但是這些方法在此時(shí)并沒(méi)有生效,通過(guò) Android爬坑之旅:軟鍵盤(pán)擋住輸入框問(wèn)題的終極解決方案 得知,這是android系統(tǒng)的一個(gè)坑,該bug在09年就被提出,至今已有十年還沒(méi)填。

好在文章提供的解決方案,實(shí)測(cè)也是有效的,這里說(shuō)一下大致流程:
- 首先獲取到Activity的根布局;
- 然后為根布局,添加布局變化監(jiān)聽(tīng);
- 獲取可視內(nèi)容區(qū)域(有效區(qū)域)的高度;
- 通過(guò)“屏幕”高度和可視高度的的差值關(guān)系,重新設(shè)置跟布局高度。
輸入框的問(wèn)題是解決了,可是又遇到了新的的問(wèn)題:
- 未兼容虛擬導(dǎo)航欄變化的情況,高度不變;
- 頻繁切換橫豎屏,也會(huì)導(dǎo)致高度異常,出現(xiàn)高度值等于寬度值的問(wèn)題。
下面是改進(jìn)后代碼,解決了上述問(wèn)題,滿足單頁(yè)面下的復(fù)雜需求:
class GlobalLayoutUtils(activity: Activity, private var isImmersed: Boolean = true) {
// 當(dāng)前界面根布局,就是我們?cè)O(shè)置的 setContentView()
private var mChildOfContent: View
private var frameLayoutParams: FrameLayout.LayoutParams
// 變化前的試圖高度
private var usableHeightPrevious = 0
init {
val content: FrameLayout = activity.findViewById(android.R.id.content)
mChildOfContent = content.getChildAt(0)
// 添加布局變化監(jiān)聽(tīng)
mChildOfContent.viewTreeObserver.addOnGlobalLayoutListener {
possiblyResizeChildOfContent(activity)
}
frameLayoutParams = mChildOfContent.layoutParams as FrameLayout.LayoutParams
}
private fun possiblyResizeChildOfContent(activity: Activity) {
// 當(dāng)前可視區(qū)域的高度
val usableHeightNow = computeUsableHeight()
// 當(dāng)前高度值和之前的進(jìn)行對(duì)比,變化將進(jìn)行重繪
if (usableHeightNow != usableHeightPrevious) {
// 獲取當(dāng)前屏幕高度
// Ps:并不是真正的屏幕高度,是當(dāng)前app的窗口高度,分屏?xí)r的高度為分屏窗口高度
var usableHeightSansKeyboard = mChildOfContent.rootView.height
// 高度差值:屏幕高度 - 可視內(nèi)容高度
val heightDifference = usableHeightSansKeyboard - usableHeightNow
// 差值為負(fù),說(shuō)明獲取屏幕高度時(shí)出錯(cuò),寬高狀態(tài)值反了,重新計(jì)算
if (heightDifference < 0) {
usableHeightSansKeyboard = mChildOfContent.rootView.width
heightDifference = usableHeightSansKeyboard - usableHeightNow
}
// 如果差值大于屏幕高度的 1/4,則認(rèn)為輸入軟鍵盤(pán)為彈出狀態(tài)
if (heightDifference > usableHeightSansKeyboard / 4) {
// keyboard probably just became visible
// 設(shè)置布局高度為:屏幕高度 - 高度差
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference
} else {
// keyboard probably just became hidden
if (heightDifference + 1 >= DisplayUtils.getNavigationBarHeight(activity)) {
// 如果高度差大于導(dǎo)航欄高度,則認(rèn)為此時(shí)虛擬導(dǎo)航欄顯示
frameLayoutParams.height =
usableHeightSansKeyboard - DisplayUtils.getNavigationBarHeight(activity)
} else {
// 其他情況直接設(shè)置為可視高度即可
frameLayoutParams.height = usableHeightNow
}
}
// 刷新布局,會(huì)重新測(cè)量、繪制
mChildOfContent.requestLayout()
// 保存高度信息
usableHeightPrevious = usableHeightNow
}
}
/**
* 獲取可視內(nèi)容區(qū)域的高度
*/
private fun computeUsableHeight(): Int {
val r = Rect()
// 當(dāng)前窗口可視區(qū)域,不包括通知欄、導(dǎo)航欄、輸入鍵盤(pán)區(qū)域
mChildOfContent.getWindowVisibleDisplayFrame(r)
return if (isImmersed) {
// 沉浸模式下,底部坐標(biāo)就是內(nèi)容有效高度
r.bottom
} else {
// 非沉浸模式下,去掉通知欄的高度 r.top(可用于通知欄高度的計(jì)算)
r.bottom - r.top
}
}
}
下面是虛擬導(dǎo)航欄所用到的工具類(lèi):
class DisplayUtils {
companion object {
// 獲取系統(tǒng)導(dǎo)航欄的高度(可能未顯示)
fun getNavigationBarHeight(context: Context): Int {
var result = 0
val resources = context.resources
val resourceId =
resources.getIdentifier("navigation_bar_height", "dimen", "android")
if (resourceId > 0) {
result = resources.getDimensionPixelSize(resourceId)
}
return result
}
// 獲取導(dǎo)航欄當(dāng)前的高度
fun getNavigationBarCurrentHeight(activity: Activity) =
if (isNavigationBarShow(activity)) {
getNavigationBarHeight(activity)
} else {
0
}
// 判斷當(dāng)前導(dǎo)航欄是否顯示,兼容華為手機(jī)
@SuppressLint("ObsoleteSdkInt")
fun isNavigationBarShow(activity: Activity): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val display = activity.windowManager.defaultDisplay
val size = Point()
val realSize = Point()
display.getSize(size)
display.getRealSize(realSize)
realSize.y != size.y
} else {
val menu = ViewConfiguration.get(activity).hasPermanentMenuKey()
val back = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK)
!(menu || back)
}
}
}
使用起來(lái)也很方便,直接 GlobalLayoutUtils(this) 就可以了,第二個(gè)參數(shù)是可選參數(shù)。
至于代碼中的有一點(diǎn)說(shuō)一下:
// 差值為負(fù),說(shuō)明獲取屏幕高度時(shí)出錯(cuò),寬高狀態(tài)值反了,重新計(jì)算
if (heightDifference < 0) {
usableHeightSansKeyboard = mChildOfContent.rootView.width
heightDifference = usableHeightSansKeyboard - usableHeightNow
}
當(dāng)高度差值為負(fù)的時(shí)候,即使不重新取值、計(jì)算,最終的結(jié)果也是正確的。
因?yàn)槠聊恍D(zhuǎn)的時(shí)候,輸入法肯定是隱藏狀態(tài),最終會(huì)取值為有效內(nèi)容高度。但為了邏輯的清晰,和代碼的健壯,予以保留。
下面附上原文鏈接:
Android爬坑之旅:軟鍵盤(pán)擋住輸入框問(wèn)題的終極解決方案
還有bug的issues地址:
WebView adjustResize windowSoftInputMode breaks when activity is fullscreen