$ git clone git@ github . com : android / socialite . git
$ cd socialite
$ git checkout codelab_improve_android_experience_2024
在 Android Studio 中開啟 SociaLite,然後在 Android 15 裝置或模擬器上執行該應用程式。您會看到類似下方的畫面:
如何因應 Android 15 的無邊框措施更動
在 Android 15 之前,應用程式的 UI 預設會受到限制,即使處於展開狀態,也不會延伸到狀態列和導覽列這類系統資訊列區塊。雖然您可以選擇採用無邊框設計,但因應用程式各不相同,這項操作可能既瑣碎又麻煩。
所幸,從 Android 15 開始,應用程式都會預設採用「無邊框」設計。您會看到以下預設設定:
三按鈕導覽列呈現透明狀態。
手勢導覽列呈現透明狀態。
狀態列呈現透明狀態。
除非是套用了插邊或邊框間距,否則內容會在系統資訊列後方繪製,例如在導覽列、狀態列和說明文字列之後。
這可確保在提升應用程式品質時,一定會採用無邊框設計,並讓打造無邊框應用程式的過程更輕鬆。不過,此更動對應用程式可能不是全然有益。我們之後會舉例說明您將目標 SDK 升級為 Android 15 後,對 SociaLite 造成的兩項負面影響。
將目標 SDK 值改為 Android 15
在 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
重新建構 SociaLite,留意下列問題:
三按鈕操作模式下的背景保護措施與導覽列不符 。在手勢操作模式下,「Chats」畫面會自動採用無邊框設計,您不必執行任何操作。但在三按鈕操作模式下,您應移除畫面的背景保護措施。
如要移除三按鈕操作模式預設的背景保護措施,請執行下列步驟:
在 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 只會在三按鈕操作模式下發揮效果,不會影響手勢操作模式。
在 Android 15 裝置上重新執行應用程式,查看任一對話。「Timeline」、「Chats」和「Settings」畫面現在都會顯示為無邊框。而應用程式的 NavigationBar (含有「Timeline」、「Chats」和「Settings」按鈕) 會在系統透明的三按鈕導覽列後方繪製。
在 SociaLite 中,InputBar 會被遮住。實際上,如果您將裝置旋轉到橫向模式,或使用大螢幕裝置,可能會發現畫面四周的元素都遭到遮蓋。因此,請針對上述所有使用情況,考量如何處理插邊。就 SociaLite 而言,您可以套用邊框間距,將 InputBar 中可輕觸的內容調到上方位置。
如要套用插邊以修正 UI 遭遮蔽的問題,請採取以下步驟:
前往 ui/chat/ChatScreen.kt 檔案,在第 178 行左右找出 ChatContent 可組合函式,其中含有對話畫面的 UI。ChatContent 會利用 Scaffold 輕鬆建構 UI。根據預設,Scaffold 可提供系統 UI 相關資訊 (例如系統資訊列的深度) 做為插邊,供您搭配使用 Scaffold 的邊框間距值 (innerPadding 參數)。請使用 Scaffold 的 innerPadding,將邊框間距新增到 InputBar 中。
在第 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
返回 ChatContent 中的 InputBar,然後變更 contentPadding,以便使用系統資訊列插邊。您可以在第 220 行左右查看此操作。
InputBar (
contentPadding = innerPadding , //Add this line.
// contentPadding = PaddingValues(0.dp), // Remove this line.
在 Android 15 裝置上重新執行應用程式。
套用底部邊框間距後,按鈕就不會再被系統資訊列遮住,但是這也同時套用了頂部邊框間距,其中涵蓋 TopAppBar 和系統資訊列的深度。Scaffold 會將邊框間距值傳遞給自身內容,以便避開頂部應用程式列和系統資訊列。
如要修正頂部邊框間距,請建立 innerPadding PaddingValues 副本,將頂部邊框間距設為 0.dp,然後將修改的副本傳遞到 contentPadding 中。
InputBar (
contentPadding = innerPadding . copy ( layoutDirection , top = 0. dp ), //Add this line.
// contentPadding = innerPadding, // Remove this line.
注意 :我們在幾行程式碼前將 layoutDirection 定義為 LocalLayoutDirection.current,方便您處理由左至右/由右至左展開的可組合函式,這在處理其他語言的本地化版本時非常有用。
注意 :copy 是 SociaLite 獨有的方法,詳情請參閱第 230 行左右的程式碼。在您的應用程式中,您或許可以建立相似的功能,或選擇以其他方式處理插邊 ,重點是這個做法要適合您的應用程式。
在 Android 15 裝置上重新執行應用程式。
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 {... }
注意 :SociaLite 只有一個活動。如果應用程式有多項活動,您應針對每個活動逐一呼叫 enableEdgeToEdge。
enableEdgeToEdge 的匯入內容是 import androidx.activity.enableEdgeToEdge。依附元件為 AndroidX Activity 1.8.0 以上版本。
如要深入瞭解如何讓無邊框應用程式回溯相容,以及如何處理插邊,請參閱以下指南:
Compose 的視窗插邊
在應用程式中以無邊框方式顯示內容
本課程中關於無邊框設計的部分到此結束。下一節內容屬於選用性質,會討論無邊框設計的其他注意事項,或許也適用於您的應用程式。
處理架構周遭的插邊
您可能已注意到,當我們變更目標 SDK 值後,SociaLite 中的許多元件「並未」更動。SociaLite 的架構採用最佳做法,因此處理這項平台變更並不困難。最佳做法包括:
使用 Material Design 3 元件 (androidx.compose.material3 ),例如 TopAppBar、BottomAppBar 和 NavigationBar,因為這類元件會自動套用插邊 。
如果應用程式使用 Compose 的 Material 2 元件 (androidx.compose.material ),這類元件本身不會自動處理插邊。不過,您可以存取插邊,然後手動套用。在 androidx.compose.material 1.6.0 以上版本,則請使用 windowInsets 參數,為 BottomAppBar 、TopAppBar 、BottomNavigation 和 NavigationRail 手動套用插邊。同樣地,對 Scaffold 也是使用 contentWindowInsets 參數。否則,請手動將插邊套用為邊框間距。
如果應用程式使用 Views 和 Material 元件 (com.google.android.material ),您可能不需採取額外行動,因為大多數以 Views 為基礎的 Material 元件 (例如 BottomNavigationView 、BottomAppBar 、NavigationRailView 和 NavigationView ) 都會處理插邊。不過,如果您使用 AppBarLayout ,就需要新增 android:fitsSystemWindows="true"。
如果應用程式使用 Views 和 BottomSheet、SideSheet 或自訂的容器,請使用 ViewCompat.setOnApplyWindowInsetsListener 套用邊框間距。對於 RecyclerView,也請使用這個事件監聽器套用邊框間距,同時新增 clipToPadding="false"。
如果是複雜的 UI,請使用 Scaffold (或 NavigationSuiteScaffold/ListDetailPaneScaffold),而非 Surface。Scaffold 可讓您輕鬆放置 TopAppBar、BottomAppBar、NavigationBar 和 NavigationRail。
您的應用程式可能含有清單;受到 Android 15 異動影響,清單中最後一個項目或許會被系統的導覽列遮住。
上圖顯示三按鈕操作模式遮住清單中最後一個項目。
使用 Compose 捲動內容
在 Compose 中,請使用 LazyColumn 的 contentPadding 為最後一個項目增加空間,但如果您使用 TextField 則例外:
Scaffold { innerPadding - >
LazyColumn (
contentPadding = innerPadding
// Content that does not contain TextField
上圖顯示三按鈕操作模式「不再」遮住清單中最後一個項目。
如果您使用 TextField,請以 Spacer 在 LazyColumn 中繪製最後一個 TextField。詳情請參閱插邊消耗 。
LazyColumn (
Modifier . imePadding ()
// Content with TextField
item {
Spacer (
Modifier . windowInsetsBottomHeight (
WindowInsets . systemBars
使用 Views 捲動內容
如果是 RecyclerView 或 NestedScrollView,請新增 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 會如下圖所示:左側邊緣有為鏡頭凹口預留的大範圍白色方塊。在三按鈕操作模式下,按鈕位於右側。
將目標版本指定為 SDK 35 後,SocialLite 會如下圖所示:左側邊緣不再為鏡頭凹口預留的大範圍白色方塊。為了達成此效果,Android 會自動設定 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 。
視您的應用程式而定,建議您在這裡處理插邊。
如要在 SociaLite 中處理插邊,請按照下列步驟操作:
在 ui/ContactRow.kt 檔案中找出 Row 可組合函式。
配合螢幕凹口修改邊框間距。
@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 看起來會像這樣:
您可以在「開發人員選項 」畫面的「螢幕凹口」之下,測試各種螢幕凹口設定。
如果應用程式具有使用 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 、LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 或 LAYOUT_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 中,如果您使用 Scaffold 的 PaddingValues、safeContent 、safeDrawing 或內建的 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 )
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
[[["容易理解","easyToUnderstand","thumb-up"],["確實解決了我的問題","solvedMyProblem","thumb-up"],["其他","otherUp","thumb-up"]],[["缺少我需要的資訊","missingTheInformationINeed","thumb-down"],["過於複雜/步驟過多","tooComplicatedTooManySteps","thumb-down"],["過時","outOfDate","thumb-down"],["翻譯問題","translationIssue","thumb-down"],["示例/程式碼問題","samplesCodeIssue","thumb-down"],["其他","otherDown","thumb-down"]],[],[],[]]