聊聊視圖變化 OnGlobalLayoutListener

為什么會(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)填。

圖片是抄過(guò)來(lá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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容