質感設計元件和版面配置

Jetpack Compose 可用於實作質感設計,後者是一套用來打造數位介面的全方位設計系統。質感設計元件 (按鈕、卡片、切換按鈕等) 和版面配置 (例如 Scaffold) 可當做可組合函式使用。

質感設計元件是可用於建立使用者介面的互動式建構元素。Compose 提供了許多可以馬上使用的現成元件。如要查看有哪些可用的元件,請參閱 Compose Material API 參考資料

質感設計元件會使用應用程式中的 MaterialTheme 提供的值:

@Composable
fun MyApp() {
    MaterialTheme {
        // Material Components like Button, Card, Switch, etc.
    }
}

如要進一步瞭解主題設定,請參閱「Compose 中的設計系統」指南

內容位置

支援內部內容 (文字標籤、圖示等) 的質感設計元件通常會提供「位置」,也就是接受可組合內容的通用 lambda,以及尺寸和邊框間距等公用常數,以便支援符合質感設計規格的配置內部內容。

此處的範例為 Button

Button(
    onClick = { /* ... */ },
    // Uses ButtonDefaults.ContentPadding by default
    contentPadding = PaddingValues(
        start = 20.dp,
        top = 12.dp,
        end = 20.dp,
        bottom = 12.dp
    )
) {
    // Inner content including an icon and a text label
    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favorite",
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

圖 1. 使用 content 位置和預設邊框間距 (左側) 的 Button,以及使用提供自訂 contentPadding (右側) 的 content 位置的 Button

Button 具有一般 content 結尾的 lambda 位置,使用 RowScope 來配置資料列的內容可組合項。另外還有 contentPadding 參數,可在內部內容中套用邊框間距。您可以使用透過 ButtonDefaults 提供的常數或是自訂值。

另一個例子是 ExtendedFloatingActionButton

ExtendedFloatingActionButton(
    onClick = { /* ... */ },
    icon = {
        Icon(
            Icons.Filled.Favorite,
            contentDescription = "Favorite"
        )
    },
    text = { Text("Like") }
)

圖 2. 使用 icontext 位置的 ExtendedFloatingActionButton

ExtendedFloatingActionButton 擁有供 icontext 標籤使用的兩個位置,而不是一般的 content lambda。儘管每個位置都支援一般的可組合內容,但這項元件能夠反映這些內部內容的配置方式。可在內部處理邊框間距、對齊方式和尺寸。

Scaffold

Compose 提供便利的版面配置,讓您將質感設計元件合併成常見的螢幕模式。例如 Scaffold 之類的可組合項會提供位置以容納各種不同的元件和其他螢幕元素。

螢幕內容

Scaffold 具有一般 content 結尾的 lambda 位置。lambda 會接收應該套用至內容根層級 (例如透過 Modifier.padding) 的 PaddingValues 執行個體,藉此位移頂端和底部長條 (如果有的話)。

Scaffold(/* ... */) { contentPadding ->
    // Screen content
    Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
}

應用程式列

Scaffold 提供頂端應用程式列底部應用程式列的位置。可組合項的位置會在內部處理。

您可以使用 topBar 位置和 TopAppBar

Scaffold(
    topBar = {
        TopAppBar { /* Top app bar content */ }
    }
) {
    // Screen content
}

您可以使用 bottomBar 位置和 BottomAppBar

Scaffold(
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

這些位置可用於其他質感設計元件 (例如 BottomNavigation)。您也可以使用自訂可組合項,例如查看 Owl 範例中的新手上路畫面

懸浮動作按鈕

Scaffold 會提供懸浮動作按鈕的位置。

您可以使用 floatingActionButton 位置和 FloatingActionButton

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    }
) {
    // Screen content
}

FAB 可組合項的底部位置會在內部處理。您可以使用 floatingActionButtonPosition 參數調整水平位置:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to FabPosition.End
    floatingActionButtonPosition = FabPosition.Center
) {
    // Screen content
}

如果您正在使用 Scaffold 可組合項的 bottomBar 位置,則可以使用 isFloatingActionButtonDocked 參數將 FAB 與底部應用程式列重疊:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to false
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

圖 3. 使用 floatingActionButton 位置和 bottomBar 位置的 ScaffoldisFloatingActionButtonDocked 參數已設為 false (頂端) 和 true (底部)。

BottomAppBar 支援使用 cutoutShape 參數的 FAB 裁剪,其可接受任何 Shape。建議您提供固定元件所使用的相同 Shape。舉例來說,FloatingActionButton 會使用 MaterialTheme.shapes.small 以及 50% 的邊角大小作為其 shape 參數的預設值:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar(
            // Defaults to null, that is, No cutout
            cutoutShape = MaterialTheme.shapes.small.copy(
                CornerSize(percent = 50)
            )
        ) {
            /* Bottom app bar content */
        }
    }
) {
  // Screen content
}

圖 4. 含有 BottomAppBar 以及固定 FloatingActionButtonScaffoldBottomAppBar 的自訂 cutoutShapeFloatingActionButton 所使用的 Shape 相符。

Snackbar

Scaffold 提供顯示 Snackbars 的方式。

這是透過包含 SnackbarHostState 屬性的 ScaffoldState 提供。您可以使用 rememberScaffoldState 建立應使用 scaffoldState 參數傳送至 ScaffoldScaffoldState 執行個體。SnackbarHostState 提供 showSnackbar 函式的存取權。此暫停函式需要 CoroutineScope (例如使用 rememberCoroutineScope) 並且可以呼叫此函式以回應 UI 事件,以便顯示 Scaffold 內部的 Snackbar

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    scaffoldState.snackbarHostState
                        .showSnackbar("Snackbar")
                }
            }
        )
    }
) {
    // Screen content
}

您可以提供選擇性動作,並調整 Snackbar 的持續期間。 snackbarHostState.showSnackbar 函式可接受額外的 actionLabelduration 參數,並傳回 SnackbarResult

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    val result = scaffoldState.snackbarHostState
                        .showSnackbar(
                            message = "Snackbar",
                            actionLabel = "Action",
                            // Defaults to SnackbarDuration.Short
                            duration = SnackbarDuration.Indefinite
                        )
                    when (result) {
                        SnackbarResult.ActionPerformed -> {
                            /* Handle snackbar action performed */
                        }
                        SnackbarResult.Dismissed -> {
                            /* Handle snackbar dismissed */
                        }
                    }
                }
            }
        )
    }
) {
    // Screen content
}

您可以提供含有 snackbarHost 參數的自訂 Snackbar。詳情請參閱 SnackbarHost API reference docs

導覽匣

Scaffold 提供強制回應導覽匣的位置。可組合項的可拖曳工作表和版面配置會在內部處理。

您可以使用 drawerContent 位置,它可使用 ColumnScope 配置資料欄中導覽匣內容的可組合項:

Scaffold(
    drawerContent = {
        Text("Drawer title", modifier = Modifier.padding(16.dp))
        Divider()
        // Drawer items
    }
) {
    // Screen content
}

Scaffold 可接受一些額外的導覽匣參數。舉例來說,您可以使用 drawerGesturesEnabled 參數來切換導覽匣是否針對拖曳事件做出回應。

Scaffold(
    drawerContent = {
        // Drawer content
    },
    // Defaults to true
    drawerGesturesEnabled = false
) {
    // Screen content
}

透過 ScaffoldState (包括應使用 scaffoldState 參數傳遞至 ScaffoldDrawerState 屬性),以程式輔助方式開啟與關閉導覽匣。DrawerState 提供 openclose 函式的存取權,以及與目前導覽匣狀態相關的屬性。這些暫停函式需要 CoroutineScope (例如使用 rememberCoroutineScope),才能收到呼叫以便回應 UI 事件。

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    drawerContent = {
        // Drawer content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Open or close drawer") },
            onClick = {
                scope.launch {
                    scaffoldState.drawerState.apply {
                        if (isClosed) open() else close()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

如果想要執行沒有 Scaffold 的強制回應導覽匣,可以使用 ModalDrawer 可組合項。它可接受與 Scaffold 類似的導覽匣參數。

val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

如果想要執行底部導覽匣,可以使用 BottomDrawer 可組合項:

val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
BottomDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

底部功能表

如果想要執行標準底部功能表,可以使用 BottomSheetScaffold 可組合項。它可接受與 Scaffold 類似的參數,例如 topBarfloatingActionButtonsnackbarHost。其中包括提供顯示底部功能表方式的其他參數。

您可以使用 sheetContent 運算單元,它會在資料欄中使用 ColumnScope 以配置工作表內容可組合項:

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

BottomSheetScaffold 可接受一些額外的工作表參數。舉例來說,您可以使用 sheetPeekHeight 參數來設定工作表的微調高度。也可以使用 sheetGesturesEnabled 參數來切換導覽匣是否針對拖曳事件做出回應。

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    },
    // Defaults to BottomSheetScaffoldDefaults.SheetPeekHeight
    sheetPeekHeight = 128.dp,
    // Defaults to true
    sheetGesturesEnabled = false

) {
    // Screen content
}

系統會透過 BottomSheetScaffoldState (包括 BottomSheetState 屬性) 以程式輔助方式展開及收合工作表。您可以使用 rememberBottomSheetScaffoldState 建立應使用 scaffoldState 參數傳送至 BottomSheetScaffoldBottomSheetScaffoldState 執行個體。BottomSheetState 提供 expandcollapse 函式的存取權,以及與目前工作表狀態相關的屬性。這些暫停函式需要 CoroutineScope (例如使用 rememberCoroutineScope),並且可以呼叫這些函式以回應 UI 事件。

val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
BottomSheetScaffold(
    scaffoldState = scaffoldState,
    sheetContent = {
        // Sheet content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Expand or collapse sheet") },
            onClick = {
                scope.launch {
                    scaffoldState.bottomSheetState.apply {
                        if (isCollapsed) expand() else collapse()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

如果想要執行強制回應底部功能表,可以使用 ModalBottomSheetLayout 可組合項:

val sheetState = rememberModalBottomSheetState(
    ModalBottomSheetValue.Hidden
)
ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

背景幕

如果想要執行背景幕,可以使用 BackdropScaffold 可組合項。

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)

BackdropScaffold 可接受一些額外的背景幕參數。舉例來說,您可以使用 peekHeightheaderHeight 參數設定背層的微調高度,以及前層的最低非作用高度。 您也可以透過 gesturesEnabled 參數來切換背景幕是否針對拖曳事件做出回應。

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    },
    // Defaults to BackdropScaffoldDefaults.PeekHeight
    peekHeight = 40.dp,
    // Defaults to BackdropScaffoldDefaults.HeaderHeight
    headerHeight = 60.dp,
    // Defaults to true
    gesturesEnabled = false
)

透過 BackdropScaffoldState 以程式輔助方式揭露及隱藏背景幕。您可以使用 rememberBackdropScaffoldState 建立應使用 scaffoldState 參數傳送至 BackdropScaffoldBackdropScaffoldState 執行個體。BackdropScaffoldState 提供 revealconceal 函式的存取權,以及與目前背景幕狀態相關的屬性。這些暫停函式需要 CoroutineScope (例如使用 rememberCoroutineScope),並且可以呼叫這些函式以回應 UI 事件。

val scaffoldState = rememberBackdropScaffoldState(
    BackdropValue.Concealed
)
val scope = rememberCoroutineScope()
BackdropScaffold(
    scaffoldState = scaffoldState,
    appBar = {
        TopAppBar(
            title = { Text("Backdrop") },
            navigationIcon = {
                if (scaffoldState.isConcealed) {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.reveal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = "Menu"
                        )
                    }
                } else {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.conceal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Close,
                            contentDescription = "Close"
                        )
                    }
                }
            },
            elevation = 0.dp,
            backgroundColor = Color.Transparent
        )
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)