1. 事前準備
如要瞭解如何使用各種 Android 平台 API,實作社群網路應用程式中的常見功能,SociaLite 是絕佳的參考範例。這款應用程式運用各式 Jetpack API 實現複雜功能,不僅可在更多裝置上穩定運作,也能減少編寫程式碼的需求。
本程式碼研究室會逐步引導您製作與 Android 15 無邊框強制措施相容的 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. 取得範例程式碼
- 前往 GitHub 下載範例程式碼。
您也可以複製整個存放區,然後查看 codelab_improve_android_experience_2024
分支。
$ git clone git@github.com:android/socialite.git
$ cd socialite
$ git checkout codelab_improve_android_experience_2024
- 在 Android Studio 中開啟 SociaLite,然後在 Android 15 裝置或模擬器上執行該應用程式。您會看到類似下方的畫面:
三按鈕操作 | 手勢操作 |
大螢幕 |
- 在「Chats」頁面上選取任一對話,例如談論狗狗的對話。
談論狗狗的聊天訊息 (三按鈕操作模式) | 談論狗狗的聊天訊息 (手勢操作模式) |
3. 讓應用程式採用 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」畫面會自動採用無邊框設計,您不必執行任何操作。但在三按鈕操作模式下,您應移除畫面的背景保護措施。
三按鈕操作模式下的「Chats」畫面 | 手勢操作模式下的「Chats」畫面 |
- UI 遭遮蔽。對話底部的 UI 元素被導覽列遮住。此問題在三按鈕操作模式下最為明顯。
談論狗狗的聊天訊息 (三按鈕操作模式) | 談論狗狗的聊天訊息 (手勢操作模式) |
修正 SociaLite
如要移除三按鈕操作模式預設的背景保護措施,請執行下列步驟:
- 在
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」按鈕) 會在系統透明的三按鈕導覽列後方繪製。
「Chats」畫面上的帶狀長條已移除。 | 手勢操作模式不受影響 |
但請注意,對話的 InputBar
仍遭到系統資訊列遮蔽。您需要妥善處理插邊,才能修正這個問題。
談論狗狗的對話 (三按鈕操作模式)。底部的輸入欄位被系統導覽列遮住。 | 談論狗狗的對話 (手勢操作模式)。底部的輸入欄位被系統導覽列遮住。 |
在 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.
...
)
- 在 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 的架構採用最佳做法,因此處理這項平台變更並不困難。最佳做法包括:
- 使用 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)
)
}
}
}
不透明的三按鈕操作列 |
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