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

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ơ bản của ứng dụng thành những dòng mã hoạt động ổn định.

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à Bottom Navigation (Đ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 ô trống (slot API).
  • Cách tạo bố cục cho nhiều cấu hình màn hình.

Bạn cần có

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. Ứng dụng này có 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 chứa các bài tập thể dục. Giao diện của ứng dụng:

Phiên bản dọc của ứng dụng

Phiên bản ngang của ứng dụng

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ã nguồn

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

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

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

Hãy xem đoạn mã vừa tải

Đoạn 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.

Bạn bê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. Lập kế hoạch trước khi bắt đầu

Chúng ta sẽ bắt đầu bằng cách triển khai thiết kế dọc của ứng dụng – hãy xem xét kỹ hơn:

thiết kế dọc

Khi được yêu cầu triển khai thiết kế, bạn nên bắt đầu bằng cách tìm hiểu rõ cấu trúc của thiết kế đó. Đừng 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.

bảng chi tiết thiết kế ứng dụng

Xem chi tiết, nội dung màn hình chứa 3 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".

bảng chi tiết thiết kế ứng dụng

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:

  • Phần tử "điều chỉnh cơ thể" xuất hiện trong một hàng có thể cuộn theo chiều ngang.

phần tử điều chỉnh cơ thể

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

thẻ bộ sưu tập yêu thích

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 này, ứ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 thiết kế:

thanh tìm kiếm

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ế một cách hoàn hảo về mặt điểm ảnh. 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 sử dụng công cụ thiết kế hoặc chia sẻ loại thiết kế đường viền màu đỏ. Trong trường hợp này, nhà thiết kế của chúng tôi chuyển giao thiết kế đường viền màu đỏ mà bạn có thể dùng để đọc mọi giá trị kích thước. 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.

đường viền đỏ của thanh tìm kiếm

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.material3.TextField

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

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

  • Bạn đã mã hoá 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:

bản xem trước thanh tìm kiếm

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 đối tượng 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 hoạt động tương tác cấp cao, như làm cho một thành phần có thể nhấp, cuộn, kéo hoặc thu phóng được.

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 đối tượng sửa đổi phù hợp, bạn có thể xem qua danh sách đối tượng 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, thành phần kết hợp này có thể lớn hơn, chẳng hạn như khi người dùng phóng to cỡ 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. Đối tượng 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.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.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 đối tượng sửa đổi ảnh hưởng đến chiều rộng, còn đối tượng khác lại ảnh hưởng đến chiều cao, nên thứ tự của những đối tượng 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. Hãy 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 thiết lập giá trị tham số. Dưới đây là tham chiếu về thiết kế:

thanh tìm kiếm

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ể thiết lập Icon. Trong trường hợp này, bạn nên thiết lập biểu tượng Search. Hãy nhớ sử dụng đúng lệnh nhập Icon của Compose.
  • Bạn có thể dùng TextFieldDefaults.textFieldColors để ghi đè các màu cụ thể. Đặt focusedContainerColorunfocusedContainerColor của trường văn bản thành màu surface của MaterialTheme.
  • 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.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.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.colors(
           unfocusedContainerColor = MaterialTheme.colorScheme.surface,
           focusedContainerColor = MaterialTheme.colorScheme.surface
       ),
       placeholder = {
           Text(stringResource(R.string.placeholder_search))
       },
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

thanh tìm kiếm

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

Ở bước này, bạn đã biết cách sử dụng các tham số và đối tượng 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ả thành phần kết hợp do thư viện Compose và Material cung cấp, cũng như 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ố để tuỳ 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ế đó:

thành phần điều chỉnh cơ thể

đường viền đỏ điều chỉnh cơ thể

Thiết kế đường viền đỏ hiện cũng chứa các khoảng cách theo hướng đườ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ữ bodyMedium.

Để 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 cố định giá trị trong mã. 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 AlignYourBodyElement và biến chúng thành động.

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

bản xem trước thành phần điều chỉnh cơ thể

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.

Đối tượng sửa đổi size điều chỉnh thành phần kết hợp cho phù hợp với một kích thước cụ thể, tương tự như đối tượng 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 đó.

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

Hiện tại, thiết kế của bạn trong Bản xem trước sẽ có dạng như sau:

bản xem trước thành phần điều chỉnh cơ thể

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à:

bản xem trước nội dung điều chỉnh cơ thể

Trong trường hợp này, loại ảnh cắt chính là loại cần sử dụng. Sau khi áp dụng đối tượng 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:

bản xem trước thành phần điều chỉnh cơ thể

Bước tiếp theo, hãy căn chỉnh văn bản theo chiều ngang bằng cách đặt 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, hãy thiết lập chỉ số 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 tuỳ chọn tương tự như các tuỳ 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 này. 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à truyền một số dữ liệu được cố định giá trị trong mã.
  • Cập nhật văn bản để dùng kiểu chữ bodyMedium.
  • Cập nhật khoảng cách đường cơ sở của phần tử văn bản theo biểu đồ.

đường viền đỏ điều chỉnh cơ thể

Sau 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
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale

@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),
           modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
           style = MaterialTheme.typography.bodyMedium
       )
   }
}

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

Hãy xem AlignYourBodyElement trong tab Thiết kế.

bản xem trước thành phần điều chỉnh cơ thể

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 đỏ:

thẻ bộ sưu tập yêu thích

đường viền đỏ của thẻ bộ sưu tập yêu thích

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 rằng văn bản sẽ là titleMedium.

Vùng chứa này dùng surfaceVariant làm màu nền và 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. Chúng ta chỉ định các thông tin này cho thẻ bộ sưu tập yêu thích bằng cách 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.material3.Surface

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

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

bản xem trước bộ sưu tập yêu thích

Tiếp theo, hãy áp dụng các bài học rút ra trong bước trước.

  • Đặt chiều rộng của Row và căn chỉnh phần tử con theo chiều dọc.
  • Đặt kích thước hình ảnh theo biểu đồ và cắt ảnh trong vùng chứa của hình ảnh đó.

đường viền đỏ của bộ sưu tập yêu thích

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.medium,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

Bản xem trước hiện sẽ có dạng như sau:

bản xem trước bộ sưu tập yêu thích

Để 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 cho hàm có khả năng kết hợp dưới dạng đối số.
  • Cập nhật màu sắc thành surfaceVariant.
  • Cập nhật văn bản để dùng kiểu chữ titleMedium.
  • 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.medium,
       color = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(drawable),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(
               text = stringResource(text),
               style = MaterialTheme.typography.titleMedium,
               modifier = Modifier.padding(horizontal = 16.dp)
           )
       }
   }
}

//..

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

Xem Bản xem trước của FavoriteCollectionCardPreview.

bản xem trước bộ sưu tập yêu thích

7. Hàng "căn chỉnh cơ thể" – Sắp xếp

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

Bắt đầu từ hàng có thể cuộn là "Điều chỉnh cơ thể".

hàng có thể cuộn "điều chỉnh cơ thể"

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

đường viền đỏ điều chỉnh cơ thể

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ỉ cho thấy 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:

bản xem trước thành phần điều chỉnh cơ thể

Các khoảng 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 những cách sắp xếp sau:

sắp xếp hàng

Và với Column:

sắp xếp cột

Ngoài những cách sắp xếp này, bạn cũng có thể dùng phương thức Arrangement.spacedBy() để thêm một 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 khoảng cách 8 dp 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ẽ có dạng như sau:

bản xem trước thành phần điều chỉnh cơ thể

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 một đối tượng 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 và xem hành vi của nó bằng cách sử dụng bản xem trước tương tác:

đường viền đỏ điều chỉnh cơ thể

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 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 một tham số cho LazyRow có tên là contentPadding và đặt tham số đó thành 16.dp.

import androidx.compose.foundation.layout.PaddingValues

@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)
       }
   }
}

Dùng thử bản xem trước tương tác để xem khoảng đệm tạo ra sự khác biệt như thế nào.

hàng có thể cuộn "điều chỉnh cơ thể"

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:

cuộn bộ sưu tập yêu thích

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 LazyRow rồi cho phép mỗi mục giữ một Column có 2 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, việc này cũng chưa cung cấp cho bạn kết quả chính xác:

bản xem trước bộ sưu tập yêu thích

Lưới chiếm nhiều không gian hơn thành phần chính, 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 để

  • Lưới có contentPadding ngang là 16 dp.
  • Cách sắp xếp theo chiều ngang và chiều dọc có khoảng cách là 16 dp.
  • Chiều cao của lưới là 168 dp.
  • Đối tượng sửa đổi của FavoriteCollectionCard chỉ định chiều cao là 80 dp.

Mã hoàn thiệ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(16.dp),
       verticalArrangement = Arrangement.spacedBy(16.dp),
       modifier = modifier.height(168.dp)
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text, Modifier.height(80.dp))
       }
   }
}

Bản xem trước sẽ có dạng như sau:

bản xem trước bộ sưu tập yêu thích

