在應用程式中以無邊框方式顯示內容,並在 Compose 中處理視窗插邊

Android 平台負責繪製系統 UI,例如狀態列和導覽列。無論使用者使用哪個應用程式,系統都會顯示這個 UI。

WindowInsets 會提供系統 UI 相關資訊,確保應用程式在正確的區域繪製,且 UI 不會遭系統 UI 遮蔽。

從邊到邊繪製系統資訊列後方的內容
圖 1. 從邊到邊繪製,並在系統資訊列後方繪製。

在 Android 14 (API 級別 34) 以下版本中,應用程式的 UI 預設不會在系統列下方繪製,也不會顯示缺口。

在 Android 15 (API 級別 35) 以上版本中,如果應用程式指定 SDK 35 以上版本,應用程式會在系統列下方繪製並顯示螢幕缺口。這可提供更流暢的使用者體驗,並讓應用程式充分運用可用的視窗空間。

在系統 UI 後方顯示內容稱為「無邊框」。您將在這頁面中瞭解不同類型的內嵌,以及如何進行邊緣對齊,並使用內嵌 API 為 UI 製作動畫,確保應用程式內容不會遭系統 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. 指定 SDK 35 以上版本,在 Android 15 以上版本中強制執行邊到邊。應用程式會顯示在系統 UI 後方。您可以透過處理內嵌調整應用程式的使用者介面。
  2. 您也可以選擇在 Activity.onCreate() 中呼叫 enableEdgeToEdge(),讓應用程式在舊版 Android 上呈現無邊框畫面。
  3. 在活動的 AndroidManifest.xml 項目中設定 android:windowSoftInputMode="adjustResize"。這項設定可讓應用程式以內嵌方式接收軟體 IME 的大小,以便在 IME 在應用程式中顯示和消失時,適當地填充及安排內容。

    <!-- 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
            )
        )
    }
}

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

IME 開啟時,IME 內嵌動畫會與 IME 大小相符,而 imePadding() 修飾符會開始套用底部邊距,以便在 IME 開啟時調整 LazyColumn 大小。當 imePadding() 修飾符開始套用底部邊框時,也會開始使用該內嵌量。因此,Spacer 的高度會開始降低,因為系統資訊列的部分間距已由 imePadding() 修飾符套用。當 imePadding() 修飾符套用比系統資訊列更大的底部邊框間距時,Spacer 的高度會為零。

當 IME 關閉時,變化會以相反的方向發生:一旦 imePadding() 套用的高度低於系統資訊列的底部,Spacer 就會從零高度開始擴展,直到 IME 完全動畫結束時,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

Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) {
    // content
    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}

如果需要未經消耗的原始視窗插邊,請直接使用 WindowInsets 值,或使用 WindowInsets.asPaddingValues() 傳回未受消耗影響的插邊 PaddingValues。不過,由於下列警告,請盡可能使用視窗插邊邊框間距修飾符和視窗插邊大小修飾符。

Insets 和 Jetpack Compose 階段

Compose 會使用基礎 AndroidX 核心 API 更新邊框並為其製作動畫,而邊框則會使用基礎平台 API 管理邊框。由於平台行為,內嵌內容與 Jetpack Compose 的階段有特殊關係。

內嵌值會在組合階段之後更新,但在版面配置階段之前更新。也就是說,讀取合成作業中的內嵌值時,通常會使用延遲一格內嵌的值。本頁所述的內建輔助鍵是為了延遲使用內嵌值,直到版面配置階段為止,藉此確保內嵌值會在更新時用於相同的框架。

使用 WindowInsets 的鍵盤 IME 動畫

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

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 元素,以便使用鍵盤
圖 3. 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

系統資訊列保護功能

應用程式指定目標為 SDK 35 以上版本後,系統會強制執行無邊框設計。系統狀態列和手勢導覽列為透明,但三按鈕導覽列則為半透明。

如要移除預設的半透明三按鈕操作模式背景保護措施,請將 Window.setNavigationBarContrastEnforced 設為 false

資源