在應用程式中以無邊框方式顯示內容,並在 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
            )
        )
    }
}

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

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

當 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 會使用管理插邊的基礎平台 API。由於平台行為,內嵌區與 Jetpack Compose 的階段有特殊關係。

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

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

您可以將 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

資源