Compose 中的視窗插邊

Android 平台負責繪製系統 UI,例如狀態列和導覽列。無論使用者使用哪個應用程式,都會顯示這個系統 UI。WindowInsets 提供系統 UI 相關資訊,確保應用程式繪製的區域正確無誤,且 UI 不會遭到系統 UI 遮蓋。

採用無邊框設計,在系統資訊列後方繪製
圖 1 採用無邊框設計,在系統資訊列後方繪製

根據預設,應用程式的 UI 只能顯示在系統 UI 中,例如狀態列和導覽列。這可確保應用程式的內容不會遭到系統 UI 元素遮蓋。

但是,我們建議應用程式選擇在顯示系統 UI 的區域,以便提供更順暢的使用者體驗,並讓應用程式充分利用可用的視窗空間。這也可讓應用程式與系統 UI 建立動畫,特別是在顯示及隱藏螢幕鍵盤時。

選擇在這些區域顯示內容,以及在系統 UI 後方顯示內容,就稱為「無邊框設計」。本頁面會介紹不同類型的插邊、如何選擇採用無邊框設計,以及如何使用插邊 API 為 UI 製作動畫效果,並避免對應用程式的某些部分造成遮蓋。

插邊基礎知識

當應用程式採用無邊框設計時,您必須確保系統 UI 不會遮蓋重要內容和互動。舉例來說,如果按鈕放在導覽列後方,使用者可能會無法點選。

系統 UI 的大小和位置的相關資訊是透過插邊指定。

系統 UI 的各部分都有對應的插邊類型,用於說明其大小和放置位置。舉例來說,狀態列插邊會提供狀態列的大小和位置,導覽列插邊則提供導覽列的大小和位置。每種插邊都有四個像素尺寸:頂端、左側、右側和底部。這些維度會指定系統 UI 從應用程式視窗對應兩側延伸的距離。為避免與該類型的系統 UI 重疊,應用程式 UI 必須在該類型內插入。

您可以透過 WindowInsets 取得以下內建 Android 插邊類型:

WindowInsets.statusBars

說明狀態列的插邊。這些是頂端系統 UI 列,內含通知圖示和其他指標。

WindowInsets.statusBarsIgnoringVisibility

顯示時間的狀態列插邊。如果狀態列目前處於隱藏狀態 (因為要進入沉浸模式),主要狀態列插邊將呈現空白,但這些插邊不會空白。

WindowInsets.navigationBars

說明導覽列的插邊。這些是裝置左側、右側或底部的系統 UI 列,描述工作列或導覽圖示。這些目錄在執行階段會根據使用者偏好的導覽方法和工作列互動而變更。

WindowInsets.navigationBarsIgnoringVisibility

導覽列插邊用於指定顯示時間。如果導覽列目前處於隱藏狀態 (因為進入全螢幕模式),主導覽列插邊將呈現空白,但這些插邊不會空白。

WindowInsets.captionBar

說明任意形式視窗中 (例如頂端標題列) 的系統 UI 視窗裝飾插邊。

WindowInsets.captionBarIgnoringVisibility

字幕列顯示時間的插邊。如果隱藏了說明文字列,主字幕列插邊將不會顯示,但這些插邊不會空白。

WindowInsets.systemBars

系統列插邊的聯集,包括狀態列、導覽列和說明文字列。

WindowInsets.systemBarsIgnoringVisibility

顯示時的系統資訊列插邊。如果系統資訊列目前處於隱藏狀態 (因為進入了沉浸式全螢幕模式),主系統列插邊將呈現空白,但這些插邊不會空白。

WindowInsets.ime

這些插邊用於說明軟體鍵盤所佔用的空間大小。

WindowInsets.imeAnimationSource

此插邊用於說明軟體鍵盤在目前的鍵盤動畫「之前」佔用的空間大小。

WindowInsets.imeAnimationTarget

此插邊用於說明軟體鍵盤在目前的鍵盤動畫「之後」佔用的空間。

