Jetpack Compose cung cấp cách triển khai Material Design, một hệ thống thiết kế toàn diện để tạo các giao diện số. Các thành phần Material Design (nút, thẻ, nút chuyển, v.v.) được xây dựng dựa trên Tuỳ chỉnh Material Design để phản ánh tốt hơn thương hiệu của sản phẩm một cách có hệ thống. Giao diện (theme) Material Design chứa các thuộc tính màu sắc, kiểu chữ và hình dạng. Khi bạn tuỳ chỉnh các thuộc tính này, thay đổi sẽ tự động phản ánh trong các thành phần mà bạn sử dụng để xây dựng ứng dụng.
Jetpack Compose triển khai các khái niệm này bằng thành phần kết hợp (composable) MaterialTheme
:
MaterialTheme( colors = // ... typography = // ... shapes = // ... ) { // app content }
Định cấu hình các tham số mà bạn truyền đến MaterialTheme
để tuỳ chỉnh giao diện cho ứng dụng.
Hình 1. Ảnh chụp màn hình đầu tiên cho thấy một ứng dụng không định cấu hình
MaterialTheme
, do đó, ứng dụng này định kiểu theo mặc định. Ảnh chụp màn hình thứ hai cho thấy một ứng dụng truyền các tham số đến MaterialTheme
để tuỳ chỉnh cách định kiểu.
Màu
Màu sắc được mô hình hoá trong Compose bằng lớp Color
, một lớp chứa dữ liệu đơn giản.
val Red = Color(0xffff0000) val Blue = Color(red = 0f, green = 0f, blue = 1f)
Mặc dù bạn có thể sắp xếp những lớp này theo bất cứ cách nào bạn muốn (như hằng số cấp cao nhất, trong một singleton hoặc trong cùng dòng đã được xác định), chúng tôi đặc biệt khuyên bạn nên chỉ định màu trong giao diện và truy xuất màu từ đó. Cách thức này giúp bạn có thể dễ dàng hỗ trợ các giao diện tối và giao diện lồng nhau.
Hình 2. Hệ màu Material.
Compose cung cấp lớp Colors
để lập mô hình
Hệ màu Material. Colors
cung cấp cho
các hàm trình tạo (builder) để tạo các nhóm màu sáng
hoặc màu tối:
private val Yellow200 = Color(0xffffeb46) private val Blue200 = Color(0xff91a4fc) // ... private val DarkColors = darkColors( primary = Yellow200, secondary = Blue200, // ... ) private val LightColors = lightColors( primary = Yellow500, primaryVariant = Yellow400, secondary = Blue700, // ... )
Sau khi xác định Colors
, bạn có thể truyền các màu đó đến MaterialTheme
:
MaterialTheme( colors = if (darkTheme) DarkColors else LightColors ) { // app content }
Sử dụng màu giao diện
Bạn có thể truy xuất Colors
được cung cấp cho thành phần kết hợp MaterialTheme
bằng cách sử dụng MaterialTheme.colors
.
Text( text = "Hello theming", color = MaterialTheme.colors.primary )
Màu bề mặt và màu nội dung
Nhiều thành phần chấp nhận một cặp màu và màu nội dung:
Surface( color = MaterialTheme.colors.surface, contentColor = contentColorFor(color), // ... ) { /* ... */ } TopAppBar( backgroundColor = MaterialTheme.colors.primarySurface, contentColor = contentColorFor(backgroundColor), // ... ) { /* ... */ }
Do vậy, bạn có thể không chỉ thiết lập màu cho một thành phần kết hợp, mà còn cung cấp màu mặc định cho nội dung, các thành phần kết hợp trong đó. Nhiều thành phần kết hợp sử dụng màu nội dung này theo mặc định. Ví dụ: màu của Text
dựa trên màu nội dung của lớp mẹ, còn Icon
sử dụng màu đó để đặt sắc thái màu.
Hình 3. Việc đặt màu nền khác nhau sẽ tạo ra các màu của biểu tượng và văn bản khác nhau.
Phương thức contentColorFor()
truy xuất màu "trên" thích hợp cho màu của bất kỳ giao diện nào. Ví dụ: nếu bạn đặt màu nền primary
lên
Surface
thì hàm này sẽ được dùng để đặt onPrimary
làm màu cho nội dung. Nếu đặt màu nền không theo giao diện, bạn cũng nên chỉ định màu nội dung phù hợp. Sử dụng LocalContentColor
để truy xuất màu nội dung ưu tiên cho nền hiện tại, tại một
vị trí nhất định trong hệ phân cấp.
Độ đậm nhạt của nội dung
Thông thường, bạn muốn thay đổi mức độ nhấn mạnh nội dung để truyền tải mức độ quan trọng và mang lại sự phân cấp thị giác. Các tài liệu đề xuất về mức độ dễ đọc văn bản trong Material Design khuyến cáo nên sử dụng nhiều mức độ đậm nhạt để truyền tải các mức độ quan trọng khác nhau.
Jetpack Compose dùng cách này để triển khai LocalContentAlpha
.
Bạn có thể chỉ định độ đậm nhạt (alpha) của nội dung cho hệ thống phân cấp bằng cách cung cấp giá trị cho CompositionLocal
.
Các thành phần kết hợp lồng ghép sử dụng giá trị này để áp dụng biện pháp xử lý độ đậm nhạt cho nội dung của chúng.
Ví dụ: theo mặc định, Text
và Icon
sử dụng tổ hợp LocalContentColor
để sử dụng LocalContentAlpha
. Material chỉ định một số giá trị độ đậm nhạt chuẩn (high
, medium
,
disabled
) được mô hình hoá bởi đối tượng ContentAlpha
.
// By default, both Icon & Text use the combination of LocalContentColor & // LocalContentAlpha. De-emphasize content by setting content alpha CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text( // ... ) } CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) { Icon( // ... ) Text( // ... ) }
Để tìm hiểu thêm về CompositionLocal
, hãy xem dữ liệu phạm vi cục bộ qua hướng dẫn về CompositionLocal.
Hình 4. Áp dụng nhiều mức độ nhấn mạnh cho văn bản để truyền đạt thứ tự cấp bậc thông tin một cách trực quan. Dòng văn bản đầu tiên là tiêu đề và chứa thông tin quan trọng nhất, do đó sử dụng ContentAlpha.high
. Dòng thứ hai chứa siêu dữ liệu ít quan trọng hơn, do đó sử dụng ContentAlpha.medium
.
Giao diện tối
Trong Compose, bạn cài đặt giao diện tối và sáng bằng cách cung cấp các nhóm Colors
khác nhau cho thành phần kết hợp MaterialTheme
:
@Composable fun MyTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { MaterialTheme( colors = if (darkTheme) DarkColors else LightColors, /*...*/ content = content ) }
Trong ví dụ này, MaterialTheme
được bọc trong một thành phần kết hợp riêng chấp nhận một tham số chỉ định xem có nên sử dụng giao diện tối hay không. Trong trường hợp này, hàm sẽ nhận giá trị mặc định cho darkTheme
bằng cách truy vấn
chế độ cài đặt giao diện trên thiết bị.
Bạn có thể sử dụng mã như thế này để kiểm tra xem Colors
hiện tại là sáng hay tối:
val isLightTheme = MaterialTheme.colors.isLight Icon( painterResource( id = if (isLightTheme) { R.drawable.ic_sun_24 } else { R.drawable.ic_moon_24 } ), contentDescription = "Theme" )
Lớp phủ độ nâng
Trong Material, các bề mặt (surface) trong giao diện tối có độ nâng cao hơn sẽ nhận được lớp phủ độ nâng để làm sáng nền tương ứng. Bề mặt có độ nâng càng cao (nâng độ nâng lên gần với nguồn sáng ngầm ẩn) thì bề mặt đó càng sáng.
Các lớp phủ này được thành phần kết hợp Surface
tự động áp dụng khi sử dụng màu tối cũng như cho mọi thành phần kết hợp Material khác sử dụng một bề mặt:
Surface( elevation = 2.dp, color = MaterialTheme.colors.surface, // color will be adjusted for elevation /*...*/ ) { /*...*/ }
Hình 5. Các thẻ và phần điều hướng dưới cùng đều sử dụng màu surface
làm màu nền. Vì thẻ và phần điều hướng dưới cùng có độ nâng khác nhau phía trên nền, nên màu sắc của chúng sẽ có sự khác biệt nhẹ – màu thẻ sáng hơn màu nền và màu của phần điều hướng dưới cùng sáng hơn màu thẻ.
Đối với các trường hợp tuỳ chỉnh không liên quan đến Surface
, hãy sử dụng LocalElevationOverlay
, một CompositionLocal
chứa ElevationOverlay
được dùng trong các thành phần Surface
:
// Elevation overlays // Implemented in Surface (and any components that use it) val color = MaterialTheme.colors.surface val elevation = 4.dp val overlaidColor = LocalElevationOverlay.current?.apply( color, elevation )
Để tắt lớp phủ nâng, hãy cung cấp null
tại điểm mong muốn trong hệ phân cấp thành phần kết hợp:
MyTheme { CompositionLocalProvider(LocalElevationOverlay provides null) { // Content without elevation overlays } }
Hạn chế các màu nhấn
Material Design đề xuất áp dụng một số màu nhấn hạn chế cho giao diện tối bằng cách ưu tiên sử dụng màu surface
thay cho màu primary
trong hầu hết các trường hợp. Theo mặc định, các thành phần kết hợp trong Material như TopAppBar
và BottomNavigation
sẽ triển khai hành vi này.
Hình 6. Giao diện tối có màu nhấn hạn chế trong Material. Thanh ứng dụng ở trên cùng sử dụng màu chính trong giao diện sáng và màu bề mặt trong giao diện tối.
Đối với các trường hợp tuỳ chỉnh, hãy sử dụng thuộc tính mở rộng primarySurface
:
Surface( // Switches between primary in light theme and surface in dark theme color = MaterialTheme.colors.primarySurface, /*...*/ ) { /*...*/ }
Kiểu chữ
Material xác định hệ thống kiểu chữ, khuyến khích bạn sử dụng một số ít các kiểu được đặt tên theo ngữ nghĩa.
Hình 7. Hệ thống kiểu chữ của Material.
Compose triển khai hệ thống kiểu chữ bằng lớp Typography
, TextStyle
và các lớp liên quan đến phông chữ. Hàm khởi tạo (constructor) Typography
cung cấp các tuỳ chọn mặc định cho từng kiểu để bạn có thể bỏ qua bất kỳ kiểu nào bạn không muốn tuỳ chỉnh:
val raleway = FontFamily( Font(R.font.raleway_regular), Font(R.font.raleway_medium, FontWeight.W500), Font(R.font.raleway_semibold, FontWeight.SemiBold) ) val myTypography = Typography( h1 = TextStyle( fontFamily = raleway, fontWeight = FontWeight.W300, fontSize = 96.sp ), body1 = TextStyle( fontFamily = raleway, fontWeight = FontWeight.W600, fontSize = 16.sp ) /*...*/ ) MaterialTheme(typography = myTypography, /*...*/) { /*...*/ }
Nếu bạn muốn sử dụng cùng một kiểu chữ, hãy chỉ định defaultFontFamily parameter
và bỏ qua fontFamily
của bất kỳ phần tử TextStyle
nào:
val typography = Typography(defaultFontFamily = raleway) MaterialTheme(typography = typography, /*...*/) { /*...*/ }
Sử dụng kiểu văn bản
Truy cập TextStyle
thông qua MaterialTheme.typography
. Truy xuất
TextStyle
như sau:
Text( text = "Subtitle2 styled", style = MaterialTheme.typography.subtitle2 )
Hình 8. Sử dụng một số kiểu chữ và cách tạo kiểu được chọn để thể hiện thương hiệu.
Hình dạng
Material xác định hệ thống hình dạng, cho phép bạn xác định các hình dạng cho các thành phần lớn, vừa và nhỏ.
Hình 9. Hệ thống hình dạng Material.
Compose triển khai hệ thống hình dạng bằng lớp
Shapes
, cho phép bạn chỉ định
CornerBasedShape
cho từng danh mục kích thước:
val shapes = Shapes( small = RoundedCornerShape(percent = 50), medium = RoundedCornerShape(0f), large = CutCornerShape( topStart = 16.dp, topEnd = 0.dp, bottomEnd = 0.dp, bottomStart = 16.dp ) ) MaterialTheme(shapes = shapes, /*...*/) { /*...*/ }
Nhiều thành phần sử dụng các hình dạng này theo mặc định. Ví dụ: Button
, TextField
và FloatingActionButton
mặc định là hình nhỏ, AlertDialog
mặc định là hình trung bình và ModalDrawer
mặc định là hình lớn — xem tham chiếu lược đồ hình dạng để liên kết hoàn chỉnh.
Sử dụng hình dạng
Truy cập Shape
thông qua MaterialTheme.shapes
. Truy xuất các Shape
bằng mã như sau:
Surface( shape = MaterialTheme.shapes.medium, /*...*/ ) { /*...*/ }
Hình 10. Sử dụng hình dạng để thể hiện thương hiệu hoặc trạng thái.
Kiểu mặc định
Không có khái niệm tương đương về các kiểu mặc định trong Compose từ Chế độ xem Android. Bạn có thể cung cấp chức năng tương tự bằng cách tạo các chức năng kết hợp 'nạp chồng' riêng bọc các thành phần Material. Ví dụ: để tạo kiểu nút, gói nút trong hàm có khả năng kết hợp của riêng bạn, hãy trực tiếp đặt các tham số bạn muốn thay đổi và cấp quyền truy cập cho các tham số khác dưới dạng tham số cho thành phần kết hợp chứa tham số.
@Composable fun MyButton( onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { Button( colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.colors.secondary ), onClick = onClick, modifier = modifier, content = content ) }
Lớp phủ giao diện
Bạn có thể đạt được mức tương đương với
lớp phủ giao diện trong thành phần hiển thị Android trong Compose bằng cách lồng ghép thành phần kết hợp
MaterialTheme
. Vì
MaterialTheme
đặt giá trị mặc định cho màu sắc, kiểu chữ và hình dạng theo giá trị giao diện hiện tại, nên nếu giao diện chỉ đặt một trong các tham số đó thì các tham số khác sẽ giữ nguyên giá trị mặc định.
Ngoài ra, khi di chuyển màn hình dựa trên Chế độ xem sang Compose, hãy lưu ý đến việc sử dụng thuộc tính android:theme
. Có thể bạn cần một
MaterialTheme
mới trong phần đó của cây Giao diện Compose.
Trong ví dụ này, màn hình chi tiết sử dụng PinkTheme
cho phần lớn màn hình và BlueTheme
cho phần có liên quan. Hãy xem ảnh chụp màn hình và mã bên dưới.
Hình 11. Giao diện lồng nhau.
@Composable fun DetailsScreen(/* ... */) { PinkTheme { // other content RelatedSection() } } @Composable fun RelatedSection(/* ... */) { BlueTheme { // content } }
Trạng thái thành phần
Các thành phần Material có thể tương tác (được nhấp, bật/tắt, v.v.) có thể ở các trạng thái thị giác khác nhau. Các trạng thái bao gồm bật, tắt, được nhấn, v.v.
Các thành phần kết hợp thường có tham số enabled
. Việc đặt thành false
sẽ ngăn tương tác và thay đổi các thuộc tính như màu sắc và elevation để cho biết trạng thái thành phần một cách trực quan.
Hình 12. Nút được enabled = true
(bên trái) và enabled = false
(phải).
Trong hầu hết các trường hợp, bạn có thể dựa vào giá trị mặc định như màu sắc và elevation. Nếu muốn định cấu hình các giá trị dùng trong các trạng thái khác nhau, bạn có thể dùng các lớp và hàm tiện lợi có sẵn. Hãy xem ví dụ về nút bên dưới:
Button( onClick = { /* ... */ }, enabled = true, // Custom colors for different states colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.colors.secondary, disabledBackgroundColor = MaterialTheme.colors.onBackground .copy(alpha = 0.2f) .compositeOver(MaterialTheme.colors.background) // Also contentColor and disabledContentColor ), // Custom elevation for different states elevation = ButtonDefaults.elevation( defaultElevation = 8.dp, disabledElevation = 2.dp, // Also pressedElevation ) ) { /* ... */ }
Hình 13. Nút được enabled = true
(trái) và enabled = false
(phải) với các giá trị màu và evaluation đã điều chỉnh.
Hiệu ứng gợn sóng
Thành phần Material sử dụng hiệu ứng gợn sóng để cho biết chúng đang được tương tác. Nếu bạn đang sử dụngMaterialTheme
trong hệ thống phân cấp,Ripple
sẽ được dùng làm Indication
mặc định bên trong công cụ sửa đổi, chẳng hạn như
clickable
và
indication
,
Trong hầu hết các trường hợp, bạn có thể sử dụng Ripple
mặc định. Nếu muốn định cấu hình hiển thị, bạn có thể sử dụng RippleTheme
để thay đổi các thuộc tính như màu sắc và alpha.
Bạn có thể kế thừa RippleTheme
và sử dụng các hàm số hiệu dụng
defaultRippleColor
và
defaultRippleAlpha
. Sau đó, bạn có thể cung cấp giao diện gợn sóng tuỳ chỉnh trong hệ thống phân cấp bằng
LocalRippleTheme
:
@Composable fun MyApp() { MaterialTheme { CompositionLocalProvider( LocalRippleTheme provides SecondaryRippleTheme ) { // App content } } } @Immutable private object SecondaryRippleTheme : RippleTheme { @Composable override fun defaultColor() = RippleTheme.defaultRippleColor( contentColor = MaterialTheme.colors.secondary, lightTheme = MaterialTheme.colors.isLight ) @Composable override fun rippleAlpha() = RippleTheme.defaultRippleAlpha( contentColor = MaterialTheme.colors.secondary, lightTheme = MaterialTheme.colors.isLight ) }
Hình 14. Các nút có các giá trị gợn sóng khác nhau được cung cấp qua RippleTheme
.
Tìm hiểu thêm
Để tìm hiểu thêm về Tuỳ chỉnh Material Design trong Compose, hãy tham khảo các tài nguyên khác sau đây.
Lớp học lập trình
Video
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Hệ thống thiết kế tuỳ chỉnh trong Compose
- Di chuyển từ Material 2 sang Material 3 trong Compose
- Hỗ trợ tiếp cận trong Compose