因應 Android 15 強制採用的無邊框措施

1. 事前準備

如要瞭解如何使用各種 Android 平台 API,實作社群網路應用程式中的常見功能,SociaLite 是絕佳的參考範例。這款應用程式運用各式 Jetpack API 實現複雜功能,不僅可在更多裝置上穩定運作,也能減少編寫程式碼的需求。

本程式碼研究室會逐步引導您製作與 Android 15 無邊框強制措施相容的 SociaLite 應用程式,並確保這款無邊框應用程式能回溯相容。採用無邊框設計後,SociaLite 會如下圖所示,具體情形因裝置和操作模式而異:

三按鈕操作模式下的 SociaLite 應用程式。

手勢操作模式下的 SociaLite 應用程式。

三按鈕操作模式下的 SociaLite

手勢操作模式下的 SociaLite

大螢幕裝置上的 SociaLite 應用程式。

大螢幕裝置上的 SociaLite

必要條件

  • 具備 Kotlin 基本知識。
  • 完成「設定 Android Studio」程式碼研究室,或熟悉 Android Studio 使用方式,且能在搭載 Android 15 的模擬器或實體裝置上測試應用程式。

課程內容

  • 如何因應 Android 15 的無邊框措施更動。
  • 如何讓無邊框應用程式回溯相容。

軟硬體需求

  • 最新版 Android Studio。
  • 搭載 Android 15 Beta 1 以上版本的測試裝置或模擬器。
  • Android 15 Beta 1 以上版本的 SDK。

2. 取得範例程式碼

  1. 前往 GitHub 下載範例程式碼

您也可以複製整個存放區,然後查看 codelab_improve_android_experience_2024 分支。

$ git clone git@github.com:android/socialite.git
$ cd socialite
$ git checkout codelab_improve_android_experience_2024
  1. 在 Android Studio 中開啟 SociaLite,然後在 Android 15 裝置或模擬器上執行該應用程式。您會看到類似下方的畫面:

三按鈕操作模式下的 SociaLite。

手勢操作模式下的 SociaLite。

三按鈕操作

手勢操作

大螢幕裝置上的 SociaLite。

大螢幕

  1. 在「Chats」頁面上選取任一對話,例如談論狗狗的對話。

談論狗狗的聊天訊息 (三按鈕操作模式)

談論狗狗的聊天訊息 (手勢操作模式)

談論狗狗的聊天訊息 (三按鈕操作模式)

談論狗狗的聊天訊息 (手勢操作模式)

3. 讓應用程式採用 Android 15 的無邊框設計

什麼是無邊框設計?

採用「無邊框」設計,是指應用程式能在系統資訊列後方繪製,帶來更優質的使用者體驗,並充分運用螢幕空間。

GIF 圖片,顯示應用程式採用無邊框設計

如何因應 Android 15 的無邊框措施更動

在 Android 15 之前,應用程式的 UI 預設會受到限制,即使處於展開狀態,也不會延伸到狀態列和導覽列這類系統資訊列區塊。雖然您可以選擇採用無邊框設計,但因應用程式各不相同,這項操作可能既瑣碎又麻煩。

所幸,從 Android 15 開始,應用程式都會預設採用「無邊框」設計。您會看到以下預設設定:

  • 三按鈕導覽列呈現透明狀態。
  • 手勢導覽列呈現透明狀態。
  • 狀態列呈現透明狀態。
  • 除非是套用了插邊或邊框間距,否則內容會在系統資訊列後方繪製,例如在導覽列、狀態列和說明文字列之後。

這可確保在提升應用程式品質時,一定會採用無邊框設計,並讓打造無邊框應用程式的過程更輕鬆。不過,此更動對應用程式可能不是全然有益。我們之後會舉例說明您將目標 SDK 升級為 Android 15 後,對 SociaLite 造成的兩項負面影響。

將目標 SDK 值改為 Android 15

  1. 在 SociaLite 應用程式的 build.gradle 檔案中,將目標和編譯 SDK 版本更改為 Android 15 或 VanillaIceCream

如果您學習這個程式碼研究室課程時,Android 15 還未推出穩定版,程式碼會如下所示:

android {
    namespace = "com.google.android.samples.socialite"
    compileSdkPreview = "VanillaIceCream"

    defaultConfig {
        applicationId = "com.google.android.samples.socialite"
        minSdk = 21
        targetSdkPreview = "VanillaIceCream"
        ...
    }
...
}

如果您學習這個程式碼研究室課程時,Android 15 已推出穩定版,程式碼會如下所示:

android {
    namespace = "com.google.android.samples.socialite"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.google.android.samples.socialite"
        minSdk = 21
        targetSdk = 35
        ...
    }
...
}
  1. 重新建構 SociaLite,留意下列問題:
  • 三按鈕操作模式下的背景保護措施與導覽列不符。在手勢操作模式下,「Chats」畫面會自動採用無邊框設計,您不必執行任何操作。但在三按鈕操作模式下,您應移除畫面的背景保護措施。

三按鈕操作模式下的「Chats」畫面。

手勢操作模式下的「Chats」畫面。

三按鈕操作模式下的「Chats」畫面

手勢操作模式下的「Chats」畫面

  • UI 遭遮蔽。對話底部的 UI 元素被導覽列遮住。此問題在三按鈕操作模式下最為明顯。

談論狗狗的聊天訊息 (三按鈕操作模式)。

談論狗狗的聊天訊息 (手勢操作模式)。

談論狗狗的聊天訊息 (三按鈕操作模式)

談論狗狗的聊天訊息 (手勢操作模式)

修正 SociaLite

如要移除三按鈕操作模式預設的背景保護措施,請執行下列步驟:

  1. MainActivity.kt 檔案中移除預設的背景保護措施,方法是將 window.isNavigationBarContrastEnforced 屬性設為 false。
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()
        super.onCreate(savedInstanceState)
        setContent {
            // Add this block:
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                window.isNavigationBarContrastEnforced = false
            }
        }
    }
    ...
}

window.isNavigationBarContrastEnforced 屬性可確保導覽列的對比度夠強,能因應全透明背景的要求。您只要將這個屬性設為 false,就能有效將三按鈕操作模式的背景設為透明。window.isNavigationBarContrastEnforced 只會在三按鈕操作模式下發揮效果,不會影響手勢操作模式。

  1. 在 Android 15 裝置上重新執行應用程式,查看任一對話。「Timeline」、「Chats」和「Settings」畫面現在都會顯示為無邊框。而應用程式的 NavigationBar (含有「Timeline」、「Chats」和「Settings」按鈕) 會在系統透明的三按鈕導覽列後方繪製。

三按鈕操作模式下的「Chats」畫面,帶狀長條已移除。

談論狗狗的對話 (手勢操作模式)。

「Chats」畫面上的帶狀長條已移除。

手勢操作模式不受影響

但請注意,對話的 InputBar 仍遭到系統資訊列遮蔽。您需要妥善處理插邊,才能修正這個問題。

談論狗狗的對話 (三按鈕操作模式)。

談論狗狗的對話 (手勢操作模式)。

談論狗狗的對話 (三按鈕操作模式)。底部的輸入欄位被系統導覽列遮住。

談論狗狗的對話 (手勢操作模式)。底部的輸入欄位被系統導覽列遮住。

在 SociaLite 中,InputBar 會被遮住。實際上,如果您將裝置旋轉到橫向模式,或使用大螢幕裝置,可能會發現畫面四周的元素都遭到遮蓋。因此,請針對上述所有使用情況,考量如何處理插邊。就 SociaLite 而言,您可以套用邊框間距,將 InputBar 中可輕觸的內容調到上方位置。

如要套用插邊以修正 UI 遭遮蔽的問題,請採取以下步驟:

  1. 前往 ui/chat/ChatScreen.kt 檔案,在第 178 行左右找出 ChatContent 可組合函式,其中含有對話畫面的 UI。ChatContent 會利用 Scaffold 輕鬆建構 UI。根據預設,Scaffold 可提供系統 UI 相關資訊 (例如系統資訊列的深度) 做為插邊,供您搭配使用 Scaffold 的邊框間距值 (innerPadding 參數)。請使用 ScaffoldinnerPadding,將邊框間距新增到 InputBar 中。
  2. 在第 214 行左右找出 ChatContent 中的 InputBar。這個自訂的可組合函式可建立 UI,供使用者撰寫訊息。以下是輸入列的預覽畫面:

輸入列的預覽畫面。

InputBar 採用了 contentPadding,並將其當做邊框間距,套用到含有其餘 UI 的 Row 可組合函式中。這個邊框間距會套用到 Row 可組合函式的每個邊。您可以在第 432 行左右發現這點。以下是供您參考的 InputBar 可組合函式 (請不要新增這段程式碼):

// Don't add this code because it's only for reference.
@Composable
private fun InputBar(
    contentPadding: PaddingValues,
    ...,
) {
    Surface(...) {
        Row(
            modifier = Modifier
                .padding(contentPadding)
            ...
        ) {
            IconButton(...) { ... } // take picture
            IconButton(...) { ... } // attach picture
            TextField(...) // write message
            FilledIconButton(...){ ... } // send message
            }
        }
    }
}
  1. 返回 ChatContent 中的 InputBar,然後變更 contentPadding,以便使用系統資訊列插邊。您可以在第 220 行左右查看此操作。
InputBar(
    ...
    contentPadding = innerPadding, //Add this line.
    // contentPadding = PaddingValues(0.dp), // Remove this line.
    ...
 )
  1. 在 Android 15 裝置上重新執行應用程式。

談論狗狗的對話 (三按鈕操作模式)。

談論狗狗的對話 (手勢操作模式)。

談論狗狗的對話 (三按鈕操作模式);其中插邊套用方式有誤。

談論狗狗的對話 (手勢操作模式);其中插邊套用方式有誤。

套用底部邊框間距後,按鈕就不會再被系統資訊列遮住,但是這也同時套用了頂部邊框間距,其中涵蓋 TopAppBar 和系統資訊列的深度。Scaffold 會將邊框間距值傳遞給自身內容,以便避開頂部應用程式列和系統資訊列。

  1. 如要修正頂部邊框間距,請建立 innerPadding PaddingValues 副本,將頂部邊框間距設為 0.dp,然後將修改的副本傳遞到 contentPadding 中。
InputBar(
    ...
    contentPadding = innerPadding.copy(layoutDirection, top = 0.dp), //Add this line.
    // contentPadding = innerPadding, // Remove this line.
    ...
 )
  1. 在 Android 15 裝置上重新執行應用程式。

談論狗狗的對話 (三按鈕操作模式)。

談論狗狗的對話 (手勢操作模式)。

談論狗狗的對話 (三按鈕操作模式);已正確套用插邊。

談論狗狗的對話 (手勢操作模式);已正確套用插邊。

恭喜!您已針對 Android 15 平台的無邊框相關更動,製作相容的 SociaLite 應用程式。接著,我們來瞭解如何確保採用無邊框設計的 SociaLite 能夠回溯相容。

4. 讓採用無邊框設計的 SociaLite 回溯相容

現在,SociaLite 在 Android 15 上已採用無邊框設計,但在較舊的 Android 裝置上卻非如此。如要讓 SociaLite 在舊版 Android 裝置上顯示無邊框效果,請呼叫 enableEdgeToEdge,然後在 MainActivity.kt 檔案中設定內容。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()
        enableEdgeToEdge() // Add this line.
        window.isNavigationBarContrastEnforced = false
        super.onCreate(savedInstanceState)
        setContent {... }
    }
}

enableEdgeToEdge 的匯入內容是 import androidx.activity.enableEdgeToEdge。依附元件為 AndroidX Activity 1.8.0 以上版本。

如要深入瞭解如何讓無邊框應用程式回溯相容,以及如何處理插邊,請參閱以下指南:

本課程中關於無邊框設計的部分到此結束。下一節內容屬於選用性質,會討論無邊框設計的其他注意事項,或許也適用於您的應用程式。

5. 選用:無邊框設計的其他注意事項

處理架構周遭的插邊

元件

您可能已注意到,當我們變更目標 SDK 值後,SociaLite 中的許多元件「並未」更動。SociaLite 的架構採用最佳做法,因此處理這項平台變更並不困難。最佳做法包括:

捲動內容