WindowInsets.tappableElement

這是一種插邊,可描述導覽 UI 的詳細資訊,其中保留由系統處理「輕觸」的空間,而非應用程式。如果是含有手勢操作的透明導覽列,部分應用程式元素可以透過系統導覽 UI 輕觸。

WindowInsets.tappableElementIgnoringVisibility

可輕觸元素插邊顯示的時間。如果可輕觸的元素目前處於隱藏狀態 (因為進入全螢幕模式),主要可輕觸的元素插邊將呈現空白,但這些插邊不會空白。

WindowInsets.systemGestures

代表系統要攔截瀏覽手勢的插邊數量。應用程式可以透過 Modifier.systemGestureExclusion 手動指定處理有限數量的手勢。

WindowInsets.mandatorySystemGestures

系統手勢的子集一律會由系統處理,且無法透過 Modifier.systemGestureExclusion 停用。

WindowInsets.displayCutout

這些插邊代表避免與螢幕凹口 (凹口或針孔) 重疊所需的間距量。

WindowInsets.waterfall

代表瀑布螢幕曲線區域的插邊。瀑布顯示畫麵包含沿著螢幕邊緣的弧形區域,其中螢幕開始沿著裝置兩側環繞。

以下類型匯總三種「安全」插邊類型,確保內容不會遭到遮蓋:

這些「安全」插邊類型會根據基礎平台插邊,以不同的方式保護內容:

插邊設定

為了讓應用程式完全控制繪製內容的位置,請按照以下設定步驟操作。如果沒有執行這些步驟,應用程式可能會在系統 UI 後方繪製黑色或純色,或不能與螢幕鍵盤同步顯示動畫。

  1. Activity.onCreate 中呼叫 enableEdgeToEdge()。這項呼叫會要求應用程式在系統 UI 背後顯示。接著,您的應用程式會控制使用這些插邊調整 UI 的方式。
  2. 在活動的 AndroidManifest.xml 項目中設定 android:windowSoftInputMode="adjustResize"。這項設定可讓應用程式以插邊接收軟體輸入法編輯器的大小,方便您在應用程式輸入法編輯器顯示和消失時,適當地安排內容及安排內容的版面配置。

    <!-- in your AndroidManifest.xml file: -->
    <activity
      android:name=".ui.MainActivity"
      android:label="@string/app_name"
      android:windowSoftInputMode="adjustResize"
      android:theme="@style/Theme.MyApplication"
      android:exported="true">
    

Compose API

當活動掌控所有插邊後,您可以使用 Compose API,確保內容不會遭到遮蓋且可互動的元素不會與系統 UI 重疊。這些 API 也會將應用程式的版面配置與插邊變更同步處理。

舉例來說,這是將插邊套用至整個應用程式內容最基本的方法:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    enableEdgeToEdge()

    setContent {
        Box(Modifier.safeDrawingPadding()) {
            // the rest of the app
        }
    }
}

這段程式碼會將 safeDrawing 視窗插邊套用為整個應用程式內容周圍的邊框間距。雖然這麼做可確保可互動的元素不會與系統 UI 重疊,但也表示沒有任何應用程式會在系統 UI 背後繪製來達到無邊框效果。為了充分利用整個視窗,您需要針對個別畫面或個別元件,微調插邊套用的位置。

所有這些插邊類型都會由 IME 動畫向後移植至 API 21 的動畫。因此,當插邊值有所變更時,使用這些插邊的所有版面配置都會自動以動畫呈現。

調整可組合項版面配置的主要方式有兩種:邊框間距修飾符和插邊大小修飾符。

邊框間距修飾符

Modifier.windowInsetsPadding(windowInsets: WindowInsets) 會套用指定視窗插邊做為邊框間距,操作與 Modifier.padding 相同。舉例來說,Modifier.windowInsetsPadding(WindowInsets.safeDrawing) 會在所有 4 端套用安全繪圖插邊做為邊框間距。

