Communitygithub.com

lerchenflo/claudeskills

使用此技能將您的 Jetpack Compose 應用程式遷移至適應性邊到邊(edge-to-edge)支援,並排除常見問題。此技能可修正被導覽列或狀態列遮擋或重疊的 UI 元件(如按鈕或列表)、修正 IME 插入區(insets)以及修正系統列的可讀性。

claudeskills 是什麼?

這個 AI 代理技能(lerchenflo/claudeskills)為使用 Jetpack Compose 的 Android 開發者提供一個逐步指南,用於將現有應用程式遷移至適應性邊到邊(edge-to-edge)支援。它解決了 UI 被系統列(狀態列、導覽列)遮擋、軟鍵盤(IME)插入區處理不當以及系統列圖示可讀性等常見問題。技能涵蓋了啟用邊到邊模式(`enableEdgeToEdge`)、使用 `Scaffold` 和 `WindowInsets` 應用插入區、處理 List、FAB 和 TextField 等特定元件、自適應 Scaffold(如 `NavigationSuiteScaffold`)中的邊際案例,以及對話框的邊到邊配置。它也包含一個檢查清單以驗證實作是否完整。該技能設計用於 Android Jetpack Compose 專案,目標 SDK 需為 35 或更高,並支援 WindowCompat 和 ComponentActivity 兩種邊到邊實作方式。

相容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/lerchenflo/claudeskills/tree/HEAD/android/edge-to-edge

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

先決條件

  • 專案必須使用 Android Jetpack Compose。
  • 專案必須將目標 SDK 設為 35 或更高。若 SDK 低於 35,請將 SDK 提升至 35。

步驟 1:規劃

  1. 找出並分析所有 Activity 類別,檢測哪些已有邊到邊支援。對於每個尚未啟用邊到邊的 Activity,規劃使其啟用邊到邊。
  2. 在每個 Activity 中,找出並分析所有列表與 FAB 元件,檢測哪些已有邊到邊支援。對於每個尚未具備邊到邊支援的元件,規劃使其具備邊到邊支援。
  3. 在每個 Activity 中,掃描 TextField、OutlinedTextField 或 BasicTextField。若找到,則您必須依照此技能的 IME 章節,確認 IME 不會隱藏輸入欄位。

步驟 2:新增邊到邊支援

  1. 在每個尚未呼叫 enableEdgeToEdge 的 Activity 的 onCreate 方法中,於 setContent 之前加入 enableEdgeToEdge。
  2. 在 AndroidManifest.xml 中,為所有使用軟體鍵盤的 Activity 新增 android:windowSoftInputMode="adjustResize"。

步驟 3:應用插入區(Insets)

  • 應用程式必須套用系統插入區,或將內容對齊參考線,以確保關鍵 UI 仍可點擊。請僅選擇一種方法,以避免雙重內距:

    1. (首選) 當可行時,使用 Scaffold 並將 PaddingValues 傳遞給內容 lambda。
    Scaffold { innerPadding ->
        // innerPadding 會考慮系統列及任何 Scaffold 元件
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .consumeWindowInsets(innerPadding),
            contentPadding = innerPadding
        ) { /* 內容 */ }
    }
    
    1. (首選) 當可行時,使用 Material 元件中的自動插入區處理或內距修飾詞。

      • Material 3 元件會為其自身元件管理安全區域,包括:
        • TopAppBar
        • SmallTopAppBar
        • CenterAlignedTopAppBar
        • MediumTopAppBar
        • LargeTopAppBar
        • BottomAppBar
        • ModalDrawerSheet
        • DismissibleDrawerSheet
        • PermanentDrawerSheet
        • ModalBottomSheet
        • NavigationBar
        • NavigationRail
      • 對於 Material 2 元件,使用 windowInsets 參數手動為 BottomAppBar、TopAppBar 與 BottomNavigation 套用插入區。請勿對父容器套用內距;取而代之的是直接將插入區傳遞給應用程式列元件。對父容器套用內距會阻止應用程式列的背景繪製到系統列的區域。例如,對於 TopAppBar,僅選擇以下選項之一:
        1. (首選) TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets)
        2. TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))
        3. TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))
    2. 對於 Scaffold 之外的元件,使用內距修飾詞,例如 Modifier.safeDrawingPadding() 或 Modifier.windowInsetsPadding(WindowInsets.safeDrawing)。

      Box(
          modifier = Modifier
              .fillMaxSize()
              .safeDrawingPadding()
      ) {
          Button(
              onClick = {},
              modifier = Modifier.align(Alignment.BottomCenter)
          ) {
              Text("Login")
          }
      }
      
    3. 對於深度巢狀且有過多內距的元件,使用 WindowInsetsRulers(例如 Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current))。請參閱 IME 章節的代碼範例。

    4. 當您需要某個元素(例如自訂標頭或裝飾性遮罩)與系統列具有相同尺寸時,使用插入區尺寸修飾詞(例如 Modifier.windowInsetsTopHeight(WindowInsets.systemBars))。 請參閱 列表 章節的代碼範例。

自適應 Scaffold

  • NavigationSuiteScaffold 會為其自身元件(如 NavigationRail 或 NavigationBar)管理安全區域。然而,自適應 Scaffold(例如 NavigationSuiteScaffold、ListDetailPaneScaffold)不會將 PaddingValues 傳播給其內部內容。您必須將插入區應用於個別畫面或元件(例如列表 contentPadding 或 FAB 內距),如 步驟 3 中所述。請勿將 safeDrawingPadding 或類似修飾詞應用於 NavigationSuiteScaffold 父容器。這會剪裁並阻止邊到邊畫面。

IME

  • 對於每個使用軟體鍵盤的 Activity,請檢查 AndroidManifest.xml 中是否設定了 android:windowSoftInputMode="adjustResize"。請勿使用 SOFT_INPUT_ADJUST_RESIZE,因為它已被棄用。然後,維持輸入欄位的焦點。選擇其中一種方式:
      1. (首選) 在內容容器中新增 Modifier.fitInside(WindowInsetsRulers.Ime.current)。這比使用 imePadding() 更佳,因為它減少了因忘記在上游層級中消費插入區而導致的抖動和額外內距。
      1. 在內容容器中新增 imePadding。此內距修飾詞必須放在 Modifier.verticalScroll() 之前。如果父容器已透過 contentWindowInsets(例如 contentWindowInsets = WindowInsets.safeDrawing)處理 IME,請勿使用 Modifier.imePadding()。這樣做會導致雙重內距。

使用 Scaffold 的 IME 代碼模式

正確

正確,因為 contentWindowInsets 包含 IME 插入區,並作為 innerPadding 傳遞給內容 lambda。

// 正確
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* 內容 */ }
}

正確,因為 fitInside 會使內容適應 IME 插入區,無論 contentWindowInsets 為何。

// 正確
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .fitInside(WindowInsetsRulers.Ime.current)
            .verticalScroll(rememberScrollState())
    ) { /* 內容 */ }
}

正確,因為預設的 contentWindowInsets 不包含 IME 插入區,而 imePadding() 會套用 IME 插入區:

// 正確
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .imePadding()
            .verticalScroll(rememberScrollState())
    ) { /* 內容 */ }
}

錯誤

錯誤,因為當 IME 開啟時會出現過多的內距。IME 插入區被套用了兩次,一次是透過傳遞的 contentWindowInsets 值中的 innerPadding(該值包含 IME 插入區),另一次是透過 imePadding:

// 錯誤
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .imePadding()
            .verticalScroll(rememberScrollState())
    ) { /* 內容 */ }
}

錯誤,因為 IME 會覆蓋內容。Scaffold 的預設 contentWindowInsets 不包含 IME 插入區。

// 錯誤
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* 內容 */ }
}

不使用 Scaffold 的 IME 代碼模式

正確

以下代碼範例不會導致過多的內距。

// 正確
Box(
    // 插入區已被消費
    modifier = Modifier.safeDrawingPadding() // 或 imePadding(), safeContentPadding(), safeGesturesPadding()
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* 內容 */ }
}

// 正確
Box(
    // 插入區已被消費
    modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // 或 WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* 內容 */ }
}

// 正確
Box(
    // 插入區未被消費,但因 fitInside 而無關緊要
    modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // 或 WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .fitInside(WindowInsetsRulers.Ime.current)
    ) { /* 內容 */ }
}

錯誤

以下代碼範例會導致過多的內距,因為 IME 插入區被套用了兩次:

// 錯誤
Box(
    // 插入區未被消費
    modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // 或 WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* 內容 */ }
}

導覽列對比度與系統列圖示

  • 如果 Activity 使用來自 WindowCompat 的 enableEdgeToEdge,您必須將 isAppearanceLightNavigationBars 和 isAppearanceLightStatusBars 設為裝置主題的相反值(適用於支援淺色和深色主題的應用程式),以使系統列圖示清晰可讀。建議在您的主題檔案中執行此操作。如果 Activity 使用來自 ComponentActivity 的 enableEdgeToEdge,請不要這樣做,因為它會自動處理圖示顏色。

    // 僅在從 `WindowCompat` 呼叫 `enableEdgeToEdge` 時使用。
    // 套用到您的主題檔案。
    @Composable
    fun MyTheme(
        darkTheme: Boolean = isSystemInDarkTheme(),
        content: @Composable () -> Unit
    ) {
        val view = LocalView.current
        if (!view.isInEditMode) {
            SideEffect {
                val window = (view.context as? Activity)?.window ?: return@SideEffect
                val controller = WindowCompat.getInsetsController(window, view)
    
                // 淺色模式 (!darkTheme) 使用深色圖示,深色模式使用淺色圖示
                controller.isAppearanceLightStatusBars = !darkTheme
                controller.isAppearanceLightNavigationBars = !darkTheme
            }
        }
    
        MaterialTheme(content = content)
    }
    
  • 如果任何畫面使用帶有底部列(例如 BottomAppBar、NavigationBar)的 Scaffold 或 NavigationSuiteScaffold,請在對應的 Activity 中為 SDK 29+ 設定 window.isNavigationBarContrastEnforced = false。這可以防止系統在導覽列上添加半透明背景,確保您的底部列顏色延伸到螢幕底部。