您的應用程式可能含有清單;受到 Android 15 異動影響,清單中最後一個項目或許會被系統的導覽列遮住。

三按鈕操作模式遮住應用程式的最後一個清單項目。

上圖顯示三按鈕操作模式遮住清單中最後一個項目。

使用 Compose 捲動內容

在 Compose 中,請使用 LazyColumncontentPadding 為最後一個項目增加空間,但如果您使用 TextField 則例外:

Scaffold { innerPadding ->
    LazyColumn(
        contentPadding = innerPadding
    ) {
        // Content that does not contain TextField
    }
}

三按鈕操作模式不再遮住應用程式的最後一個清單項目。

上圖顯示三按鈕操作模式「不再」遮住清單中最後一個項目。

如果您使用 TextField,請以 SpacerLazyColumn 中繪製最後一個 TextField。詳情請參閱插邊消耗

LazyColumn(
    Modifier.imePadding()
) {
    // Content with TextField
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

使用 Views 捲動內容

如果是 RecyclerViewNestedScrollView,請新增 android:clipToPadding="false"

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    app:layoutManager="LinearLayoutManager" />

請使用 setOnApplyWindowInsetsListener 從視窗插邊提供左右兩側和底部的邊框間距:

ViewCompat.setOnApplyWindowInsetsListener(binding.recycler) { v, insets ->
    val i = insets.getInsets(
        WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.displayCutout()
    )
    v.updatePadding(
        left = i.left,
        right = i.right,
        bottom = i.bottom + bottomPadding,
    )
    WindowInsetsCompat.CONSUMED
}

使用 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS

將目標版本指定為 SDK 35 前,處於橫向模式的 SocialLite 會如下圖所示:左側邊緣有為鏡頭凹口預留的大範圍白色方塊。在三按鈕操作模式下,按鈕位於右側。

橫向模式的 SociaLite 應用程式。

將目標版本指定為 SDK 35 後,SocialLite 會如下圖所示:左側邊緣不再為鏡頭凹口預留的大範圍白色方塊。為了達成此效果,Android 會自動設定 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS橫向模式的 SociaLite 應用程式。

視您的應用程式而定,建議您在這裡處理插邊。

如要在 SociaLite 中處理插邊,請按照下列步驟操作:

  1. ui/ContactRow.kt 檔案中找出 Row 可組合函式。
  2. 配合螢幕凹口修改邊框間距。
@Composable
fun ChatRow(
   chat: ChatDetail,
   onClick: (() -> Unit)?,
   modifier: Modifier = Modifier,
) {
   // Add layoutDirection, displayCutout, startPadding, and endPadding.
   val layoutDirection = LocalLayoutDirection.current
   val displayCutout = WindowInsets.displayCutout.asPaddingValues()
   val startPadding = displayCutout.calculateStartPadding(layoutDirection)
   val endPadding = displayCutout.calculateEndPadding(layoutDirection)
   Row(
       modifier = modifier
           ...
           // .padding(16.dp) // Remove this line.
           // Add this block:
           .padding(
               PaddingValues(
                   top = 16.dp,
                   bottom = 16.dp,
                   // Ensure content is not occluded by display cutouts
                   // when rotating the device.
                   start = startPadding.coerceAtLeast(16.dp),
                   end = endPadding.coerceAtLeast(16.dp)
               )
           ),
       ...
   ) { ... }

處理螢幕凹口後,SociaLite 看起來會像這樣:

橫向模式的 SociaLite 應用程式。

您可以在「開發人員選項畫面的「螢幕凹口」之下,測試各種螢幕凹口設定

如果應用程式具有使用 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULTLAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVERLAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 的「非」浮動式視窗 (例如 Activity),那麼從 Android 15 Beta 2 開始,Android 會將這些凹口模式解讀為 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS。在之前的 Android 15 Beta 1 中,應用程式會異常終止。

說明文字列也是系統資訊列

說明文字列的用途是說明任意形式視窗中的系統 UI 視窗裝飾,例如頂部標題列,因此也可說是系統資訊列。在 Android Studio 的桌機模擬器中,您可以查看說明文字列。下方的螢幕截圖顯示說明文字列位於應用程式頂部。

模擬器顯示說明文字列。

在 Compose 中,如果您使用 ScaffoldPaddingValuessafeContentsafeDrawing 或內建的 WindowInsets.systemBars,應用程式會正常顯示。但如果使用 statusBar 處理插邊,應用程式內容可能就不會正常顯示,因為狀態列沒有空間容納說明文字列。

在 Views 中,如果您使用 WindowInsetsCompat.systemBars 手動處理插邊,應用程式會正常顯示。但如果使用 WindowInsetsCompat.statusBars 手動處理插邊,應用程式可能就不會正常顯示,因為狀態列並非說明文字列。

沉浸模式下的應用程式

Android 15 強制執行的無邊框措施對沉浸模式畫面的影響不大,因為沉浸式的應用程式已經採用無邊框設計。

保護系統資訊列

建議您讓應用程式採用透明的手勢操作列,但採用半透明或不透明的三按鈕操作列。

Android 15 預設使用半透明的三按鈕操作列,因為這個平台會將 window.isNavigationBarContrastEnforced 屬性設為 true。手勢操作列則維持透明。

三按鈕操作模式下的應用程式。

三按鈕操作列預設為半透明。

半透明的三按鈕操作列應可滿足一般需求。不過,在某些情況下,應用程式可能會需要不透明的三按鈕操作列。此時,請先將 window.isNavigationBarContrastEnforced 屬性設為 false。接著,使用 WindowInsetsCompat.tappableElement (針對 Views) 或 WindowInsets.tappableElement (針對 Compose)。如果這些值都是 0,表示使用者採用手勢操作模式。若是其他值,表示使用者採用三按鈕操作模式。如果使用者採用三按鈕操作模式,請在導覽列後方繪製檢視畫面或方塊。以 Compose 來說,可能會像這樣:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            window.isNavigationBarContrastEnforced = false
            MyTheme {
                Surface(...) {
                    MyContent(...)
                    ProtectNavigationBar()
                }
            }
        }
    }
}

// Use only if required.
@Composable
fun ProtectNavigationBar(modifier: Modifier = Modifier) {
   val density = LocalDensity.current
   val tappableElement = WindowInsets.tappableElement
   val bottomPixels = tappableElement.getBottom(density)
   val usingTappableBars = remember(bottomPixels) {
       bottomPixels != 0
   }
   val barHeight = remember(bottomPixels) {
       tappableElement.asPaddingValues(density).calculateBottomPadding()
   }

   Column(
       modifier = modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Bottom
   ) {
       if (usingTappableBars) {
           Box(
               modifier = Modifier
                   .background(MaterialTheme.colorScheme.background)
                   .fillMaxWidth()
                   .height(barHeight)
           )
       }
   }
}

三按鈕操作模式下的應用程式。

不透明的三按鈕操作列

6. 查看解決方案程式碼

MainActivity.kt 檔案的 onCreate 方法應如下所示:

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       installSplashScreen()
       enableEdgeToEdge()
       window.isNavigationBarContrastEnforced = false
       super.onCreate(savedInstanceState)
       setContent {
           Main(
               shortcutParams = extractShortcutParams(intent),
           )
       }
   }
}

ChatScreen.kt 檔案內的 ChatContent 可組合函式應會處理插邊:

private fun ChatContent(...) {
   ...
   Scaffold(...) { innerPadding ->
       Column {
           ...
           InputBar(
               input = input,
               onInputChanged = onInputChanged,
               onSendClick = onSendClick,
               onCameraClick = onCameraClick,
               onPhotoPickerClick = onPhotoPickerClick,
               contentPadding = innerPadding.copy(
                    layoutDirection, top = 0.dp
                ),
               sendEnabled = sendEnabled,
               modifier = Modifier
                   .fillMaxWidth()
                   .windowInsetsPadding(
                       WindowInsets.ime.exclude(WindowInsets.navigationBars)
                    ),
            )
       }
   }
}

解決方案程式碼位於主要分支中。如果您已下載 SociaLite,請執行以下指令:

git checkout main

如果還沒下載 SociaLite,可以再次下載程式碼,直接或透過 Git 查看主要分支:

git clone git@github.com:android/socialite.git