此外,還有數種內建公用程式方法適用於最常見的插邊類型。Modifier.safeDrawingPadding() 是這類方法,相當於 Modifier.windowInsetsPadding(WindowInsets.safeDrawing)。其他插邊類型也有類似修飾符。

插邊大小修飾符

下列修飾符會將元件大小設為插邊的大小,以套用一定數量的視窗插邊:

Modifier.windowInsetsStartWidth(windowInsets: WindowInsets)

將 windowInsets 的起始端做為寬度 (例如 Modifier.width)

Modifier.windowInsetsEndWidth(windowInsets: WindowInsets)

以 windowInsets 的結尾作為寬度 (例如 Modifier.width)

Modifier.windowInsetsTopHeight(windowInsets: WindowInsets)

將 windowInsets 的頂部套用至高度 (例如 Modifier.height)

Modifier.windowInsetsBottomHeight(windowInsets: WindowInsets)

套用 windowInsets 的底部做為高度 (例如 Modifier.height)

如要調整佔用插邊空間的 Spacer 大小,這些修飾符特別實用:

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

插邊消耗量

插邊邊框間距修飾符 (windowInsetsPaddingsafeDrawingPadding 等輔助程式) 會自動取用用做邊框間距套用的插邊部分。在深入研究組合樹狀結構時,巢狀內插邊框間距修飾符和插邊大小修飾符知道外部插邊邊框間距修飾符已取用的部分部分,並避免重複使用插邊的相同部分,這會導致過多的額外空間。

如果已取用插邊,則插邊大小修飾符也避免重複使用相同部分插邊。不過,由於容器會直接變更大小,因此不會使用插邊。

因此,巢狀邊框間距修飾符會自動變更套用至每個可組合項的邊框間距量。

查看與先前相同的 LazyColumn 範例,LazyColumn 正由 imePadding 修飾符調整大小。在 LazyColumn 中,最後一個項目的大小會調整為系統資訊列底部的高度:

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

輸入法編輯器關閉時,imePadding() 修飾符不會套用邊框間距,因為輸入法編輯器沒有高度。由於 imePadding() 修飾符不會套用任何邊框間距,因此不會使用插邊,且 Spacer 的高度會是系統資訊列底部邊的大小。

IME 開啟時,IME 插邊會根據輸入法編輯器的大小建立動畫,imePadding() 修飾符會開始套用底部邊框間距,並在輸入法編輯器開啟時調整 LazyColumn 的大小。當 imePadding() 修飾符開始套用底部邊框間距時,也會開始消耗該數量的插邊。因此,Spacer 的高度會開始減少,因為 imePadding() 修飾符已套用系統列的間距部分。當 imePadding() 修飾符套用的底部邊框間距大於系統資訊列時,Spacer 的高度就會為 0。

輸入法編輯器關閉時,變更會反向發生:當 imePadding() 套用的低於系統資訊列底部後,Spacer 就會開始從高度從零展開,直到輸入法編輯器完全動畫化後,Spacer 會與系統資訊列底部側邊的高度相符。

圖 2.使用 TextField 的無邊框延遲資料欄

這項行為是透過所有 windowInsetsPadding 修飾符之間的通訊完成,並且可以透過其他幾種方式影響。

Modifier.consumeWindowInsets(insets: WindowInsets) 也會使用插邊與 Modifier.windowInsetsPadding 相同,但不會套用已耗用的插邊做為邊框間距。這適合與插邊大小修飾符搭配使用,用於表示同層已使用了一定數量的插邊:

Column(Modifier.verticalScroll(rememberScrollState())) {
    Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))

    Column(
        Modifier.consumeWindowInsets(
            WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
        )
    ) {
        // content
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
    }

    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}

Modifier.consumeWindowInsets(paddingValues: PaddingValues) 的行為與含 WindowInsets 引數的版本非常類似,但會使用任意 PaddingValues 來使用。如果邊框間距或間距是透過其他機制 (例如一般 Modifier.padding 或固定高度空格字元) 提供的其他機制提供,這就很適合用來通知子項:

@OptIn(ExperimentalLayoutApi::class)
Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) {
    // content
    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}

如果需要使用原始視窗插邊而不消耗,請直接使用 WindowInsets 值,或使用 WindowInsets.asPaddingValues() 傳回不受用量影響的插邊 PaddingValues。不過,基於下方注意事項,我們還是傾向於盡可能使用視窗插邊邊框間距修飾符和視窗插邊大小修飾符。

插邊和 Jetpack Compose 階段

Compose 會使用基礎 AndroidX 核心 API 更新插邊並加上動畫效果,這種 API 會使用管理插邊的基礎平台 API。基於這個平台行為,插邊與 Jetpack Compose 階段具有特殊關係。

插邊值的值會在組合階段「之後」更新,但會在版面配置階段「之前」更新。也就是說,讀取組合中插邊的值時,通常會使用影格時間延遲一秒的插邊值。本頁說明的內建修飾符會延遲使用插邊值到版面配置階段為止,以確保插邊值會在更新過的影格上使用。

使用 WindowInsets 的鍵盤輸入法編輯器動畫

您可以將 Modifier.imeNestedScroll() 套用至捲動容器,以便在捲動至容器底部時自動開啟及關閉輸入法編輯器。

class WindowInsetsExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                MyScreen()
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
        }
    }
}

動畫顯示的是上下捲動的 UI 元素,以便使用鍵盤

圖 1 IME 動畫

Material 3 元件的插邊支援

為方便起見,許多內建的 Material 3 可組合項 (androidx.compose.material3) 都會根據 Material 規格,根據可組合項在應用程式中放置的方式自行處理插邊。

插邊處理可組合項

以下 Material 元件清單會自動處理插邊。

應用程式列

內容容器

Scaffold

根據預設,Scaffold 會提供插邊做為參數 paddingValues,供您使用和使用。Scaffold 並不會將插邊套用至內容,您必須負責承擔相關責任。舉例來說,如要利用 Scaffold 內的 LazyColumn 使用這些插邊:

Scaffold { innerPadding ->
    // innerPadding contains inset information for you to use and apply
    LazyColumn(
        // consume insets as scaffold doesn't do it by default
        modifier = Modifier.consumeWindowInsets(innerPadding),
        contentPadding = innerPadding
    ) {
        items(count = 100) {
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .background(colors[it % colors.size])
            )
        }
    }
}

覆寫預設插邊

您可以變更傳送至可組合項的 windowInsets 參數,設定可組合項的行為。這個參數可以是不同類型的視窗插邊,也可以傳送空白執行個體來停用該參數:WindowInsets(0, 0, 0, 0)

舉例來說,如要停用 LargeTopAppBar 的插邊處理功能,請將 windowInsets 參數設為空白例項:

LargeTopAppBar(
    windowInsets = WindowInsets(0, 0, 0, 0),
    title = {
        Text("Hi")
    }
)

與 View 系統插邊互通

當您的螢幕在相同階層中同時擁有 View 和 Compose 程式碼時,您可能需要覆寫預設插邊。在這種情況下,您必須明確指出在何者能使用插邊,哪些應忽略插邊。

舉例來說,如果最外層版面配置是 Android View 版面配置,您應該使用 View 系統中的插邊,並針對 Compose 忽略這些插邊。或者,如果最外層版面配置是可組合項,您應使用 Compose 中的插邊,然後據此為 AndroidView 可組合項填入內容。

根據預設,每個 ComposeView 都會在耗用 WindowInsetsCompat 層級使用所有插邊。如要變更這個預設行為,請將 ComposeView.consumeWindowInsets 設為 false

資源

  • Now in Android - 完全使用 Kotlin 和 Jetpack Compose 建構的 Android 應用程式,功能齊全。