Bố cục cơ bản trong Compose

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

1. Giới thiệu

Compose là một bộ công cụ giao diện người dùng giúp bạn dễ dàng triển khai các thiết kế của ứng dụng. Bạn có thể mô tả giao diện người dùng theo cách bạn muốn, và Compose sẽ xử lý việc vẽ giao diện người dùng trên màn hình. Lớp học lập trình này sẽ hướng dẫn bạn cách viết giao diện người dùng trong Compose. Phần này giả định là bạn đã hiểu các khái niệm được học trong lớp học lập trình cơ bản, vì thế hãy đảm bảo bạn đã hoàn thành lớp học lập trình đó trước. Trong lớp học lập trình về Khái niệm cơ bản, bạn đã tìm hiểu cách triển khai bố cục đơn giản bằng cách sử dụng Surfaces, RowsColumns. Bạn cũng đã tăng cường những bố cục này bằng các công cụ sửa đổi như padding, fillMaxWidthsize.

Ở lớp học lập trình này, bạn sẽ triển khai một bố cục thực tế và phức tạp hơn, tìm hiểu về nhiều thành phần có thể kết hợpcông cụ sửa đổi độc đáo trong quá trình triển khai. Sau khi kết thúc lớp học lập trình này, bạn sẽ có thể chuyển đổi thiết kế của ứng dụng cơ bản thành mã đang hoạt động.

Lớp học lập trình này không thêm hành vi thực tế nào vào ứng dụng. Thay vào đó, để tìm hiểu về trạng thái và hoạt động tương tác, hãy hoàn thành lớp học lập trình Trạng thái trong Compose.

Để được hỗ trợ thêm khi tham gia lớp học lập trình này, vui lòng xem nội dung các bước tập lập trình bên dướ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:

  • Cách công cụ sửa đổi giúp bạn bổ sung các thành phần kết hợp.
  • Cách các thành phần bố cục chuẩn như Column và LazyRow định vị thành phần kết hợp con.
  • Cách căn chỉnh và sắp xếp thay đổi vị trí của các thành phần kết hợp con trong thành phần mẹ.
  • Cách các thành phần kết hợp Material như Scaffold và Điều hướng dưới cùng giúp bạn tạo bố cục toàn diện.
  • Cách tạo thành phần kết hợp linh hoạt bằng API khe.

Bạn cần có

  • Đã cài đặt Android Studio Chipmunk trở lên.
  • Kinh nghiệm về cú pháp Kotlin, bao gồm cả lambda.
  • Kinh nghiệm cơ bản về Compose. Hãy hoàn thành lớp học lập trình cơ bản về Jetpack Compose nếu bạn chưa thực hiện trước khi bắt đầu lớp học lập trình này.
  • Kiến thức cơ bản về thành phần kết hợp và công cụ sửa đổi là gì.

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

Trong lớp học lập trình này, bạn sẽ triển khai một thiết kế ứng dụng thực tế dựa trên các bản mô phỏng do nhà thiết kế cung cấp. MySoothe là một ứng dụng bảo vệ sức khỏe liệt kê nhiều cách để cải thiện cơ thể và tâm trí của bạn. Chứa một phần liệt kê các bộ sưu tập yêu thích của bạn và một phần có các bài tập thể dục. Giao diện của ứng dụng:

24ff9efa75f22198.png

2. Thiết lập

Ở bước này, bạn sẽ tải mã chứa chủ đề và một số chế độ thiết lập cơ bản xuống.

Lấy mã

Bạn có thể tìm thấy mã dành cho lớp học lập trình này trong android-compose-codelabs trên kho lưu trữ GitHub. Để sao chép, hãy chạy:

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

Ngoài ra, bạn có thể tải 2 tệp zip xuống:

Hãy xem mã

Mã đã tải xuống chứa mã cho tất cả lớp học lập trình Compose hiện có. Để hoàn tất lớp học lập trình này, hãy mở dự án BasicLayoutsCodelab trong Android Studio.

Nên bắt đầu bằng mã trong nhánh main và làm theo hướng dẫn từng bước của lớp học lập trình theo tốc độ của bạn.

3. Hãy bắt đầu với một kế hoạch

Cùng xem xét kỹ hơn thiết kế:

c31e78e48cc1f336.png

Khi được yêu cầu triển khai thiết kế, bạn nên hiểu rõ cấu trúc của thiết kế đó. Đừng bắt đầu lập trình ngay lập tức mà hãy phân tích chính thiết kế đó. Bạn có thể chia giao diện người dùng này thành nhiều phần có thể sử dụng lại bằng cách nào?

Hãy cùng xem xét thiết kế của chúng tôi nhé. Ở cấp độ trừu tượng cao nhất, chúng ta có thể chia thiết kế này thành hai phần:

  • Nội dung của màn hình.
  • Thanh điều hướng dưới cùng.

9a0f4be94a5a206c.png

Xem chi tiết, nội dung màn hình chứa ba phần phụ:

  • Thanh Tìm kiếm.
  • Một phần có tên là "Căn chỉnh cơ thể".
  • Một phần có tên là "Bộ sưu tập yêu thích".

d9bf2ca5a0939959.png

Bên trong mỗi phần, bạn cũng có thể thấy một số thành phần cấp thấp hơn được sử dụng lại:

  • Thành phần "điều chỉnh cơ thể" hiển thị trong một hàng cuộn theo chiều ngang.

29bed1f813622dc.png

  • Thẻ "bộ sưu tập yêu thích" xuất hiện trong một lưới có thể cuộn theo chiều ngang.

cf1fe8b2d682bfca.png

Sau khi đã phân tích thiết kế, bạn có thể bắt đầu triển khai các thành phần kết hợp cho mọi phần đã xác định trên giao diện người dùng. Bắt đầu với các thành phần kết hợp cấp thấp nhất và tiếp tục kết hợp chúng vào những thành phần kết hợp phức tạp hơn. Khi kết thúc lớp học lập trình, ứng dụng mới sẽ có dạng như thiết kế được cung cấp.

4. Thanh tìm kiếm – Công cụ sửa đổi

Thành phần đầu tiên để chuyển đổi thành thành phần kết hợp là thanh Tìm kiếm. Hãy cùng xem lại việc triển khai :

6b7c2f913d189b9a.png

Nếu chỉ dựa vào ảnh chụp màn hình này, sẽ rất khó để triển khai thiết kế theo cách hoàn hảo cho pixel. Nhìn chung, một nhà thiết kế phải truyền tải nhiều thông tin hơn về thiết kế. Họ có thể cấp cho bạn quyền truy cập vào công cụ thiết kế hoặc chia sẻ cái gọi là thiết kế giảm giá. Ở đây, nhà thiết kế của chúng tôi giao thiết kế màu đỏ mà bạn có thể sử dụng để đọc bất kỳ giá trị kích thước nào. Thiết kế được hiển thị với lớp phủ lưới 8 dp, do đó bạn có thể dễ dàng thấy khoảng cách giữa các phần tử và xung quanh. Ngoài ra, một số khoảng cách được thêm vào rõ ràng để làm rõ một số kích thước nhất định.

6c6854661a89e995.png

Bạn có thể thấy là thanh tìm kiếm phải có chiều cao là 56 pixel không phụ thuộc vào mật độ. Nó cũng phải lấp đầy chiều rộng của thành phần mẹ.

Để triển khai thanh tìm kiếm, hãy dùng thành phần Material có tên là Trường văn bản. Thư viện Compose Material chứa một thành phần kết hợp được gọi là TextField. Đây là quá trình triển khai thành phần Material này.

Hãy bắt đầu bằng cách triển khai TextField cơ bản. Trong cơ sở mã của bạn, mở MainActivity.kt và tìm thành phần kết hợp SearchBar.

Bên trong thành phần kết hợp có tên SearchBar, hãy viết nội dung triển khai TextField cơ bản:

import androidx.compose.material.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
   )
}

Một số điểm cần lưu ý:

  • Bạn đã mã hóa cứng giá trị của trường văn bản và lệnh gọi lại onValueChange không thực hiện bất kỳ chức năng nào. Vì đây là lớp học lập trình tập trung vào bố cục, nên vui lòng bỏ qua mọi vấn đề liên quan đến trạng thái.
  • Hàm có khả năng kết hợp SearchBar chấp nhận tham số modifier và truyền tham số này vào TextField. Đây là phương pháp hay nhất theo các nguyên tắc Compose. Điều này cho phép phương thức gọi sửa đổi giao diện của thành phần kết hợp, nhờ đó giao diện linh hoạt hơn và có thể tái sử dụng. Bạn sẽ tiếp tục sử dụng phương pháp hay nhất này đối với tất cả thành phần kết hợp trong lớp học lập trình này.

Hãy xem bản xem trước của thành phần kết hợp này. Nhớ là bạn có thể dùng chức năng Xem trước trong Android Studio để nhanh chóng lặp lại thành phần kết hợp riêng lẻ. MainActivity.kt chứa bản xem trước tất cả các thành phần kết hợp mà bạn sẽ tạo trong lớp học lập trình này. Trong trường hợp này, phương thức SearchBarPreview sẽ kết xuất thành phần kết hợp SearchBar, với một số nền và khoảng đệm để cung cấp thêm ngữ cảnh. Với cách triển khai bạn vừa thêm vào, ứng dụng sẽ có dạng như sau:

c2e1eec30f36bc72.png

Có một số mục bị thiếu. Trước tiên, hãy xác định kích thước của thành phần kết hợp bằng cách sử dụng công cụ sửa đổi.

Khi viết các thành phần kết hợp, bạn sử dụng công cụ sửa đổi để:

  • Thay đổi kích thước, bố cục, hành vi và giao diện của ứng dụng.
  • Thêm thông tin, như nhãn hỗ trợ tiếp cận.
  • Xử lý dữ liệu do người dùng nhập.
  • Thêm các lượt tương tác cấp cao, như làm cho một thành phần có thể nhấp vào, cuộn được,có thể kéo hoặc thu phóng.

Mỗi thành phần kết hợp bạn gọi đều có một tham số modifier mà bạn có thể thiết lập để điều chỉnh giao diện và hành vi của thành phần kết hợp đó. Khi đặt hệ số sửa đổi, bạn có thể liên kết nhiều phương thức sửa đổi để tạo một phương thức điều chỉnh phức tạp hơn.

Trong trường hợp này, thanh tìm kiếm phải có chiều cao tối thiểu là 56 dp, đồng thời phải lấp đầy chiều rộng của thành phần mẹ. Để tìm công cụ sửa đổi phù hợp, bạn có thể xem qua danh sách công cụ sửa đổi và xem mục Kích thước. Bạn có thể sử dụng hệ số sửa đổi heightIn để biết chiều cao. Việc này đảm bảo thành phần kết hợp có chiều cao tối thiểu cụ thể. Tuy nhiên, phông chữ này có thể lớn hơn khi người dùng phóng to kích thước phông chữ hệ thống của mình. Đối với chiều rộng, bạn có thể sử dụng phím bổ trợ fillMaxWidth. Công cụ sửa đổi này đảm bảo thanh tìm kiếm sử dụng hết không gian ngang của thành phần mẹ.

Cập nhật công cụ sửa đổi để phù hợp với mã bên dưới:

import androidx.compose.material.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

Trong trường hợp này, vì một công cụ sửa đổi ảnh hưởng đến chiều rộng, còn công cụ khác lại ảnh hưởng đến chiều cao, nên thứ tự của những công cụ sửa đổi này không quan trọng.

Bạn cũng phải đặt một vài tham số của TextField. Cố gắng làm cho thành phần kết hợp trông giống như thiết kế bằng cách đặt giá trị tham số. Dưới đây là tham chiếu về thiết kế:

6b7c2f913d189b9a.png

Bạn cần thực hiện các bước sau để cập nhật phương thức triển khai:

  • Thêm biểu tượng tìm kiếm. TextField chứa tham số leadingIcon chấp nhận một thành phần kết hợp khác. Bên trong, bạn có thể đặt Icon. Trong trường hợp này, bạn nên đặt biểu tượng Search. Hãy nhớ sử dụng đúng lệnh nhập Icon của Compose.
  • Đặt màu nền của trường văn bản thành màu surface của MaterialTheme. Bạn có thể dùng TextFieldDefaults.textFieldColors để ghi đè các màu cụ thể.
  • Thêm văn bản phần giữ chỗ "Tìm kiếm" (bạn có thể tìm thấy văn bản này dưới dạng tài nguyên chuỗi R.string.placeholder_search).

Sau khi bạn hoàn tất, thành phần kết hợp sẽ có dạng như sau:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.ui.res.stringResource
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       leadingIcon = {
           Icon(
               imageVector = Icons.Default.Search,
               contentDescription = null
           )
       },
       colors = TextFieldDefaults.textFieldColors(
           backgroundColor = MaterialTheme.colors.surface
       ),
       placeholder = {
           Text(stringResource(R.string.placeholder_search))
       },
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

Lưu ý là:

  • Bạn đã thêm leadingIcon hiển thị biểu tượng tìm kiếm. Biểu tượng này không cần mô tả nội dung, vì phần giữ chỗ của trường văn bản đã mô tả ý nghĩa của trường văn bản. Hãy nhớ là nội dung mô tả thường được dùng cho các mục đích hỗ trợ tiếp cận, và giúp người dùng ứng dụng thể hiện hình ảnh hoặc biểu tượng bằng văn bản.
  • Để điều chỉnh màu nền của trường văn bản, bạn cần đặt thuộc tính colors. Thay vì một tham số riêng cho từng màu, thành phần kết hợp lại chứa một tham số kết hợp. Ở đây, bạn truyền một bản sao của lớp dữ liệu TextFieldDefaults, trong đó bạn chỉ cập nhật các màu sắc khác nhau. Trong trường hợp này, đó chỉ là màu nền.
  • Bạn đặt chiều cao tối thiểu chứ không phải chiều cao cố định. Đây là phương pháp được đề xuất để trường văn bản vẫn có thể tăng kích thước khi người dùng tăng kích thước phông chữ trong phần cài đặt hệ thống.

Ở bước này, bạn đã biết cách sử dụng các tham số và công cụ sửa đổi có thể kết hợp để thay đổi giao diện của thành phần kết hợp. Điều này áp dụng cho cả các thành phần kết hợp do thư viện Compose và Material cung cấp cũng như cho các thành phần kết hợp do bạn tự viết. Bạn phải luôn nghĩ đến việc cung cấp các tham số để tùy chỉnh thành phần kết hợp mà bạn đang viết. Bạn cũng nên thêm thuộc tính modifier để có thể điều chỉnh giao diện của thành phần kết hợp từ bên ngoài.

5. Căn chỉnh cơ thể – Căn chỉnh

Thành phần kết hợp tiếp theo mà bạn sẽ triển khai là phần tử "Căn chỉnh cơ thể". Hãy cùng xem thiết kế của SDK, bao gồm cả thiết kế đường viền đỏ bên cạnh thiết kế đó:

29bed1f813622dc.png 9d11e16a8817686f.png

Thiết kế đường viền đỏ hiện cũng chứa các khoảng cách theo hướng cơ sở. Dưới đây là thông tin mà chúng tôi thu được từ hình ảnh đó:

  • Hình ảnh phải cao 88 dp.
  • Khoảng cách giữa đường cơ sở của văn bản và hình ảnh phải là 24 dp.
  • Khoảng cách giữa đường cơ sở và đáy của phần tử phải là 8 dp.
  • Văn bản này phải có kiểu chữ H3.

Để triển khai thành phần kết hợp này, bạn cần có thành phần kết hợp ImageText. Bạn cần thêm các cột này vào Column để chúng được đặt bên dưới nhau.

Tìm thành phần kết hợp AlignYourBodyElement trong mã và cập nhật nội dung của thành phần đó bằng cách triển khai cơ bản sau:

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.res.painterResource

@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null
       )
       Text(
           text = stringResource(R.string.ab1_inversions)
       )
   }
}

Lưu ý là:

  • Bạn đặt contentDescription của hình ảnh thành rỗng, vì hình ảnh này chỉ mang tính chất trang trí. Văn bản bên dưới hình ảnh mô tả đủ ý nghĩa, vì vậy hình ảnh không cần thêm nội dung mô tả.
  • Bạn đang sử dụng hình ảnh và văn bản được mã hoá cứng. Trong bước tiếp theo, bạn sẽ di chuyển các tham số này để sử dụng các tham số được cung cấp trong thành phần kết hợp AlightYourBodyElement và biến chúng thành động.

Hãy xem trước bản xem trước của thành phần kết hợp này:

b9686f83eb73c542.png

Chúng tôi cần cải thiện một số điểm. Đáng chú ý nhất là hình ảnh quá lớn và không có hình tròn. Bạn có thể điều chỉnh thành phần kết hợp Image bằng các hệ số sửa đổi sizeclip và tham số contentScale.

Công cụ sửa đổi size sẽ điều chỉnh thành phần kết hợp để phù hợp với một kích thước nhất định, tương tự như công cụ sửa đổi fillMaxWidthheightIn mà bạn đã thấy ở bước trước. Công cụ sửa đổi clip hoạt động theo cách khác và điều chỉnh giao diện của thành phần kết hợp. Bạn có thể đặt thuộc tính này thành bất kỳ Shape nào, và nó sẽ cắt nội dung của thành phần kết hợp thành hình dạng đó.

Hình ảnh cũng cần được điều chỉnh theo tỷ lệ chính xác. Để làm việc này, chúng ta có thể dùng tham số contentScale của Image. Có một vài lựa chọn, đáng chú ý nhất trong số đó là:

5f17f07fcd0f1dc.png

Trong trường hợp này, loại ảnh cắt là loại được sử dụng chính xác. Sau khi áp dụng hệ số sửa đổi và tham số, mã của bạn sẽ có dạng như sau:

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(R.string.ab1_inversions)
       )
   }
}

Tệp của bạn hiện sẽ có dạng như sau:

6576ed1e8b1cde30.png

Bước tiếp theo, hãy căn chỉnh văn bản theo chiều ngang bằng cách thiết lập thuộc tính căn chỉnh của Column.

Nhìn chung, để căn chỉnh các thành phần kết hợp bên trong một vùng chứa gốc, bạn hãy đặt độ căn chỉnh của vùng chứa gốc đó. Do đó, thay vì yêu cầu thành phần con tự đặt mình vào vị trí gốc, bạn sẽ yêu cầu cha mẹ cách căn chỉnh vị trí con của mình.

Đối với Column, bạn có thể quyết định cách căn chỉnh phần tử con theo chiều ngang. Có các lựa chọn sau:

  • Bắt đầu
  • Căn giữa theo chiều ngang
  • Kết thúc

Đối với Row, bạn hãy đặt cách căn chỉnh dọc. Các tùy chọn tương tự như các tùy chọn của Column:

  • Trên cùng
  • Căn giữa theo chiều dọc
  • Dưới cùng

Đối với Box, bạn sẽ kết hợp cả căn chỉnh ngang và dọc. Có các lựa chọn sau:

  • TopStart
  • TopCenter
  • TopEnd
  • CenterStart
  • Center
  • CenterEnd
  • BottomStart
  • BottomCenter
  • BottomEnd

Tất cả các phần tử con của vùng chứa sẽ tuân theo cùng một mẫu căn chỉnh. Bạn có thể ghi đè hành vi của một thành phần con bằng cách thêm phím bổ trợ align vào thành phần đó.

Đối với thiết kế này, văn bản phải được căn giữa theo chiều ngang. Để làm việc đó, hãy đặt horizontalAlignment của Column ở giữa theo chiều ngang:

import androidx.compose.ui.Alignment
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       horizontalAlignment = Alignment.CenterHorizontally,
       modifier = modifier
   ) {
       Image(
           //..
       )
       Text(
           //..
       )
   }
}

Sau khi triển khai các phần này, bạn chỉ cần thực hiện một số thay đổi nhỏ để đảm bảo thành phần kết hợp giống với thiết kế. Hãy cố gắng tự triển khai các mã này hoặc tham khảo mã cuối cùng nếu bạn gặp khó khăn. Hãy thử thực hiện các bước sau:

  • Tạo hình ảnh và văn bản động. Truyền chúng dưới dạng đối số vào hàm có khả năng kết hợp. Đừng quên cập nhật Bản xem trước tương ứng và chuyển một số dữ liệu được mã hoá cứng.
  • Cập nhật văn bản để dùng kiểu chữ phù hợp.
  • Cập nhật khoảng cách cơ sở của thành phần văn bản.

Khi đã hoàn tất các bước này, mã của bạn sẽ trông giống như sau:

import androidx.compose.foundation.layout.paddingFromBaseline

@Composable
fun AlignYourBodyElement(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Image(
           painter = painterResource(drawable),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(text),
           style = MaterialTheme.typography.h3,
           modifier = Modifier.paddingFromBaseline(
               top = 24.dp, bottom = 8.dp
           )
       )
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun AlignYourBodyElementPreview() {
   MySootheTheme {
       AlignYourBodyElement(
           text = R.string.ab1_inversions,
           drawable = R.drawable.ab1_inversions,
           modifier = Modifier.padding(8.dp)
       )
   }
}

6. Thẻ bộ sưu tập yêu thích – Material Surface

Thành phần kết hợp tiếp theo cần triển khai tương tự như phần tử "Căn chỉnh nội dung". Dưới đây là thiết kế, bao gồm cả các đường viền đỏ:

71fcfc487ef8c02a.png

f2f4fb696389ba4f.png

Trong trường hợp này, kích thước đầy đủ của thành phần kết hợp được cung cấp. Bạn có thể thấy văn bản đó một lần nữa sẽ là H3.

Vùng chứa này có một số màu nền khác với màu nền của toàn bộ màn hình. Nó cũng có các góc bo tròn. Vì nhà thiết kế không chỉ định màu nên chúng ta có thể giả định là màu sẽ được xác định theo giao diện. Đối với vùng chứa như vậy, chúng ta sẽ sử dụng thành phần kết hợp Surface của Material.

Bạn có thể điều chỉnh Surface cho phù hợp với nhu cầu của mình, bằng cách đặt các tham số và hệ số sửa đổi. Trong trường hợp này, bề mặt phải có các góc tròn. Bạn có thể sử dụng tham số shape cho trường hợp này. Thay vì đặt hình dạng thành Shape như đối với Hình ảnh trong bước trước đó, bạn sẽ sử dụng giá trị lấy từ giao diện Material.

Hãy cùng xem nhé:

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Surface

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.small,
       modifier = modifier
   ) {
       Row {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

Và hãy xem trước Bản xem trước này:

5584e459f9838f8b.png

Tiếp theo, hãy áp dụng các nội dung đã học được trong bước trước. Đặt kích thước và cắt ảnh trong vùng chứa của hình ảnh đó. Đặt chiều rộng của Row và căn chỉnh phần tử con theo chiều dọc. Hãy cố gắng tự triển khai các thay đổi này trước khi xem xét mã giải pháp!

Mã của bạn bây giờ sẽ có dạng như sau:

import androidx.compose.foundation.layout.width

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.small,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(192.dp)
       ) {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(56.dp)
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

Mã hiện sẽ giống như bên dưới:

e0afeb1658a6d82a.png

Để hoàn tất thành phần kết hợp này, hãy triển khai các bước sau:

  • Tạo hình ảnh và văn bản động. Truyền chúng vào các đối số dưới dạng hàm có khả năng kết hợp.
  • Cập nhật văn bản để dùng kiểu chữ phù hợp.
  • Cập nhật khoảng cách giữa hình ảnh và văn bản.

Kết quả cuối cùng sẽ có dạng như sau:

@Composable
fun FavoriteCollectionCard(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.small,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(192.dp)
       ) {
           Image(
               painter = painterResource(drawable),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(56.dp)
           )
           Text(
               text = stringResource(text),
               style = MaterialTheme.typography.h3,
               modifier = Modifier.padding(horizontal = 16.dp)
           )
       }
   }
}

//..

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun FavoriteCollectionCardPreview() {
   MySootheTheme {
       FavoriteCollectionCard(
           text = R.string.fc2_nature_meditations,
           drawable = R.drawable.fc2_nature_meditations,
           modifier = Modifier.padding(8.dp)
       )
   }
}

7. Căn chỉnh hàng nội dung của bạn - Sắp xếp

Sau khi đã tạo các thành phần kết hợp cơ bản hiển thị trên màn hình, bạn có thể bắt đầu tạo các phần khác nhau trên màn hình.

Bắt đầu với hàng có thể cuộn "Căn chỉnh cơ thể".

25089e1f3e5eab4e.gif

Dưới đây là thiết kế của đường viền đỏ cho thành phần này:

9d943fabcb5ae632.png

Hãy nhớ là một khối của lưới đại diện cho 8 dp. Vì vậy, trong thiết kế này có khoảng trống 16 dp trước mục đầu tiên và sau mục cuối cùng trong hàng. Có 8dp khoảng cách giữa mỗi mục.

Trong tính năng Compose, bạn có thể triển khai một hàng có thể cuộn như thế này bằng cách sử dụng thành phần kết hợp LazyRow. Tài liệu về danh sách chứa nhiều thông tin hơn các danh sách Lazy như LazyRowLazyColumn. Đối với lớp học lập trình này, bạn chỉ cần biết rằng LazyRow chỉ hiển thị các thành phần hiển thị trên màn hình, thay vì tất cả các thành phần cùng một lúc, giúp ứng dụng hoạt động hiệu quả.

Bắt đầu với cách triển khai cơ bản của LazyRow này:

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

Như bạn có thể thấy, thành phần con của LazyRow không phải là thành phần kết hợp. Thay vào đó, bạn sẽ sử dụng DSL danh sách Lazy để cung cấp các phương thức như itemitems phát ra các thành phần kết hợp dưới dạng mục danh sách. Đối với mỗi mục trong alignYourBodyData được cung cấp, bạn sẽ phát hành một thành phần kết hợp AlignYourBodyElement đã triển khai trước đó.

Lưu ý cách hiển thị này:

b88f30efe9067f53.png

Các dấu cách mà chúng tôi thấy trong thiết kế đường viền đỏ vẫn bị thiếu. Để triển khai những tính năng này, bạn phải tìm hiểu về cách sắp xếp.

Trong bước trước, bạn đã tìm hiểu về cách căn chỉnh, dùng để căn chỉnh phần tử con của một vùng chứa trên Trục chéo. Đối với Column, trục chéo là trục hoành, còn đối với Row, trục chéo là trục tung.

Tuy nhiên, chúng ta cũng có thể quyết định cách đặt các thành phần kết hợp con trên Trục chính của vùng chứa (ngang với Row, dọc) của Column.

Đối với Row, bạn có thể chọn các cách sắp xếp sau:

c1e6c40e30136af2.gif

Và với Column:

df69881d07b064d0.gif

Ngoài các cách sắp xếp này, bạn cũng có thể sử dụng phương thức Arrangement.spacedBy() để thêm khoảng trống cố định giữa mỗi thành phần kết hợp con.

Trong ví dụ này, phương thức spacedBy là phương thức bạn cần sử dụng, vì bạn muốn đặt 8dp khoảng cách giữa mỗi mục trong LazyRow.

import androidx.compose.foundation.layout.Arrangement

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

Thiết kế hiện sẽ trông giống như bên dưới:

c29a8ee73f218868.png

Bạn cũng cần thêm một số khoảng đệm ở các cạnh của LazyRow. Trong trường hợp này, việc thêm công cụ sửa đổi khoảng đệm đơn giản sẽ không gây ra lỗi. Hãy thử thêm khoảng đệm vào LazyRow để xem lớp này hoạt động như thế nào:

6b3f390040e2b7fd.gif

Như bạn có thể thấy, khi cuộn, mục đầu tiên và mục hiển thị cuối cùng sẽ bị cắt ở cả hai bên màn hình.

Để duy trì cùng một khoảng đệm nhưng vẫn cuộn nội dung trong giới hạn của danh sách gốc mà không cắt đoạn, tất cả danh sách đều phải cung cấp tham số có tên là contentPadding.

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       contentPadding = PaddingValues(horizontal = 16.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

8. Lưới bộ sưu tập yêu thích – Lưới lazy

Phần tiếp theo cần triển khai là phần "Bộ sưu tập yêu thích" trên màn hình. Thành phần kết hợp này cần một lưới thay vì một hàng:

4378867d758590ae.gif

Bạn có thể triển khai phần này tương tự như phần trước, bằng cách tạo một LazyRow và cho phép mỗi mục giữ một Column có hai thực thể FavoriteCollectionCard. Tuy nhiên, trong bước này, bạn sẽ sử dụng LazyHorizontalGrid để cung cấp ánh xạ đẹp hơn từ các mục đến các phần tử lưới.

Bắt đầu với cách triển khai lưới đơn giản bằng hai hàng cố định:

import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       modifier = modifier
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text)
       }
   }
}

Như bạn có thể thấy, chỉ cần thay thế LazyRow từ bước trước bằng LazyHorizontalGrid.

Tuy nhiên, điều này cũng chưa cung cấp cho bạn kết quả chính xác:

e51beb5c00457902.png

Lưới chiếm nhiều không gian hơn cha mẹ, có nghĩa là các thẻ bộ sưu tập yêu thích sẽ bị kéo giãn quá nhiều theo chiều dọc. Điều chỉnh thành phần kết hợp để các ô lưới có kích thước và khoảng cách chính xác.

Bài kiểm thử của bạn sẽ có dạng như sau:

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       contentPadding = PaddingValues(horizontal = 16.dp),
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier.height(120.dp)
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(
               drawable = item.drawable,
               text = item.text,
               modifier = Modifier.height(56.dp)
           )
       }
   }
}

9. Phần trang chủ - API khe

Trong màn hình chính của MySoothe, có nhiều phần tuân theo cùng một mẫu. Mỗi trò chơi đều có tiêu đề và một số nội dung thay đổi tùy theo từng phần. Dưới đây là thiết kế mà chúng tôi muốn triển khai:

2c0bc456d50bb078.png

Như bạn có thể thấy, mỗi phần có một tiêu đề và một khe. Tiêu đề chứa một vài thông tin về khoảng cách và kiểu liên kết với tiêu đề đó. Vị trí này có thể được điền tự động với nhiều nội dung, tùy thuộc vào từng phần.

Để triển khai vùng chứa phần linh hoạt này, bạn hãy sử dụng API khe. Trước khi bạn triển khai phương thức này, vui lòng đọc phần trên trang tài liệu về bố cục dựa trên khe. Điều này sẽ giúp bạn hiểu bố cục dựa trên khe là gì và cách bạn có thể sử dụng API khe để tạo một bố cục như vậy.

Điều chỉnh thành phần kết hợp HomeSection để nhận nội dung tiêu đề và vị trí. Bạn cũng nên điều chỉnh Bản xem trước liên kết để gọi HomeSection này bằng tiêu đề và nội dung "Căn chỉnh cơ thể":

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(stringResource(title))
       content()
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun HomeSectionPreview() {
   MySootheTheme {
       HomeSection(R.string.align_your_body) {
           AlignYourBodyRow()
       }
   }
}

Bạn có thể sử dụng tham số content cho khe của thành phần kết hợp. Bằng cách này, khi sử dụng thành phần kết hợp HomeSection, bạn có thể sử dụng biểu thức lambda tạo vệt để lấp đầy vùng nội dung. Khi một thành phần kết hợp cung cấp nhiều khe để điền vào, bạn có thể đặt cho chúng những cái tên ý nghĩa, đại diện cho hàm của chúng trong vùng chứa có thể kết hợp lớn hơn. Chẳng hạn như TopAppBar của Material cung cấp các khe cho title, navigationIconactions.

Hãy xem cách triển khai phần này:

d824b60e650deeb.png

Thành phần kết hợp Text cần thêm một vài thông tin để khớp với thiết kế. Hãy cập nhật để:

  • Thông tin này được viết hoa toàn bộ (gợi ý: bạn có thể sử dụng phương thức uppercase() của String để viết hoa).
  • Sử dụng kiểu chữ H2.
  • Nút này có khoảng đệm phù hợp với thiết kế của các đường viền đỏ.

Giải pháp cuối cùng của bạn sẽ có dạng như sau:

import java.util.*

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(
           text = stringResource(title).uppercase(Locale.getDefault()),
           style = MaterialTheme.typography.h2,
           modifier = Modifier
               .paddingFromBaseline(top = 40.dp, bottom = 8.dp)
               .padding(horizontal = 16.dp)
       )
       content()
   }
}

10. Màn hình chính - Cuộn

Giờ thì bạn đã tạo tất cả khối xây dựng riêng biệt, bạn có thể kết hợp chúng thành triển khai toàn màn hình.

Dưới đây là thiết kế bạn đang muốn triển khai:

a535e10437e9df31.png

Chúng ta chỉ cần đặt thanh tìm kiếm và hai phần bên dưới lẫn nhau. Bạn cần thêm một số khoảng trống để mọi thứ có thể phù hợp với thiết kế này. Một thành phần kết hợp mà chúng ta chưa từng sử dụng trước đây là Spacer, giúp đặt thêm chỗ trống bên trong Column. Thay vào đó, nếu đặt khoảng đệm của Column, bạn sẽ nhận được hành vi cắt bỏ đã thấy trước đây trong lưới Bộ sưu tập yêu thích.

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(modifier) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

Mặc dù thiết kế vừa với hầu hết các kích thước thiết bị, nhưng bạn cần phải cuộn được theo chiều dọc trong trường hợp thiết bị không đủ cao – ví dụ như ở chế độ ngang. Để làm được điều đó, bạn phải thêm hành vi cuộn.

Như chúng ta đã thấy, các bố cục Lazy, chẳng hạn như LazyRowLazyHorizontalGrid sẽ tự động thêm hành vi cuộn. Tuy nhiên, không phải lúc nào bạn cũng cần bố cục Lazy. Nhìn chung, bạn sẽ sử dụng bố cục Lazy khi có nhiều phần tử trong danh sách hoặc tập dữ liệu lớn để tải, vì vậy, việc phát hành tất cả các mục cùng lúc sẽ dẫn đến chi phí hiệu suất và làm chậm ứng dụng của bạn. Khi một danh sách chỉ có số lượng phần tử bị hạn chế, bạn có thể chọn sử dụng Column hoặc Row đơn giản rồi thêm hành vi cuộn theo cách thủ công. Để thực hiện việc này, bạn cần sử dụng công cụ sửa đổi verticalScroll hoặc horizontalScroll. Phương thức này yêu cầu phải có ScrollState, chứa trạng thái hiện tại của thao tác cuộn dùng để sửa đổi trạng thái cuộn từ bên ngoài. Trong trường hợp này, bạn không muốn sửa đổi trạng thái cuộn, do đó chỉ cần tạo một phiên bản ScrollState cố định bằng cách sử dụng rememberScrollState.

Kết quả cuối cùng sẽ có dạng như sau:

import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(
       modifier
           .verticalScroll(rememberScrollState())
           .padding(vertical = 16.dp)
   ) {
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
   }
}

Để xác minh hành vi cuộn của thành phần kết hợp, hãy giới hạn chiều cao của Bản xem trước và chạy trong bản xem trước tương tác:

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2, heightDp = 180)
@Composable
fun ScreenContentPreview() {
   MySootheTheme { HomeScreen() }
}

11. Thanh điều hướng dưới cùng – Material

Giờ thì khi đã triển khai nội dung trên màn hình, bạn có thể sẵn sàng để thêm trang trí cửa sổ. Trong trường hợp của MySoothe, có một thanh điều hướng dưới cùng cho phép người dùng chuyển đổi giữa các màn hình.

Trước tiên, hãy triển khai thành phần kết hợp điều hướng dưới cùng này, sau đó đưa vào ứng dụng của bạn.

Hãy cùng xem thiết kế nhé:

2f42ad2b882885f8.png

Rất may là bạn không phải tự triển khai toàn bộ thành phần kết hợp này. Bạn có thể dùng thành phần kết hợp BottomNavigation thuộc thư viện Compose Material. Bên trong thành phần kết hợp BottomNavigation, bạn có thể thêm một hoặc nhiều phần tử BottomNavigationItem. Sau đó, thư viện Material sẽ tự động tạo kiểu cho thư viện này.

Bắt đầu với cách triển khai cơ bản của bảng điều hướng dưới cùng này:

import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   BottomNavigation(modifier) {
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

Cách triển khai cơ bản:

5bdb7729d75e1a72.png

Bạn nên điều chỉnh một số kiểu. Trước hết, bạn có thể cập nhật màu nền của thanh điều hướng dưới cùng bằng cách đặt tham số backgroundColor của thanh điều hướng đó. Bạn có thể sử dụng màu nền trên giao diện Material cho việc này. Bằng cách đặt màu nền, màu của biểu tượng và văn bản sẽ tự động điều chỉnh theo màu onBackground của giao diện. Giải pháp cuối cùng của bạn sẽ có dạng như sau:

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   BottomNavigation(
       backgroundColor = MaterialTheme.colors.background,
       modifier = modifier
   ) {
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

12. Ứng dụng MySoothe – Scaffold

Đối với bước cuối cùng này, hãy tạo phương thức triển khai toàn màn hình, bao gồm cả thanh điều hướng dưới cùng. Sử dụng thành phần kết hợp Scaffold của Material. Scaffold cung cấp cho bạn thành phần kết hợp có thể định cấu hình cấp cao nhất đối với các ứng dụng triển khai Material Design. Thành phần này chứa các khe cho nhiều khái niệm Material, trong đó có một thành phần là thanh dưới cùng. Trong thanh dưới cùng này, bạn có thể đặt thành phần kết hợp điều hướng dưới cùng đã tạo ở bước trước đó.

Triển khai thành phần kết hợp MySootheApp. Đây là thành phần kết hợp cấp cao nhất dành cho ứng dụng, do đó bạn nên:

  • Áp dụng giao diện Material MySootheTheme.
  • Thêm biến Scaffold.
  • Đặt thanh dưới cùng thành thành phần kết hợp SootheBottomNavigation.
  • Thiết lập nội dung thành thành phần kết hợp HomeScreen.

Kết quả cuối cùng của bạn sẽ là:

import androidx.compose.material.Scaffold

@Composable
fun MySootheApp() {
   MySootheTheme {
       Scaffold(
           bottomBar = { SootheBottomNavigation() }
       ) { padding ->
           HomeScreen(Modifier.padding(padding))
       }
   }
}

Đã hoàn tất quá trình triển khai! Nếu muốn kiểm tra xem liệu phiên bản của mình có được triển khai theo cách hoàn hảo trong một pixel hay không, bạn có thể tải hình ảnh dưới đây xuống và so sánh với Bản xem trước triển khai của riêng bạn.

24ff9efa75f22198.png

13. 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ìm hiểu thêm về bố cục trong Compose. Thông qua việc triển khai thiết kế thực tế, bạn đã tìm hiểu về các công cụ sửa đổi, căn chỉnh, sắp xếp, bố cục Lazy, API khe, cuộn và các thành phần Material.

Hãy tham khảo các lớp học lập trình khác trên Lộ trình học tập về Compose. Đồng thời xem các mã mẫu.

Tài liệu

Để biết thêm thông tin và hướng dẫn về những chủ đề này, vui lòng xem các tài liệu sau: