Tuỳ chỉnh giao diện trong Jetpack Compose

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng các API giao diện của Jetpack Compose để tạo kiểu cho ứng dụng. Chúng ta sẽ tìm hiểu cách tuỳ chỉnh màu sắc, hình dạng và kiểu chữ để sử dụng chúng một cách nhất quán trong ứng dụng, hỗ trợ nhiều giao diện như giao diện sáng và tối.

Kiến thức bạn sẽ học được

Trong lớp học lập trình này, bạn sẽ tìm hiểu:

  • Sơ lược về Material Design và cách tuỳ chỉnh Material Design cho thương hiệu
  • Cách Compose triển khai hệ thống Material Design
  • Cách xác định và sử dụng màu sắc, kiểu chữ cũng như hình dạng trên toàn ứng dụng
  • Cách tạo kiểu cho các thành phần
  • Cách hỗ trợ các giao diện sáng và tối

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, chúng ta sẽ định kiểu cho một ứng dụng đọc tin tức. Hãy bắt đầu với một ứng dụng chưa định kiểu và áp dụng những gì đã học để tạo giao diện cho ứng dụng cũng như hỗ trợ giao diện tối.

Hình ảnh về Jetnews, một ứng dụng đọc tin tức, trước khi áp dụng các kiểu.

Hình ảnh về Jetnews, một ứng dụng đọc tin tức, sau khi áp dụng các kiểu.

Hình ảnh về Jetnews, một ứng dụng đọc tin tức, được tạo kiểu trong giao diện tối.

Trước: ứng dụng chưa định kiểu

Sau: ứng dụng đã được định kiểu

Sau: giao diện tối

Điều kiện tiên quyết

2. Thiết lập

Ở bước này, bạn sẽ tải mã xuống cho lớp học lập trình này. Mã này bao gồm cả một ứng dụng đơn giản để đọc tin tức mà chúng ta sẽ định kiểu.

Bạn cần có

Tải mã xuống

Nếu đã cài đặt git, bạn có thể chỉ cần chạy lệnh bên dưới. Để kiểm tra xem git đã được cài đặt hay chưa, hãy nhập git --version vào dòng lệnh hoặc cửa sổ dòng lệnh và xác minh rằng mã này được thực thi đúng cách.

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ThemingCodelabM2

Nếu không có git, bạn có thể nhấp vào nút sau đây để tải tất cả mã dành cho lớp học lập trình này:

Mở dự án trong Android Studio rồi chọn 'File (Tệp) > Import Project (Nhập dự án)', sau đó duyệt đến thư mục ThemingCodelabM2.

Dự án này có 3 gói chính:

  • com.codelab.theming.data Mã này chứa các lớp mô hình và dữ liệu mẫu. Bạn không cần chỉnh sửa gói này trong lớp học lập trình này.
  • com.codelab.theming.ui.start Đây là điểm xuất phát của lớp học lập trình. Bạn nên thực hiện tất cả thay đổi được yêu cầu ở lớp học lập trình trong gói này.
  • com.codelab.theming.ui.finish Đây là trạng thái kết thúc của lớp học lập trình để bạn tham khảo.

Tạo và chạy ứng dụng

Ứng dụng có 2 cấu hình chạy phản ánh trạng thái bắt đầu và kết thúc của lớp học lập trình. Chọn một trong hai cấu hình và nhấn vào nút chạy sẽ triển khai mã cho thiết bị hoặc trình mô phỏng của bạn.

a43ae3c4fa75836e.png

Ứng dụng này cũng chứa Bản xem trước bố cục trong Compose. Khi duyệt đến Home.kt trong gói start hoặc finish, và mở chế độ xem bản thiết kế, bạn sẽ thấy một số bản xem trước cho phép lặp lại nhanh trên mã giao diện người dùng:

758a285ad8a6cd51.png

3. Tuỳ chỉnh giao diện Material

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ẻ, Công tắc, v.v.) được xây dựng dựa trên tính năng Tuỳ chỉnh giao diện Material để tuỳ chỉnh Material Design, nhằm 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 Material 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.

Bạn nên nắm được kiến thức về tính năng Tuỳ chỉnh giao diện Material để hiểu cách tạo giao diện cho ứng dụng Jetpack Compose. Dưới đây là phần mô tả ngắn gọn về các khái niệm đó. Nếu đã quen thuộc với tính năng Tuỳ chỉnh giao diện Material, bạn có thể bỏ qua phần này.

Màu

Material Design xác định một số màu được đặt tên theo ngữ nghĩa mà bạn có thể sử dụng trong toàn bộ ứng dụng của mình:

62ccfe5761fd9eda.png

Màu chính là màu cốt lõi của thương hiệu và màu phụ được dùng để tạo điểm nhấn. Bạn có thể cung cấp biến thể tối/sáng hơn cho khu vực cần độ tương phản. Màu nền và màu bề mặt được sử dụng cho các vùng chứa chứa thành phần thường nằm trên "bề mặt" trong ứng dụng. Material cũng xác định màu "nằm trên" – màu dùng cho nội dung nằm trên một trong các màu được đặt tên, ví dụ như văn bản trong vùng chứa màu "bề mặt" phải có màu "trên bề mặt". Thành phần Material được định cấu hình để sử dụng các màu giao diện này, chẳng hạn như theo mặc định, Nút hành động nổi có màu secondary, Thẻ mặc định là màu surface, v.v.

Bằng cách xác định màu được đặt tên, bạn có thể cung cấp bảng màu thay thế, chẳng hạn như cả giao diện sáng và tối:

1a9b78141ddfa87b.png

Ngoài ra, bạn cũng nên xác định một bảng màu nhỏ và sử dụng các màu đó nhất quán trong toàn bộ ứng dụng. Công cụ chọn màu Material có thể giúp bạn chọn màu và tạo bảng màu, thậm chí còn đảm bảo việc dễ dàng kết hợp các màu.

Kiểu chữ

Tương tự, Material xác định một số kiểu loại được đặt tên theo ngữ nghĩa:

1d44de3ff2f7fd1c.png

Mặc dù bạn có thể không thay đổi kiểu loại theo chủ đề, nhưng việc sử dụng kiểu chữ sẽ làm tăng tính nhất quán trong ứng dụng của bạn. Việc cung cấp phông chữ và các phần tuỳ chỉnh loại khác sẽ được phản ánh trong thành phần Material mà bạn sử dụng trong ứng dụng, ví dụ: Thanh ứng dụng sử dụng kiểu h6 theo mặc định, việc sử dụng Nút, lỗi, button Công cụ tạo tỷ lệ kiểu chữ của Material có thể giúp bạn tạo kiểu chữ.

Hình dạng

Material hỗ trợ sử dụng hình dạng một cách có hệ thống để truyền tải thương hiệu của bạn. Thẻ này xác định 3 loại: thành phần nhỏ, trung bình và lớn; mỗi kiểu có thể xác định một hình dạng để sử dụng, tuỳ chỉnh kiểu góc (cắt hoặc bo tròn) và kích thước.

886b811cc9cad18e.png

Kết quả tuỳ chỉnh giao diện hình dạng sẽ được phản ánh trên nhiều thành phần, ví dụ như NútTrường văn bản sử dụng giao diện hình dạng kích cỡ nhỏ, ThẻHộp thoại sử dụng kích cỡ trung bình và Trang tính sử dụng giao diện hình dạng kích cỡ lớn theo mặc định. Bạn có thể liên kết đầy đủ các thành phần để định hình các giao diện tại đây. Công cụ tuỳ chỉnh hình dạng Material có thể giúp bạn tạo giao diện hình dạng.

Cơ sở

Material mặc định có giao diện "cơ sở" (tức là bảng phối màu tím, tỷ lệ kiểu chữ loại Roboto và hình dạng hơi tròn được thấy trong các hình ảnh ở trên. Nếu bạn không chỉ định hoặc tuỳ chỉnh giao diện thì các thành phần sẽ sử dụng giao diện cơ sở.

4. Xác định giao diện của bạn

MaterialTheme

Phần tử cốt lõi để triển khai giao diện trong Jetpack Compose là thành phần kết hợp MaterialTheme. Việc đặt thành phần kết hợp này vào hệ phân cấp của Compose cho phép bạn chỉ định các phần tuỳ chỉnh về màu sắc, loại và hình dạng cho mọi thành phần trong đó. Dưới đây là cách xác định thành phần kết hợp này trong thư viện:

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) { ...

Sau đó, bạn có thể truy xuất các tham số được chuyển vào thành phần kết hợp này bằng cách sử dụng object MaterialTheme để hiện các thuộc tính colors, typographyshapes. Chúng ta sẽ đi sâu vào từng phương pháp này sau.

Mở Home.kt rồi tìm hàm có khả năng kết hợp Home — đây là điểm truy cập chính vào ứng dụng. Vui lòng lưu ý trong khi khai báo một MaterialTheme, chúng ta sẽ không chỉ định tham số nào sẽ nhận kiểu "cơ sở" mặc định:

@Composable
fun Home() {
  ...
  MaterialTheme {
    Scaffold(...

Hãy tạo các tham số màu, kiểu và hình dạng để triển khai một giao diện cho ứng dụng.

Tạo một giao diện

Để tập trung vào kiểu, bạn nên tạo thành phần kết hợp riêng để gói và định cấu hình MaterialTheme. Nhờ đó, bạn có thể chỉ định một nơi để tuỳ chỉnh giao diện cũng như dễ dàng sử dụng lại ở nhiều vị trí, chẳng hạn như trên nhiều màn hình hoặc @Preview. Bạn có thể tạo nhiều thành phần kết hợp giao diện nếu cần, chẳng hạn như nếu bạn muốn hỗ trợ các kiểu riêng cho từng phần của ứng dụng.

Trong gói này, com.codelab.theming.ui.start.theme hãy tạo một tệp Kotlin mới có tên Theme.kt. Thêm hàm có khả năng kết hợp mới gọi là JetnewsTheme. Hàm này chấp nhận các thành phần kết hợp khác làm nội dung và gói MaterialTheme:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(content = content)
}

Bây giờ, hãy quay lại Home.kt và thay thế MaterialTheme bằng JetnewsTheme (và nhập nó):

-  MaterialTheme {
+  JetnewsTheme {
    ...

Bạn sẽ chưa nhận thấy bất kỳ thay đổi nào trong @Preview trên màn hình này. Cập nhật PostItemPreviewFeaturedPostPreview để gói nội dung của chúng bằng thành phần kết hợp JetnewsTheme mới để các bản xem trước sử dụng giao diện mới của chúng ta:

@Preview("Featured Post")
@Composable
private fun FeaturedPostPreview() {
  val post = remember { PostRepo.getFeaturedPost() }
+ JetnewsTheme {
    FeaturedPost(post = post)
+ }
}

Màu

Đây là bảng màu chúng tôi muốn triển khai trong ứng dụng của mình (hiện tại đây chỉ là một bảng màu sáng, nhưng chúng tôi sẽ sớm hỗ trợ giao diện tối):

b2635ed3ec4bfc8f.png

Màu sắc trong Compose được xác định bằng cách sử dụng lớp Color. Có nhiều hàm dựng cho phép bạn chỉ định màu dưới dạng ULong hoặc bằng kênh màu riêng biệt.

Tạo tệp Color.kt mới trong gói theme. Thêm các màu sau làm thuộc tính công khai cấp cao nhất trong tệp này:

val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)

Lúc này, khi đã xác định được màu của ứng dụng, hãy kéo các màu đó vào một đối tượng ColorsMaterialTheme yêu cầu, rồi chỉ định màu cụ thể cho các màu đã đặt tên của Material. Chuyển về Theme.kt rồi thêm vào như sau:

private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

Ở đây, chúng ta sử dụng hàm lightColors để tạo Colors. Việc này cung cấp các giá trị mặc định hợp lý để chúng ta không phải chỉ định tất cả các màu tạo nên một bảng màu Material. Ví dụ, lưu ý chúng ta chưa chỉ định màu background hoặc nhiều màu "nằm trên", chúng ta sẽ sử dụng giá trị mặc định.

Lúc này, hãy sử dụng những màu này trong ứng dụng. Cập nhật thành phần kết hợp JetnewsTheme để sử dụng Colors mới của chúng ta:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
+   colors = LightColors,
    content = content
  )
}

Mở Home.kt rồi làm mới bản xem trước. Hãy lưu ý bảng phối màu mới được phản ánh trong các thành phần như TopAppBar.

Kiểu chữ

Dưới đây là tỷ lệ kiểu chữ mà chúng tôi muốn triển khai trong ứng dụng của mình:

54c420f78529b77d.png

Trong Compose, chúng ta có thể xác định các đối tượng TextStyle để xác định thông tin cần thiết nhằm tạo kiểu cho một số văn bản. Dưới đây là một ví dụ về các thuộc tính này:

data class TextStyle(
    val color: Color = Color.Unset,
    val fontSize: TextUnit = TextUnit.Inherit,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontFamily: FontFamily? = null,
    val letterSpacing: TextUnit = TextUnit.Inherit,
    val background: Color = Color.Unset,
    val textAlign: TextAlign? = null,
    val textDirection: TextDirection? = null,
    val lineHeight: TextUnit = TextUnit.Inherit,
    ...
)

Tỷ lệ kiểu mong muốn của chúng tôi sẽ sử dụng Montserrat cho tiêu đề và Domine cho nội dung văn bản. Các tệp phông chữ có liên quan đã được thêm vào thư mục res/fonts của dự án.

Tạo tệp Typography.kt mới trong gói theme. Trước tiên, hãy xác định FontFamily (kết hợp các trọng số khác nhau của từng Font ):

private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)

Bây giờ, hãy tạo một đối tượng TypographyMaterialTheme chấp nhận, chỉ định TextStyle cho mỗi kiểu ngữ nghĩa trong tỷ lệ:

val JetnewsTypography = Typography(
    h4 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 30.sp
    ),
    h5 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 24.sp
    ),
    h6 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 20.sp
    ),
    subtitle1 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    ),
    subtitle2 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    body2 = TextStyle(
        fontFamily = Montserrat,
        fontSize = 14.sp
    ),
    button = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    ),
    overline = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 12.sp
    )
)

Mở Theme.kt và cập nhật thành phần kết hợp JetnewsTheme để sử dụng Typography mới:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
+   typography = JetnewsTypography,
    content = content
  )
}

Mở Home.kt và làm mới bản xem trước để xem kiểu chữ mới mới có hiệu lực.

Hình dạng

Chúng ta muốn sử dụng hình dạng để thể hiện thương hiệu của mình trong ứng dụng. Chúng ta muốn sử dụng hình dạng góc cắt trên một số phần tử:

9b60c78a78c61570.png

Compose cung cấp các lớp RoundedCornerShapeCutCornerShape mà bạn có thể dùng để xác định giao diện hình dạng.

Tạo tệp mới Shape.kt trong gói theme và thêm nội dung sau:

val JetnewsShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)

Mở Theme.kt và cập nhật thành phần kết hợp JetnewsTheme để sử dụng các Shapes sau:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
    typography = JetnewsTypography,
+   shapes = JetnewsShapes,
    content = content
  )
}

Mở Home.kt rồi làm mới bản xem trước để xem Card cho thấy bài đăng nổi bật phản ánh giao diện hình dạng mới được áp dụng như thế nào.

Giao diện tối

Việc hỗ trợ giao diện tối trong ứng dụng không chỉ giúp ứng dụng của bạn tích hợp tốt hơn trên thiết bị của người dùng (có chuyển đổi giao diện tối trên toàn cục từ Android 10 trở đi), mà còn có thể giảm mức sử dụng điện năng và hỗ trợ các nhu cầu hỗ trợ tiếp cận. Material cung cấp hướng dẫn thiết kế về cách tạo giao diện tối. Dưới đây là một bảng màu thay thế mà chúng tôi muốn triển khai cho giao diện tối:

21768b33f0ccda5f.png

Mở Color.kt và thêm các màu sau:

val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)

Bây giờ, hãy mở Theme.kt rồi thêm:

private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

Bây giờ, hãy cập nhật JetnewsTheme:

@Composable
fun JetnewsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  MaterialTheme(
+   colors = if (darkTheme) DarkColors else LightColors,
    typography = JetnewsTypography,
    shapes = JetnewsShapes,
    content = content
  )
}

Ở đây, chúng ta thêm một tham số mới về việc có sử dụng giao diện tối hay không, rồi mặc định sử dụng giao diện này để truy vấn thiết bị về chế độ cài đặt chung. Điều này mang lại cho chúng ta một giá trị mặc định tối ưu, nhưng vẫn rất dễ dẫn đến tình trạng ghi đè nếu chúng ta muốn một màn hình cụ thể luôn/không bao giờ tối hoặc để tạo giao diện @Preview theo chủ đề tối.

Mở Home.kt rồi tạo một bản xem trước mới cho thành phần kết hợp FeaturedPost để hiển thị bản xem trước đó trong giao diện tối:

@Preview("Featured Post • Dark")
@Composable
private fun FeaturedPostDarkPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme(darkTheme = true) {
        FeaturedPost(post = post)
    }
}

Làm mới ngăn xem trước để xem bản xem trước của giao diện tối.

84f93b209ce4fd46.png

5. Làm việc với màu sắc

Trong bước cuối cùng, chúng ta đã xem cách tạo giao diện riêng để thiết lập màu sắc, kiểu và hình dạng cho ứng dụng. Mọi thành phần Material đều sử dụng những phần tuỳ chỉnh này ngay từ đầu. Ví dụ: thành phần kết hợp FloatingActionButton mặc định sử dụng màu secondary trên giao diện, nhưng bạn có thể thiết lập màu thay thế bằng cách chỉ định một giá trị khác cho tham số này:

@Composable
fun FloatingActionButton(
  backgroundColor: Color = MaterialTheme.colors.secondary,
  ...
) {

Không phải lúc nào bạn cũng muốn sử dụng chế độ cài đặt mặc định, phần này trình bày cách làm việc với màu sắc trong ứng dụng.

Màu thô

Như chúng ta đã thấy, tính năng compose cung cấp một lớp Color. Bạn có thể tạo các tệp này cục bộ, giữ chúng trong object, v.v.:

Surface(color = Color.LightGray) {
  Text(
    text = "Hard coded colors don't respond to theme changes :(",
    textColor = Color(0xffff00ff)
  )
}

Color có một số phương thức hữu ích, chẳng hạn như copy cho phép bạn tạo màu mới theo nhiều giá trị alpha/red/green/blue.

Màu giao diện

Một phương pháp linh hoạt hơn là truy xuất màu từ giao diện:

Surface(color = MaterialTheme.colors.primary)

Ở đây, chúng ta đang sử dụng MaterialTheme object có thuộc tính colors trả về Colors được thiết lập trong thành phần kết hợp MaterialTheme. Tức là chúng ta có thể hỗ trợ nhiều giao diện bằng cách cung cấp các nhiều nhóm màu cho giao diện và không cần đụng đến mã xử lý ứng dụng. Ví dụ như AppBar sử dụng màu primary và nền màn hình là surface; việc thay đổi màu giao diện được phản ánh trong các thành phần kết hợp sau:

b0b0ca02b52453a7.png

253ab041d7ea904e.png

Vì mỗi màu trong giao diện của chúng ta là các phiên bản Color, nên chúng ta cũng có thể dễ dàng nhận dạng màu bằng cách sử dụng phương thức copy:

val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)

Ở đây, chúng ta đang tạo một bản sao màu onSurface nhưng có độ mờ 10%. Phương pháp này đảm bảo các màu sắc hoạt động tuỳ theo giao diện, thay vì các màu tĩnh được mã hoá cứng.

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: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(color),
  ...

TopAppBar(
  backgroundColor: Color = MaterialTheme.colors.primarySurface,
  contentColor: Color = 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", tức là 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ụ như màu Text hoặc sắc thái Icon. Phương thức contentColorFor truy xuất "màu trên" thích hợp cho bất kỳ màu giao diện nào, chẳng hạn như nếu bạn đặt nền primary thì màu này sẽ trả về onPrimary làm màu nội dung. Nếu thiết lập màu nền không theo giao diện thì bạn nên tự cung cấp màu nội dung hợp lý.

Surface(color = MaterialTheme.colors.primary) {
  Text(...) // default text color is 'onPrimary'
}
Surface(color = MaterialTheme.colors.error) {
  Icon(...) // default tint is 'onError'
}

Bạn có thể sử dụng LocalContentColor CompositionLocal để truy xuất màu tương phản với nền hiện tại:

BottomNavigationItem(
  unselectedContentColor = LocalContentColor.current ...

Khi thiết lập màu của bất cứ thành phần nào, hãy sử dụng Surface để thực hiện việc này vì nó thiết lập màu nội dung phù hợp với giá trị CompositionLocal. Hãy cảnh giác với các lệnh gọi Modifier.background trực tiếp không thiết lập màu nội dung thích hợp.

-Row(Modifier.background(MaterialTheme.colors.primary)) {
+Surface(color = MaterialTheme.colors.primary) {
+  Row(
...

Hiện tại, các thành phần Header của chúng tôi luôn có nền Color.LightGray. Giao diện này có vẻ ổn trong giao diện sáng, nhưng sẽ có độ tương phản cao so với nền trong giao diện tối. Họ cũng không chỉ định màu văn bản cụ thể, vì vậy hãy kế thừa màu nội dung hiện tại có thể không tương phản với màu nền:

7329ac6ead5097eb.png

Hãy khắc phục vấn đề này. Ở thành phần kết hợp Header trong Home.kt, hãy xoá phím bổ trợ background để chỉ định màu được được cố định giá trị trong mã. Thay vào đó, hãy đặt Text trong một Surface bằng màu bắt nguồn của giao diện, và chỉ định nội dung cần có màu primary:

+ Surface(
+   color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
+   contentColor = MaterialTheme.colors.primary,
+   modifier = modifier
+ ) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
-     .background(Color.LightGray)
      .padding(horizontal = 16.dp, vertical = 8.dp)
  )
+ }

Độ đậm nhạt của nội dung

Thường thì chúng ta muốn nhấn mạnh hoặc giảm nhấn mạnh nội dung để truyền đạt mức độ quan trọng và mang lại sự phân cấp về mặt thị giác. Material Design đề xuất sử dụng nhiều mức độ mờ để 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 con có thể sử dụng giá trị này, ví dụ như TextIcon theo mặc định, sử dụng tổ hợp LocalContentColor được điều chỉnh để sử dụng LocalContentAlpha. Material chỉ định một số giá trị alpha chuẩn (high, medium, disabled) được đối tượng ContentAlpha mô hình hoá. Lưu ý: MaterialTheme mặc định là LocalContentAlpha trong ContentAlpha.high.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting a different content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(...)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(...)
    Text(...)
}

Điều này giúp bạn truyền tải dễ dàng và nhất quán tầm quan trọng của các thành phần.

Chúng tôi sẽ sử dụng nội dung alpha để làm rõ hệ thống phân cấp thông tin của bài đăng nổi bật. Trong Home.kt, ở thành phần kết hợp PostMetadata, hãy làm nổi bật siêu dữ liệu medium:

+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
  Text(
    text = text,
    modifier = modifier
  )
+ }

103ff62c71744935.png

Giao diện tối

Như chúng ta đã thấy, để triển khai giao diện tối trong Compose, bạn chỉ cần cung cấp các bộ màu và màu truy vấn khác nhau thông qua giao diện. Dưới đây là một số trường hợp ngoại lệ mà bạn cần lưu ý:

Bạn có thể kiểm tra xem mình có đang chạy trong giao diện sáng hay không:

val isLightTheme = MaterialTheme.colors.isLight

Giá trị này được đặt bởi các hàm tạo lightColors hoặc DarkColors.

Về chất liệu, ở giao diện tối, các bề mặt có độ nâng cao hơn sẽ nhận được lớp phủ độ nâng (nền đã được làm sáng). Việc này được triển khai tự động khi sử dụng bảng màu tối:

Surface(
  elevation = 2.dp,
  color = MaterialTheme.colors.surface, // color will be adjusted for elevation
  ...

Chúng ta có thể thấy hành vi tự động này trong ứng dụng ở cả hai thành phần TopAppBarCard đang sử dụng; chúng có độ nâng 4 dp và 1 dp theo mặc định, vậy nên nền của chúng được tự động làm sáng trong giao diện tối để truyền tải tốt hơn độ nâng này:

cb8c617b8c151820.png

Material Design đề xuất tránh các khu vực lớn có màu sắc tươi sáng trong giao diện tối. Một mẫu phổ biến là tô màu một vùng chứa primary trong giao diện sáng và màu surface trong giao diện tối; nhiều thành phần sử dụng chiến lược này theo mặc định, chẳng hạn như Thanh ứng dụngĐiều hướng dưới cùng. Để triển khai việc này dễ dàng hơn, Colors cung cấp màu primarySurface cung cấp chính xác hành vi này và các thành phần này sử dụng theo mặc định.

Ứng dụng của chúng ta đang đặt Thanh ứng dụng thành màu primary, chúng ta có thể làm theo hướng dẫn này bằng cách chuyển tham số sang primarySurface hoặc chỉ cần loại bỏ tham số này khỏi tham số mặc định. Trong thành phần kết hợp AppBar, hãy thay đổi tham số backgroundColor của TopAppBar:

@Composable
private fun AppBar() {
  TopAppBar(
    ...
-   backgroundColor = MaterialTheme.colors.primary
+   backgroundColor = MaterialTheme.colors.primarySurface
  )
}

6. Làm việc với văn bản

Khi làm việc với văn bản, chúng ta sử dụng thành phần kết hợp Text để hiển thị văn bản, TextFieldOutlinedTextField để nhập văn bản và TextStyle để áp dụng một kiểu duy nhất cho văn bản. Chúng ta có thể dùng AnnotatedString để áp dụng nhiều kiểu cho văn bản.

Như chúng ta đã thấy với màu sắc, các thành phần Material hiển thị văn bản sẽ có các phần tuỳ chỉnh kiểu chữ theo giao diện:

Button(...) {
  Text("This text will use MaterialTheme.typography.button style by default")
}

Việc này có thể phức tạp hơn một chút so với việc sử dụng các tham số mặc định như đã thấy với màu sắc. Điều này là do các thành phần không có xu hướng tự hiển thị văn bản, mà thay vào đó cung cấp "API ô trống" (slot API) giúp bạn truyền thành phần kết hợp Text. Vậy các thành phần thiết lập kiểu chữ theo giao diện như thế nào? Trong trường hợp này, chúng sử dụng thành phần kết hợp ProvideTextStyle (chính thành phần này sử dụng CompositionLocal) để đặt TextStyle hiện tại. Theo mặc định, thành phần kết hợp Text để truy vấn kiểu "hiện tại" này nếu bạn không cung cấp tham số textStyle cụ thể.

Ví dụ: từ các lớp ButtonText của Compose:

@Composable
fun Button(
    // many other parameters
    content: @Composable RowScope.() -> Unit
) {
  ...
  ProvideTextStyle(MaterialTheme.typography.button) { //set the "current" text style
    ...
    content()
  }
}

@Composable
fun Text(
    // many, many parameters
    style: TextStyle = LocalTextStyle.current // get the value set by ProvideTextStyle
) { ...

Kiểu văn bản của giao diện

Giống như với màu sắc, tốt nhất là bạn nên truy xuất TextStyle trên giao diện hiện tại. Bạn nên sử dụng một tập hợp nhỏ các kiểu nhất quán và giúp chúng dễ duy trì hơn. MaterialTheme.typography truy xuất phiên bản Typography đã đặt trong thành phần kết hợp MaterialTheme, cho phép bạn sử dụng các kiểu đã xác định sau:

Text(
  style = MaterialTheme.typography.subtitle2
)

Nếu cần tuỳ chỉnh TextStyle thì bạn có thể copy nó rồi ghi đè các thuộc tính (chỉ là data class) hoặc thành phần kết hợp Text chấp nhận một số tham số định kiểu sẽ được phủ lên TextStyle bất kỳ:

Text(
  text = "Hello World",
  style = MaterialTheme.typography.body1.copy(
    background = MaterialTheme.colors.secondary
  )
)
Text(
  text = "Hello World",
  style = MaterialTheme.typography.subtitle2,
  fontSize = 22.sp // explicit size overrides the size in the style
)

Nhiều vị trí trong ứng dụng của chúng tôi tự động áp dụng các giao diện TextStyle, chẳng hạn như kiểu TopAppBar, title của nó là h6ListItem tạo kiểu cho văn bản chính và văn bản phụ thành subtitle1body2 tương ứng.

Hãy áp dụng kiểu phông chữ giao diện cho phần còn lại của ứng dụng. Thiết lập Header để sử dụng subtitle2, thiết lập văn bản trong FeaturedPost để sử dụng h6 cho tiêu đề và thiết lập body2 cho tác giả cũng như siêu dữ liệu:

@Composable
fun Header(...) {
  ...
  Text(
    text = text,
+   style = MaterialTheme.typography.subtitle2

45dbf11d6c1013a0.png

Nhiều kiểu

Nếu cần áp dụng nhiều kiểu cho một số văn bản, bạn có thể dùng lớp AnnotatedString để áp dụng mã đánh dấu, thêm SpanStyle vào một loạt văn bản. Bạn có thể thêm các yếu tố này một cách linh động hoặc sử dụng cú pháp DSL để tạo nội dung:

val text = buildAnnotatedString {
  append("This is some unstyled text\n")
  withStyle(SpanStyle(color = Color.Red)) {
    append("Red text\n")
  }
  withStyle(SpanStyle(fontSize = 24.sp)) {
    append("Large text")
  }
}

Hãy cùng định kiểu cho các thẻ mô tả từng bài đăng trong ứng dụng. Hiện tại, chúng sử dụng cùng một kiểu văn bản với phần còn lại của siêu dữ liệu; chúng ta sẽ sử dụng kiểu văn bản overline và một màu nền để phân biệt chúng. Trong thành phần kết hợp PostMetadata:

+ val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
+   background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
+ )
post.tags.forEachIndexed { index, tag ->
  ...
+ withStyle(tagStyle) {
    append(" ${tag.toUpperCase()} ")
+ }
}

3f504aaa0a94599a.png

7. Làm việc với hình dạng

Giống như màu sắc và kiểu chữ, việc thiết lập giao diện hình dạng sẽ được phản ánh trong các thành phần Material, ví dụ như Button sẽ chọn hình dạng cho các thành phần nhỏ:

@Composable
fun Button( ...
  shape: Shape = MaterialTheme.shapes.small
) {

Giống như màu sắc, thành phần Material sử dụng các tham số mặc định để dễ dàng kiểm tra danh mục hình dạng mà thành phần sẽ sử dụng hoặc để cung cấp giải pháp thay thế. Để có liên kết đầy đủ các thành phần tạo danh mục, vui lòng xem tài liệu này.

Lưu ý là một số thành phần sử dụng hình dạng giao diện được sửa đổi cho phù hợp với ngữ cảnh của chúng. Ví dụ: theo mặc định, TextField sử dụng giao diện hình dạng nhỏ, nhưng áp dụng kích thước góc bằng 0 cho các góc dưới cùng:

@Composable
fun FilledTextField(
  // other parameters
  shape: Shape = MaterialTheme.shapes.small.copy(
    bottomStart = ZeroCornerSize, // overrides small theme style
    bottomEnd = ZeroCornerSize // overrides small theme style
  )
) {

1f5fa6cf1355e7a6.png

Hình dạng giao diện

Tất nhiên, bạn có thể tự sử dụng hình dạng khi tạo các thành phần riêng bằng cách sử dụng thành phần kết hợp hoặc Modifier chấp nhận các hình dạng, ví dụ: Surface, Modifier.clip, Modifier.background, Modifier.border, v.v.

@Composable
fun UserProfile(
  ...
  shape: Shape = MaterialTheme.shapes.medium
) {
  Surface(shape = shape) {
    ...
  }
}

Hãy thêm giao diện hình dạng vào hình ảnh xuất hiện trong PostItem; chúng ta sẽ áp dụng hình dạng small của giao diện cho clip Modifier để cắt góc trên cùng bên trái:

@Composable
fun PostItem(...) {
  ...
  Image(
    painter = painterResource(post.imageThumbId),
+   modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
  )

2f989c7c1b8d9e63.png

8. Thành phần "Kiểu"

Compose không cung cấp cách rõ ràng để trích xuất kiểu của một thành phần như kiểu Android View hoặc kiểu css. Vì tất cả các thành phần Compose đều được biên soạn trong Kotlin, nên có nhiều cách khác để đạt được cùng một mục tiêu đó. Thay vào đó, hãy tạo thư viện riêng gồm các thành phần được tuỳ chỉnh và sử dụng các thành phần đó trong ứng dụng.

Chúng ta đã thực hiện việc này trong ứng dụng của mình:

@Composable
fun Header(
  text: String,
  modifier: Modifier = Modifier
) {
  Surface(
    color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
    contentColor = MaterialTheme.colors.primary,
    modifier = modifier.semantics { heading() }
  ) {
    Text(
      text = text,
      style = MaterialTheme.typography.subtitle2,
      modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
  }
}

Thành phần kết hợp Header về cơ bản là một Text được tạo kiểu mà chúng ta có thể sử dụng trên ứng dụng đó.

Chúng ta nhận thấy tất cả các thành phần được xây dựng từ các khối bản dựng cấp thấp hơn, nên bạn có thể sử dụng cùng khối bản dựng này để tuỳ chỉnh các thành phần của Material. Ví dụ như chúng ta thấy Button sử dụng thành phần kết hợp ProvideTextStyle để đặt kiểu văn bản mặc định cho nội dung được truyền tới. Bạn có thể sử dụng cùng một cơ chế chính xác để đặt kiểu văn bản của riêng mình:

@Composable
fun LoginButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonConstants.defaultButtonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier
    ) {
        ProvideTextStyle(...) { // set our own text style
            content()
        }
    }
}

Ở ví dụ này, chúng ta tạo "kiểu" LoginButton bằng cách gói lớp Button tiêu chuẩn cũng như chỉ định một số thuộc tính cụ thể như backgroundColor và kiểu văn bản khác.

Ngoài ra, không có khái niệm nào về định kiểu mặc định, tức là cách tuỳ chỉnh giao diện mặc định của một loại thành phần. Xin nhắc lại, bạn có thể thực hiện được điều này bằng cách tạo thành phần riêng có thể gói và tuỳ chỉnh thành phần thư viện. Ví dụ: nếu bạn muốn tuỳ chỉnh hình dạng của tất cả các Button trong toàn bộ ứng dụng nhưng không muốn thay đổi giao diện hình dạng nhỏ, thì điều này sẽ ảnh hưởng đến các thành phần khác (không phải Button). Để làm việc này, hãy tạo thành phần kết hợp riêng rồi sử dụng xuyên suốt:

@Composable
fun AcmeButton(
  // expose Button params consumers should be able to change
) {
  val acmeButtonShape: Shape = ...
  Button(
    shape = acmeButtonShape,
    // other params
  )
}

9. Xin chúc mừng

Xin chúc mừng, bạn đã hoàn tất thành công lớp học lập trình này và tạo kiểu cho ứng dụng Jetpack Compose!

Bạn đã triển khai một giao diện Material, tuỳ chỉnh màu sắc, kiểu chữ và hình dạng dùng trong ứng dụng để thể hiện thương hiệu của mình và tăng cường tính nhất quán. Bạn đã thêm được hỗ trợ cho cả giao diện sáng lẫn tối.

Nội dung tiếp theo là gì?

Hãy tham khảo các lớp học lập trình khác trên Lộ trình học Compose.

Tài liệu đọc thêm

Ứng dụng mẫu

  • Ứng dụng Owl thể hiện nhiều chủ đề
  • Jetcaster minh hoạ chủ đề động
  • Jetsnack minh hoạ cách triển khai một hệ thống thiết kế tuỳ chỉnh

Tài liệu tham khảo