9. Phần trang chủ - API dạng ô trống (Slot APIs)

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 phần đều có tiêu đề và một số nội dung thay đổi tuỳ theo phần đó. Dưới đây là thiết kế của đường viền đỏ mà chúng tôi muốn triển khai:

đường viền đỏ của phần trang chủ

Như bạn có thể thấy, mỗi phần có một tiêu đề và một ô trống. 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 đề đó. Ô trống này có thể được tự động điền nhiều nội dung, tuỳ 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. Phần này sẽ giúp bạn hiểu bố cục theo ô trống là gì và cách bạn có thể sử dụng API ô trống (slot API) để 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 = 0xFFF5F0EE)
@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 trailing lambda (lambda theo sau) để lấp đầy ô trống nội dung. Khi một thành phần kết hợp cung cấp nhiều ô trống để đ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:

bản xem trước phần trang chủ

Thành phần kết hợp Text (Văn bản) cần thêm một vài thông tin để khớp với thiết kế.

đường viền đỏ của phần trang chủ

Hãy cập nhật để:

  • Hệ thống sử dụng kiểu chữ titleMedium.
  • Khoảng cách giữa đường cơ sở của văn bản và phần trên cùng là 40 dp.
  • Khoảng cách giữa đường cơ sở và phần dưới cùng của phần tử là 16 dp.
  • Khoảng đệm ngang là 16 dp.

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

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

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

Giờ thì bạn đã tạo tất cả thành phần riêng biệt, bạn có thể kết hợp chúng thành thiết kế triển khai toàn màn hình.

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

đường viền đỏ của phần trang chủ

Chúng ta chỉ cần đặt thanh tìm kiếm và 2 phần này bên dưới 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ậy nên 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ử 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.layout.Spacer
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(
       modifier
           .verticalScroll(rememberScrollState())
   ) {
       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))
   }
}

Để 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 = 0xFFF5F0EE, heightDp = 180)
@Composable
fun ScreenContentPreview() {
   MySootheTheme { HomeScreen() }
}

cuộn nội dung trên màn hình

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

Bây giờ, sau khi triển khai nội dung trên màn hình, bạn đã 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 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 thanh điều hướng, sau đó đưa vào ứng dụng của bạn.

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

thiết kế thanh điều hướng dưới cùng

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 NavigationBar thuộc thư viện Compose Material. Bên trong thành phần kết hợp NavigationBar, bạn có thể thêm một hoặc nhiều phần tử NavigationBarItem. Sau đó, thư viện Material sẽ tự động tạo kiểu cho các phần tử đó.

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

import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa

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

Đây là giao diện triển khai cơ bản – không có nhiều sự tương phản giữa màu nội dung và màu của thanh điều hướng.

bản xem trước thanh điều hướng dưới cùng

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ố containerColor của thanh điều hướng đó. Bạn có thể dùng màu surfaceVariant trên giao diện Material cho việc này. 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) {
   NavigationBar(
       containerColor = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

Giờ thì thanh điều hướng sẽ có dạng như thế này, hãy chú ý đến cách thanh điều hướng cung cấp độ tương phản cao hơn.

thiết kế thanh điều hướng dưới cùng

12. Ứng dụng MySoothe – Scaffold

Đối với bước 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 MySootheAppPortrait(). Đâ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.material3.Scaffold

@Composable
fun MySootheAppPortrait() {
   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 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ể so sánh hình ảnh này với Bản xem trước triển khai của riêng bạn.

cách triển khai MySoothe

13. Dải điều hướng – Material

Khi tạo bố cục cho các ứng dụng, bạn cũng cần lưu ý đến giao diện của ứng dụng trong nhiều cấu hình, bao gồm cả chế độ ngang trên điện thoại. Đây là thiết kế của ứng dụng ở chế độ ngang, hãy chú ý đến cách thanh điều hướng dưới cùng chuyển thành một dải ở bên trái của nội dung trên màn hình.

thiết kế ngang

Để triển khai việc này, bạn sẽ dùng thành phần kết hợp NavigationRail thuộc thư viện Compose Material và có cách triển khai tương tự như NavigationBar dùng để tạo thanh điều hướng dưới cùng. Bên trong thành phần kết hợp NavigationRail, bạn sẽ thêm các phần tử NavigationRailItem cho Trang chủ và Hồ sơ.

thiết kế thanh điều hướng dưới cùng

Hãy bắt đầu từ cách triển khai cơ bản cho Dải điều hướng.

import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem

@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   NavigationRail(
   ) {
       Column(
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )

           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

bản xem trước dải điều hướng

Bạn nên điều chỉnh một số kiểu.

  • Thêm khoảng đệm 8 dp ở đầu và cuối dải.
  • Cập nhật màu nền của dải điều hướng bằng cách đặt tham số containerColor của dải điều hướng đó bằng màu nền trong Giao diện Material cho việc này. Khi bạn đặ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.
  • Cột phải lấp đầy chiều cao tối đa.
  • Đặt chế độ sắp xếp theo chiều dọc của cột vào chính giữa.
  • Đặt chế độ căn chỉnh ngang của cột vào chính giữa theo chiều ngang.
  • Thêm khoảng đệm 8 dp giữa 2 biểu tượng.

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

import androidx.compose.foundation.layout.fillMaxHeight

@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   NavigationRail(
       modifier = modifier.padding(start = 8.dp, end = 8.dp),
       containerColor = MaterialTheme.colorScheme.background,
   ) {
       Column(
           modifier = modifier.fillMaxHeight(),
           verticalArrangement = Arrangement.Center,
           horizontalAlignment = Alignment.CenterHorizontally
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )
           Spacer(modifier = Modifier.height(8.dp))
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

thiết kế dải điều hướng

Bây giờ, hãy thêm Dải điều hướng vào bố cục ngang.

thiết kế ngang

Đối với phiên bản dọc của ứng dụng, bạn đã dùng một Scaffold. Tuy nhiên, đối với phiên bản ngang, bạn sẽ dùng một Hàng và đặt dải điều hướng và nội dung trên màn hình cạnh nhau.

@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Row {
           SootheNavigationRail()
           HomeScreen()
       }
   }
}

Khi bạn dùng một Scaffold trong phiên bản dọc, việc này cũng giúp bạn đặt màu nội dung thành màu nền. Để đặt màu của Dải điều hướng, hãy gói Hàng đó trong một Surface và đặt thành màu nền.

@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Surface(color = MaterialTheme.colorScheme.background) {
           Row {
               SootheNavigationRail()
               HomeScreen()
           }
       }
   }
}

bản xem trước ở chế độ ngang

14. Ứng dụng MySoothe – Kích thước cửa sổ

Bạn có Bản xem trước ở chế độ ngang trông khá ổn. Tuy nhiên, nếu bạn chạy ứng dụng trên một thiết bị hoặc trình mô phỏng rồi xoay sang cạnh bên, ứng dụng đó sẽ không hiện phiên bản ngang cho bạn. Đó là vì chúng ta cần cho ứng dụng biết thời điểm sẽ hiện cấu hình của ứng dụng. Để thực hiện việc này, hãy dùng hàm calculateWindowSizeClass() để xem điện thoại đang ở cấu hình nào.

biểu đồ kích thước cửa sổ

Có 3 chiều rộng lớp kích thước cửa sổ: Nhỏ gọn, Trung bình và Mở rộng. Khi ở chế độ dọc, ứng dụng có chiều rộng Nhỏ gọn, còn khi ở chế độ ngang, ứng dụng có chiều rộng Mở rộng. Trong phạm vi của lớp học lập trình này, bạn sẽ không làm việc với chiều rộng Trung bình.

Trong Thành phần kết hợp MySootheApp, hãy cập nhật để lấy trong WindowSizeClass của thiết bị. Nếu chiều rộng là nhỏ gọn, hãy truyền phiên bản dọc của ứng dụng. Nếu là chế độ ngang, hãy truyền phiên bản ngang của ứng dụng.

import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
   when (windowSize.widthSizeClass) {
       WindowWidthSizeClass.Compact -> {
           MySootheAppPortrait()
       }
       WindowWidthSizeClass.Expanded -> {
           MySootheAppLandscape()
       }
   }
}

Trong setContent(), hãy tạo một val có tên là windowSizeClass và đặt thành calculateWindowSize() rồi truyền nó vào MySootheApp().

calculateWindowSize() vẫn đang trong quá trình thử nghiệm nên bạn cần chọn sử dụng lớp ExperimentalMaterial3WindowSizeClassApi.

import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

class MainActivity : ComponentActivity() {
   @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           val windowSizeClass = calculateWindowSizeClass(this)
           MySootheApp(windowSizeClass)
       }
   }
}

Bây giờ, hãy chạy ứng dụng trên trình mô phỏng hoặc thiết bị của bạn và quan sát cách màn hình thay đổi khi xoay.

Phiên bản dọc của ứng dụng

Phiên bản ngang của ứng dụng

15. 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 đối tượng sửa đổi, căn chỉnh, sắp xếp, bố cục Lazy, API ô trống, cuộn, các thành phần Material và thiết kế bố cục cụ thể.

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: