Material Design 2 trong Compose

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ữ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.

Hai ảnh chụp màn hình tương phản. Ảnh chụp đầu tiên tạo kiểu MaterialTheme theo mặc định, ảnh chụp thứ hai sử dụng kiểu đã qua chỉnh sửa.

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.

Ví dụ về bảng màu giao diện

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 MaterialThemebằ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.

Hai ví dụ về cùng một biểu ngữ nhưng có màu sắc khác nhau

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, TextIcon 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.

Ảnh chụp màn hình của tiêu đề bài viết cho thấy các cấp độ nhấn mạnh văn bản khác nhau

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_24dp
        } else {
          R.drawable.ic_moon_24dp
        }
    ),
    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
    /*...*/
) { /*...*/ }

Ảnh chụp màn hình một ứng dụng cho thấy màu sắc có sự khác biệt nhẹ được sử dụng cho các phần tử ở nhiều độ nâng

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ư TopAppBarBottomNavigation 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.

Ví dụ về một số kiểu chữ dưới nhiều dạng khác nhau

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, TextStylecá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 Rubik = FontFamily(
    Font(R.font.rubik_regular),
    Font(R.font.rubik_medium, FontWeight.W500),
    Font(R.font.rubik_bold, FontWeight.Bold)
)

val MyTypography = Typography(
    h1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = Rubik,
        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 = Rubik)
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
)

Ảnh chụp màn hình cho thấy có sự kết hợp nhiều kiểu chữ khác nhau cho các mục đích khác nhau

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ỏ.

Cho thấy nhiều hình dạng của Material Design

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 ,TextFieldFloatingActionButton 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, /*...*/
) {
    /*...*/
}

Ảnh chụp màn hình ứng dụng sử dụng hình dạng Material để cho biết trạng thái của một thành phần

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 mẫu Owl, màn hình sử dụng PinkTheme cho phần chi tiết trên hầu hết 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 trong mẫu Owl.

@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, chuyển đổi, 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

Material Components (Thành phần trong Material Design) 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ư clickableindication ,

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 defaultRippleColordefaultRippleAlpha. 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
  )
}

alt_text

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