列表

  • 將插入區內距(如 Scaffold 的 innerPadding)套用到可捲動元件(例如 LazyColumn、LazyRow)的 contentPadding 參數。請勿將其作為 Modifier.padding() 套用到列表的父容器,因為這會剪裁內容並阻止其在系統列後方捲動。
  • 建立一個覆蓋系統列的半透明 Composable,以便圖示仍然清晰可讀。
class SystemBarProtectionSnippets : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // enableEdgeToEdge 設定 window.isNavigationBarContrastEnforced = true
        // 用於為三按鈕導覽添加半透明遮罩
        enableEdgeToEdge()

        setContent {
            MyTheme {
                // 主要內容
                MyContent()

                // 在繪製主要內容之後,繪製狀態列保護
                StatusBarProtection()
            }
        }
    }
}

@Composable
private fun StatusBarProtection(
    color: Color = MaterialTheme.colorScheme.surfaceContainer,
) {
    Spacer(
        modifier = Modifier
            .fillMaxWidth()
            .height(
                with(LocalDensity.current) {
                    (WindowInsets.statusBars.getTop(this) * 1.2f).toDp()
                }
            )
            .background(
                brush = Brush.verticalGradient(
                    colors = listOf(
                        color.copy(alpha = 1f),
                        color.copy(alpha = 0.8f),
                        Color.Transparent
                    )
                )
            )
    )
}

對話框

如果以下兩個條件皆成立,則對話框為全螢幕,且必須設為邊到邊:

  1. DialogProperties 包含 usePlatformDefaultWidth = false。
  2. 對話框呼叫 Modifier.fillMaxSize()。

若要將全螢幕對話框設為邊到邊,請在 DialogProperties 中設定 decorFitsSystemWindows = false。

Dialog(
    onDismissRequest = { /* 處理關閉 */ },
    properties = DialogProperties(
        // 1. 允許對話框橫跨螢幕的完整寬度
        usePlatformDefaultWidth = false,
        // 2. 允許對話框在狀態列和導覽列後方繪製
        decorFitsSystemWindows = false
    )
) { /* 內容 */ }

檢查清單

  • [ ] 每個 Activity 是否都呼叫了 enableEdgeToEdge()?
  • [ ] AndroidManifest.xml 中是否設定了 adjustResize?
  • [ ] 每個 TextField、OutlinedTextField 或 BasicTextField 是否都有一個設定為 WindowInsets.safeDrawing 或 WindowInsets.ime 的包含 imePadding()、fitInside、Modifier.safeDrawingPadding()、Modifier.safeContentPadding()、Modifier.safeGesturesPadding() 或 contentWindowInsets 的父容器?
  • [ ] 第一個和最後一個列表項目是否透過將插入區傳遞給 contentPadding 而遠離系統列繪製?
  • [ ] FAB 是否透過位於 Scaffold 內部或套用 Modifier.safeDrawingPadding() 而在導覽列上方繪製?
  • [ ] 專案是否可建置?請執行 ./gradlew build 以確認。

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

lerchenflo/claudeskills

Upgrades, or migrates, an Android project to use Android Gradle Plugin (AGP) version 9. Do not use this skill for migrating Kotlin Multiplatform (KMP) projects.

lerchenflo/claudeskills

Provides instructions for installing and using the `android` CLI. The `android` command-line tool is a critical tool for Android development and helps you create new Android projects, run Android apps on devices, manage and interact with Android virtual devices (including screenshots and UI inspection), manage Android SDK components, look up official Android documentation, and discover and install official Android skills.

lerchenflo/claudeskills

Manages Android performance profiling and debugging. Triggers when the user asks to record or analyze Android performance data, such as system traces, heap dumps, method recordings, callstack samples, memory allocations, or investigate bottlenecks, jank, memory leaks, and app startup issues on Android, or when the user asks to write, debug, or execute ad-hoc SQL queries. Applies to both user and system apps or services.

lerchenflo/claudeskills

Analyzes Android apps to identify key user workflows for AppFunctions such as creating a note, playing media, or sending an automated or AI agent triggered message, voice commands, or system shortcuts, without needing to open the app UI. Generates Kotlin code to expose these workflows to the Android system, allowing agents to discover and execute them on-device. Also refines KDoc documentation to ensure AI agents correctly understand and use the provided functionality.

相關技能