Jetpack Compose處理“導航欄、狀態列、鍵盤” 影響內容顯示的問題集錦

語言: CN / TW / HK

theme: arknights highlight: androidstudio


1.前言

寫Compose相關例子的時候,突然不怎麼使用xml,有些東西不清楚怎麼下手,就比如Compose中狀態列,導航欄沉浸,鍵盤遮擋等問題如何處理,對這方面不清楚的同學,請往下翻看看我們如何去處理它的
前方高能預警:一定要記得收藏起來,划走了可就再也找不到了😅😅🙈🙈

2.初始態

預設建立一個工程,新增如下程式碼,頁面除了“內容區域”之外,還有“導航欄、狀態列” kotlin setContent { Surface(color = Color(0xFFF74C4C),modifier = Modifier.fillMaxSize()){} }

我們可以看到狀態列顏色是style.xml中配置的,導航欄不處理的話,預設是黑色
我們下面來看Compose中如何處理“導航欄、狀態列、鍵盤”遮擋的問題

3.思考並解決

3.1-內容延伸到狀態列

上面的紅色背景有點突兀,不方便對比,我們用圖片來代替紅色背景
首先把狀態列顏色設定透明,下面提供大家簡單的三種方式來設定狀態列顏色,原理都是一樣的,你隨便使用哪種方式都可以
- :style.xml設定狀態列透明色 xml <item name="android:statusBarColor">@android:color/transparent</item> - :window設定狀態列透明色

kotlin window.statusBarColor = ResourcesCompat.getColor(resources,android.R.color.transparent,null) - :systemuicontroller設定狀態列透明色 我們需要使用這個元件庫: Jetpack Compose - Accompanist 元件庫

gradle implementation "com.google.accompanist:accompanist-systemuicontroller:<version>" 程式碼使用如下,原理也是一樣的最終都是通過window來設定的 kotlin val systemUiController = rememberSystemUiController() systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false) 我們要把內容延伸到狀態列,可以使用如下方法:

kotlin WindowCompat.setDecorFitsSystemWindows(window,false)

3.2-狀態列遮擋列表問題

上面我們使用圖片可以看到,效果還是不錯的,如果不是圖片,我們是列表,這個時候第一位的內容是不是跑到螢幕外面了?
如果列表足夠長,最後一個條目也無法完整顯示因為第一個條目頂出到狀態列外面了


列表“第一個”和“最後一個”都無法正常顯示

我們來一步一步的解決這個問題,我們先解決狀態列遮擋第一個條目的內容;
我們需要有一個狀態列這麼高的大小,作為內邊距來保證安全邊距,我們在使用LazyColumn的時候,可以看到內部有個contentPadding屬性:為整個內容新增的內部填充,不是針對單獨的item進行填充的;

點選檢視Compose如何把px轉dp,dp轉px

那麼我們如何獲取“狀態列”的高度呢?
在Compose中有兩種方式獲取:

```kotlin //方式一:系統狀態列的高度 fun Resources.getStatusBarHeight():Int { var statusBarHeight = 0 val resourceId = getIdentifier("status_bar_height", "dimen", "android") if(resourceId >0 ){ statusBarHeight = getDimensionPixelSize(resourceId) } return statusBarHeight }

//方式二:系統狀態列的高度 //1.先依賴lib庫 implementation "com.google.accompanist:accompanist-insets:" //2.依賴lib庫之後,使用ProvideWindowInsets可以訪問LocalWindowInsets.current值 //本質是通過CompositionLocalProvider傳值的 ProvideWindowInsets { //這個返回的型別是PaddingValues,列印一下就可以看到狀態列的高度了 rememberInsetsPaddingValues(LocalWindowInsets.current.statusBars) } ``` 使用示例如下:

```kotlin ProvideWindowInsets { //方式一獲取到的“狀態列高度” //注意:此種方式獲取到,設定僅PaddingValues是四個方向全部都有值 /val sbPaddingValues = PaddingValues(with(LocalDensity.current) { LocalContext.current.resources.getStatusBarHeight().toDp() })/

//方式二獲取到的“狀態列高度”
//注意:這個方式獲取到的PaddingValues只有頂部狀態列方向有值,其他方向為0.dp
val sbPaddingValues = rememberInsetsPaddingValues(LocalWindowInsets.current.statusBars)
LazyColumn(modifier = Modifier.fillMaxSize()
    .background(Color(0xFFDA8E70)),
    contentPadding = sbPaddingValues
) {
    items(12) { index ->
        Text(
            text = "Item:$index", color = Color.Black, fontSize = 20.sp,
            modifier = Modifier.padding(5.dp).height(60.dp)
        )
        Divider()
    }
}

} ```


狀態列遮擋問題處理

我們可以看到狀態列不再遮擋,但是導航欄會遮擋,上面我們使用的是方式二,只有頂部才會有值,不建議設定四個方向都是狀態列高度,設定之後就會影響介面其他元素邊距,那麼如何正確的讓底部導航欄不遮擋住列表最後一個條目呢?請往下看

3.3-導航欄遮擋列表問題

看完上面狀態列遮擋的處理方法之後,導航欄不也是同樣嗎?稍微不同的是,我們不能在通過類似resId方式獲取,現在手機都可以動態更換狀態列了,所以我們還是使用LocalWindowInsets來動態獲取

kotlin ProvideWindowInsets { //獲取導航欄的高度 val navPaddingValues = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars) LazyColumn(modifier = Modifier .fillMaxSize() .background(Color(0xFFDA8E70)), contentPadding = navPaddingValues ) { ...... } }


導航欄遮擋問題處理

可能有人看到這裡突然來了句,底部怎麼這麼大的距離?正常嗎?請仔細看一下,每個item的高度都是一樣的哦,仔細看一下是正常的;

我們把導航欄背景色設定為透明色,再來看一下效果,效果更明顯

kotlin val systemUiController = rememberSystemUiController() //設定導航欄透明色 systemUiController.setNavigationBarColor(Color.Transparent, darkIcons = false)


導航欄遮擋問題處理

3.4-同時處理狀態列和導航欄遮擋

我們使用LocalWindowInsets.current.systemBars來給contentPadding賦值,它可以同時獲取“狀態列、導航欄”所佔的高度,防止遮擋,這個方法不含鍵盤高度


同時處理 狀態列和導航欄 遮擋問題

3.5-Modifier方式

為什麼使用這個方式呢?不是所有控制元件都有contentPadding屬性可以使用,那麼我們可以通過Modifier修飾符來處理,我們同時新增navigationBarPadding()和statusBarsPadding(),或者只新增一個systemBarsPadding()(這個方法不含鍵盤高度)來處理遮擋問題,本質上內部是使用了Modifier.padding kotlin ProvideWindowInsets { LazyColumn(modifier = Modifier .fillMaxSize() .background(Color(0xFFDA8E70)).systemBarsPadding() ) { ...... } } 但是Modifier的方式無法讓內容延伸到“狀態列和導航欄”下方,檢視整體是在“狀態列和導航欄”上方


Modifier.systemBarsPadding() 效果

3.6-處理鍵盤遮擋問題

我們上面提到鍵盤高度,那麼鍵盤會有什麼問題呢?我們先看下面這樣的一個例子,以經典的文字輸入框為例

```kotlin @Composable fun LoginTextField( name : String, updateName : (String) -> Unit, pwd : String, updatePwd : (String) -> Unit ){ Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom, //先讓它在導航欄上方顯示,否則會顯示在導航欄下方 //因為我們文章上面設定了內容延伸 modifier = Modifier.fillMaxSize().navigationBarsPadding() ) { OutlinedTextField( value = name, onValueChange = updateName , label = { Text("使用者名稱") }, placeholder = { Text(text = "請輸入使用者名稱") }, ...... ) OutlinedTextField( value = pwd, onValueChange = updatePwd , label = { Text("密碼") }, placeholder = { Text(text = "請輸入密碼") }, ...... keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), //隱藏密碼內容 visualTransformation = PasswordVisualTransformation('*') ) } }

//使用如下: ProvideWindowInsets { var name by rememberSaveable { mutableStateOf("") } val updateName = { _name : String -> name = _name } var password by rememberSaveable { mutableStateOf("") } val updatePassword = { _pwd : String -> password = _pwd } LoginTextField( name = name, updateName = updateName, pwd = password, updatePwd = updatePassword ) } ``` 看一下,上面例子出現的問題,動圖如下:


鍵盤遮擋問題

很明顯,我們需要知道鍵盤的高度,才能做到不遮擋,我們可以使用Modifier.navigationBarsWithImePadding() 來做到安全不遮擋,會自動計算鍵盤開啟和關閉以及導航欄的高度最大值;

我們看替換完之後的效果,一定要注意一個事情,不要因為使用了Compose而把AndroidManifest.xml遺忘了,一定要給你所在的Activity配置如下屬性

xml android:windowSoftInputMode="adjustResize" 如有其他疑惑的小夥伴,可以在評論區留言


鍵盤遮擋問題修復

4.總結

(1). 我們需要如下兩個Lib庫幫助我們

gradle implementation "com.google.accompanist:accompanist-systemuicontroller:<version>" implementation "com.google.accompanist:accompanist-insets:<version>" (2). 狀態列和導航欄變色

```kotlin val systemUiController = rememberSystemUiController() //分開設定,考慮到背景顏色,我們需要動態更新圖示顏色嘛 systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true) systemUiController.setNavigationBarColor(Color.Transparent, darkIcons = false)

//或者使用,直接統一兩個欄 systemUiController.setSystemBarsColor(.....) ``` (2). 如果是列表,需要內容延伸出狀態列和導航欄,可以使用contentPadding屬性,設定內容邊距

kotlin ProvideWindowInsets { //做到導航欄和狀態列都可以延伸內容 val paddingValues = rememberInsetsPaddingValues(LocalWindowInsets.current.systemBars) LazyColumn(modifier = Modifier .fillMaxSize() .background(Color(0xFFDA8E70)), contentPadding = navPaddingValues ) { ...... } } (3). 如果使用Modifier方式處理遮擋問題,無法做到內容延伸出“狀態列和導航欄”

kotlin Modifier.navigationBarsPadding() Modifier.statusBarsPadding() Modifier.systemBarsPadding() (4). 鍵盤遮擋問題

kotlin AndroidManifer.xml配置: android:windowSoftInputMode="adjustResize" //防止鍵盤遮擋,文字輸入框 Modifier.navigationBarsWithImePadding()

有些小夥伴可能看完會有個疑問:列表使用contentPadding的時候和Modifier.padding內部做什麼,導致它們效果不同的呢?
感興趣的同學看一下:LazyListMeasure裡面的calculateItemsOffsets方法


往期文章推薦:
1.Android跨程序傳大圖思考及實現——附上原理分析
2.閒聊Android懸浮的“系統文字選擇選單”和“ActionMode解析”——附上原理分析 3.Jetpack Compose實現bringToFront功能——附上原理分析
4.Jetpack Compose UI建立佈局繪製流程+原理 —— 內含概念詳解(滿滿乾貨)
5.Jetpack App Startup如何使用及原理分析
6.Jetpack Compose - Accompanist 元件庫
7.原始碼分析 | ThreadedRenderer空指標問題,順便把Choreographer認識一下
8.原始碼分析 | 事件是怎麼傳遞到Activity的?
9.聊聊CountDownLatch 原始碼
10.Android正確的保活方案,不要掉進保活需求死迴圈陷進