Bố cục thích ứng (Adaptive Layouts)

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. Trước khi bắt đầu

Thiết bị Android có nhiều hình dạng, kích thước và kiểu dáng. Bạn nên thiết kế ứng dụng để chạy trên các loại thiết bị khác nhau, từ thiết bị màn hình nhỏ đến thiết bị màn hình lớn hơn. Các nhà phát triển viết ứng dụng sẵn sàng sản xuất (production-ready) có thể hỗ trợ Android Wear, Android AutoAndroid TV. Tuy nhiên, những chủ đề này không thuộc phạm vi của khoá học này. Khi ứng dụng hỗ trợ đa dạng các loại màn hình, bạn có thể cung cấp ứng dụng cho một lượng người dùng rất lớn trên các thiết bị khác nhau.

Ứng dụng của bạn phải có bố cục linh hoạt. Thay vì định nghĩa bố cục với những kích thước cứng nhắc theo một tỷ lệ khung hình và kích thước màn hình giả định nào đó, bạn có thể định nghĩa bố cục có khả năng thích ứng mượt mà với đa dạng kích thước và hướng màn hình. Nguyên tắc này cũng áp dụng khi chạy ứng dụng trên một thiết bị có thể gập lại. Do đó, kích thước màn hình và tỷ lệ khung hình có thể thay đổi khi ứng dụng đang chạy. Kết thúc lớp học lập trình này, bạn sẽ được giới thiệu sơ lược về các thiết bị có thể gập lại.

aecb59fc49fb4abf.png

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

  • Biết cách tải mã xuống Android Studio và chạy mã này.
  • Quen thuộc với các thành phần kiến trúc Android ViewModelLiveData.
  • Kiến thức cơ bản về Thành phần điều hướng.

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

  • Cách thêm SlidingPaneLayout vào ứng dụng.

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

  • Cập nhật ứng dụng Sports (Thể thao) để thích ứng với màn hình lớn.

Bạn cần có

  • Máy tính đã cài đặt Android Studio.
  • Mã khởi động cho ứng dụng Sports.

Tải mã khởi động cho lớp học lập trình này

Lớp học lập trình này sẽ cung cấp mã khởi động, cho phép bạn mở rộng bằng các tính năng được dạy trong lớp học lập trình này. Mã khởi động có thể chứa những đoạn mã đã quen thuộc với bạn từ các lớp học lập trình trước, đồng thời cũng có những đoạn mã còn lạ lẫm mà bạn sẽ tìm hiểu trong các lớp sau.

Để lấy mã cho lớp học lập trình này trên GitHub và mở trong Android Studio, hãy thực hiện các bước sau.

  1. Khởi động Android Studio.
  2. Trên cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Get from VCS (Lấy trên VCS).

61c42d01719e5b6d.png

  1. Trong hộp thoại Get from Version Control (Lấy từ hệ thống Quản lý phiên bản), bạn hãy nhớ chọn Git cho mục Version control(Quản lý phiên bản).

9284cfbe17219bbb.png

  1. Dán URL mã đã cung cấp vào hộp URL.
  2. Bạn có thể thay đổi thư mục mặc định được đề xuất trong Directory (Thư mục).

5ddca7dd0d914255.png

  1. Nhấp vào Clone (Sao chép). Android Studio bắt đầu tìm nạp mã của bạn.
  2. Đợi Android Studio mở khởi động xong.
  3. Chọn mô-đun chính xác cho mã khởi động, ứng dụng hoặc mã giải pháp của lớp học này.

2919fe3e0c79d762.png

  1. Nhấp vào nút Run (Chạy) 8de56cba7583251f.png để tạo và chạy mã của bạn.

2. Xem video tập lập trình (Không bắt buộc)

Nếu bạn muốn xem một trong những người hướng dẫn của khoá học hoàn thành lớp học lập trình, hãy phát video bên dưới.

Bạn nên mở rộng video ra toàn màn hình (có biểu tượng Biểu tượng này hiển thị 4 góc trên một hình vuông được làm nổi bật để biểu thị chế độ toàn màn hình. ở góc dưới bên phải của video). Nhờ đó, bạn có thể nhìn thấy Android Studio và mã rõ ràng hơn.

Bước này là bước không bắt buộc. Bạn cũng có thể bỏ qua video này và bắt đầu tham gia lớp học lập trình ngay.

3. Tổng quan về ứng dụng khởi động

Ứng dụng Sports gồm có hai màn hình. Màn hình đầu tiên hiển thị danh sách các môn thể thao. Người dùng có thể chọn một môn thể thao nào đó, sau đó màn hình thứ hai sẽ hiển thị. Màn hình thứ hai là màn hình chi tiết hiển thị tin tức về môn thể thao đã chọn. Màn hình chi tiết này sẽ hiển thị văn bản giữ chỗ (placeholder text), giúp đơn giản hoá quá trình triển khai.

Tìm hiểu mã khởi động

Mã khởi động bạn tải xuống có màn hình danh sách và bố cục màn hình chi tiết được thiết kế sẵn phù hợp với bạn. Trong lộ trình này, bạn sẽ tập trung vào việc triển khai ứng dụng để thích ứng với màn hình lớn. Bạn sẽ dùng SlidingPaneLayout để khai thác màn hình lớn. Dưới đây là hướng dẫn ngắn gọn về một số tệp để bạn bắt đầu.

fragment_sports_list.xml

  • Mở res/layout/fragment_sports_list.xml trong chế độ xem Design (Thiết kế).
  • Thành phần này chứa bố cục màn hình đầu tiên của ứng dụng, chính là danh sách các môn thể thao.
  • Bố cục này bao gồm một Recyclerview hiển thị danh sách tin tức thể thao.

f50d3e7b41fcb338.png

d9af155f87ddbcdf.png

Sports_list_item.xml

  • Mở res/layout/sports_list_item.xml trong chế độ xem Design (Thiết kế).
  • Thành phần này chứa bố cục của từng môn thể thao trong RecyclerView.
  • Bố cục này bao gồm hình ảnh thu nhỏ của môn thể thao, tiêu đề tin tức và văn bản giữ chỗ cho tin tức thể thao ngắn gọn.

b19fd0e779c1d7c3.png

fragment_sports_news.xml

  • Mở res/layout/fragment_sports_news.xml trong chế độ xem Design (Thiết kế).
  • Thành phần này chứa bố cục màn hình thứ hai của ứng dụng. Màn hình này sẽ hiển thị khi người dùng chọn một môn thể thao trong RecyclerView.
  • Bố cục này bao gồm một biểu ngữ hình ảnh thể thao và văn bản giữ chỗ cho tin tức thể thao.

c2073b1752342d97.png

main_activity.xml and content_main.xml

Hai tệp này định nghĩa bố cục hoạt động chính bằng một mảnh (fragment).

Biểu đồ điều hướng chứa hai đích đến, một cho danh sách các môn thể thao và một cho tin tức thể thao.

Thư mục res/values

Bạn đã làm quen với các tệp tài nguyên trong thư mục này.

  • colors.xml chứa màu giao diện (theme) dùng trong ứng dụng.
  • strings.xml chứa tất cả chuỗi ứng dụng cần.
  • themes.xml chứa tuỳ biến giao diện người dùng cho ứng dụng.

MainActivity.kt

Tệp này chứa mã được tạo mặc định từ mẫu để thiết lập thành phần hiển thị nội dung của hoạt động là main_activity.xml. Phương thức onSupportNavigateUp() sẽ được ghi đè để xử lý điều hướng Lên theo mặc định trong thanh ứng dụng.

model/Sport.kt

Đây là lớp dữ liệu lưu trữ dữ liệu sẽ hiển thị trong mỗi hàng của danh sách các môn thể thao Recyclerview.

data/SportsData.kt

Tệp này chứa một hàm có tên là getSportsData(), trả về một ArrayList được điền sẵn dữ liệu các môn thể thao được mã hoá cứng.

SportsViewModel.kt

Đây là ViewModel dùng chung cho ứng dụng. ViewModel này được chia sẻ cho SportsListFragment, màn hình đầu tiên chứa danh sách các môn thể thao và NewsDetailsFragment, màn hình thứ hai chứa tin tức thể thao chi tiết.

  • Thuộc tính _currentSport có kiểu MutableLiveData,, dùng để lưu trữ môn thể thao hiện tại mà người dùng chọn. Thuộc tính currentSport là thuộc tính hỗ trợ cho _currentSport và hiển thị dưới dạng phiên bản chỉ đọc công khai (public read-only) cho các lớp khác.
  • Thuộc tính _sportsData chứa danh sách dữ liệu thể thao. Tương tự như thuộc tính trước, sportsData là phiên bản chỉ đọc công khai cho thuộc tính này.
  • Khối lệnh khởi tạo init{} khởi chạy các thuộc tính _currentSport_sportsData. _sportsData được khởi chạy để chứa toàn bộ danh sách các môn thể thao trong data/SportsData.kt. _currentSport được khởi chạy để chứa môn thể thao đầu tiên trong danh sách.
  • Hàm updateCurrentSport() lấy một thực thể Sports và cập nhật _currentSport bằng giá trị đã truyền vào.

SportsAdapter.kt

Đây là trình chuyển đổi cho RecyclerView. Trong hàm khởi tạo, trình nghe lượt nhấp được truyền vào. Hầu hết mã trong tệp này là mã nguyên mẫu mà bạn quen thuộc từ các lớp học lập trình trước.

SportsListFragment.kt

Đây là mảnh màn hình đầu tiên dùng để hiển thị danh sách các môn thể thao.

  • Hàm onCreateView() tăng cường XML bố cục fragment_sports_list thông qua đối tượng liên kết.
  • Hàm onViewCreated() thiết lập trình chuyển đổi RecyclerView. Hàm này sẽ cập nhật môn thể thao mà người dùng chọn thành môn thể thao hiện tại trong ViewModelSportsViewModel dùng chung. Hàm này sẽ chuyển đến màn hình chi tiết chứa tin tức thể thao, đồng thời gửi danh sách các môn thể thao đến trình chuyển đổi để hiển thị danh sách này bằng phương thức submitList(List).

NewsDetailsFragment.kt

Đây là màn hình thứ hai trong ứng dụng, dùng để hiển thị văn bản giữ chỗ cho tin tức thể thao.

  • Hàm onCreateView() tăng cường XML bố cục fragment_sports_news thông qua đối tượng liên kết.
  • Hàm onViewCreated() sẽ đính kèm trình quan sát trên thuộc tính của SportsViewModel, currentSport, để tự động cập nhật giao diện người dùng khi dữ liệu thay đổi. Phần tiêu đề, hình ảnh và tin tức thể thao sẽ được cập nhật bên trong trình quan sát này.

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

  1. Tạo và chạy ứng dụng trên trình mô phỏng hoặc thiết bị. Chọn bất kỳ môn thể thao nào từ danh sách. Ứng dụng sẽ chuyển đến màn hình thứ hai chứa văn bản giữ chỗ cho phần tin tức.

4. Mẫu List-Detail

Ứng dụng khởi động hiện tại không thể khai thác tối đa không gian màn hình trên các thiết bị lớn hơn như máy tính bảng. Để giải quyết vấn đề này, bạn sẽ hiển thị giao diện người dùng của ứng dụng theo mẫu List-Detail sẽ được giới thiệu trong lớp học này.

Chạy ứng dụng trên máy tính bảng

Trong nhiệm vụ này, bạn sẽ tạo một trình mô phỏng theo hồ sơ người dùng máy tính bảng. Sau khi trình mô phỏng được tạo, bạn sẽ chạy mã khởi động cho ứng dụng thể thao và quan sát giao diện người dùng.

  1. Trong Android Studio, hãy chuyển đến mục Tools (Công cụ) > AVD Manager (Trình quản lý AVD).
  2. Cửa sổ Android Virtual Device Manager (Trình quản lý thiết bị ảo Android) sẽ xuất hiện. Nhấp vào + Create New Virtual Device... (+ Tạo thiết bị ảo mới...) hiển thị ở phần dưới cùng.
  3. Cửa sổ Virtual Device Configuration (Cấu hình thiết bị ảo) sẽ hiển thị. Tại đây, bạn sẽ định cấu hình phần cứng và hệ điều hành cho trình mô phỏng. Nhấp vào Tablet (Máy tính bảng) trong ngăn bên trái. Chọn Pixel C (Pixel C) hoặc bất kỳ hồ sơ phần cứng nào tương tự trong ngăn ở giữa.

8303f9b3e70321eb.png

  1. Nhấp vào Tiếp theo.
  2. Chọn ảnh hệ thống mới nhất. Tại thời điểm viết lớp học lập trình này, ảnh hệ thống mới nhất là R (API cấp 30).
  3. Nhấp vào Tiếp theo.
  4. Bạn có thể đổi tên thiết bị ảo ngay bây giờ. Thao tác này không bắt buộc.
  5. Nhấp vào Finish (Hoàn tất).
  6. Thao tác này sẽ đưa bạn trở lại cửa sổ Android Virtual Device Manager (Trình quản lý thiết bị ảo Android). Nhấp vào biểu tượng khởi chạy 38752506de85d293.png bên cạnh thiết bị ảo mới tạo.
  7. Trình mô phỏng theo hồ sơ người dùng máy tính bảng sẽ được khởi chạy. Hãy kiên nhẫn, quá trình này có thể mất chút thời gian.
  8. Đóng cửa sổ Android Virtual Device Manager (Trình quản lý thiết bị ảo Android).
  9. Chạy ứng dụng thể thao trên trình mô phỏng mới được tạo.

200e209de7a2f0ad.png

Lưu ý trên các thiết bị lớn, ứng dụng không sử dụng toàn bộ màn hình. Mẫu list-detail hoạt động hiệu quả hơn trên màn hình lớn thay vì danh sách. Mẫu item-detail, còn gọi là mẫu master-detail, hiển thị danh sách các mục ở một bên của bố cục và phần chi tiết sẽ hiển thị bên cạnh khi nhấn vào mục đó. Thông thường, các thành phần hiển thị này chỉ hiển thị trên màn hình lớn, chẳng hạn như máy tính bảng, vì có không gian hiển thị nội dung lớn hơn.

Những hình ảnh sau đây là ví dụ về một mẫu list-detail:

71698910dd129a91.png

Các mẫu list-detail ở trên hiển thị một danh sách các hạng mục ở bên trái và chi tiết của hạng mục được chọn ở bên phải.

Tương tự như vậy, nếu bạn sử dụng mẫu trên trong ứng dụng thể thao, mảnh tin tức sẽ là màn hình chi tiết.

51c9542717d2f875.png

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách triển khai giao diện người dùng list-detail bằng cách sử dụng SlidingPaneLayout.

5. Mẫu SlidingPaneLayout

Giao diện người dùng list-detail cần được xử lý khác nhau tuỳ thuộc vào kích thước màn hình. Trên các màn hình lớn, có nhiều không gian để hiển thị danh sách và các ngăn chi tiết cạnh nhau. Bạn có thể nhấp vào một mục trong danh sách để xem thông tin chi tiết về mục đó trong ngăn chi tiết. Tuy nhiên, trên những màn hình nhỏ, các danh sách này thường rất chật chội. Thay vì hiển thị cả hai ngăn cùng lúc, bạn nên hiển thị từng ngăn. Ban đầu, ngăn danh sách sẽ lấp đầy màn hình. Khi nhấn vào một mục, ngăn chi tiết cho mục đó sẽ hiển thị trên màn hình, thay thế cho ngăn danh sách.

Bạn sẽ tìm hiểu cách sử dụng SlidingPaneLayout để quản lý logic lựa chọn trải nghiệm người dùng phù hợp dựa trên kích thước màn hình hiện tại.

b0a205de3494e95d.gif

Hãy chú ý cách ngăn chi tiết trượt qua ngăn danh sách trên các màn hình nhỏ hơn.

Dưới đây là hình ảnh minh hoạ cách SlidingPaneLayout xuất hiện trên màn hình nhỏ hơn. Quan sát cách ngăn chi tiết chồng lên ngăn danh sách khi chọn một mục từ danh sách. Như vậy, cả hai ngăn đều luôn hiển thị!

e26f94d9579b6121.png

471b0b38d4dfa95a.png

Do đó, SlidingPaneLayout hỗ trợ hiển thị 2 ngăn cạnh nhau trên các thiết bị lớn hơn. Đồng thời, tự động điều chỉnh để chỉ hiển thị 1 ngăn tại một thời điểm trên các thiết bị nhỏ hơn, chẳng hạn như điện thoại.

6. Thêm phần phụ thuộc thư viện

  1. Mở build.gradle (Module: Sports.app).
  2. Trong phần dependencies, hãy thêm phần phụ thuộc sau đây để sử dụng SlidingPaneLayout trong ứng dụng.
dependencies {
...
    implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01"
}

7. Định cấu hình xml cho mảnh danh sách môn thể thao

Trong nhiệm vụ này, bạn chuyển đổi bố cục gốc của fragment_sports_list thành SlidingPaneLayout. Như đã tìm hiểu, SlidingPaneLayout cung cấp một bố cục theo chiều ngang gồm có hai ngăn để sử dụng cho giao diện người dùng ở cấp cao nhất. Bố cục này sử dụng ngăn đầu tiên làm danh sách nội dung hoặc trình duyệt, phụ thuộc vào thành phần hiển thị chi tiết chính để hiển thị nội dung trong ngăn khác.

Trong ứng dụng Sports, ngăn đầu tiên sẽ là một RecyclerView hiển thị danh sách các môn thể thao và ngăn thứ hai hiển thị tin tức thể thao.

Thêm SlidingPaneLayout

  1. Mở fragment_sports_list.xml. Lưu ý rằng bố cục gốc là một FrameLayout.
  2. Thay đổi FrameLayout thành androidx.slidingpanelayout.widget.SlidingPaneLayout.
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".SportsListFragment">

   <androidx.recyclerview.widget.RecyclerView...>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
  1. Thêm thuộc tính android:id vào SlidingPaneLayout, sau đó gán giá trị @+id/sliding_pane_layout cho thuộc tính này.
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   ...
   android:id="@+id/sliding_pane_layout"
   ...>

Thêm ngăn thứ hai vào SlidingPaneLayout

Trong nhiệm vụ này, bạn sẽ thêm thành phần con thứ hai vào SlidingPaneLayout. Thành phần này sẽ hiển thị dưới dạng ngăn nội dung bên phải.

  1. Trong fragment_sports_list.xml, bên dưới RecyclerView, thêm thành phần con thứ hai là androidx.fragment.app.FragmentContainerView.
  2. Thêm các thuộc tính bắt buộc, layout_heightlayout_width vào FragmentContainerView. Gán giá trị match_parent cho các thuộc tính này. Lưu ý rằng bạn sẽ cập nhật lại các giá trị này sau.
<androidx.fragment.app.FragmentContainerView
   android:layout_height="match_parent"
   android:layout_width="match_parent"/>
  1. Thêm thuộc tính android:id vào FragmentContainerView và gán giá trị @+id/detail_container cho thuộc tính này.
android:id="@+id/detail_container"
  1. Thêm NewsDetailsFragment vào FragmentContainerView bằng cách sử dụng thuộc tính android:name.
android:name="com.example.android.sports.NewsDetailsFragment"

Cập nhật thuộc tính layout_width

SlidingPaneLayout sử dụng chiều rộng của hai ngăn để xác định xem các ngăn này có hiển thị cạnh nhau hay không. Ví dụ: nếu kích thước tối thiểu của ngăn danh sách là 300dp và ngăn chi tiết là 400dp thì SlidingPaneLayout sẽ tự động hiển thị hai ngăn cạnh nhau nếu chiều rộng có sẵn tối thiểu cho hai ngăn là 700dp.

Các thành phần hiển thị con sẽ bị chồng lấp nếu tổng chiều rộng của các thành phần này vượt quá chiều rộng có sẵn trong SlidingPaneLayout. Trong tình huống này, các thành phần hiển thị con sẽ mở rộng để lấp đầy chiều rộng có sẵn trong SlidingPaneLayout.

Để xác định chiều rộng của các thành phần hiển thị con, bạn cần có một số thông tin cơ bản về chiều rộng của màn hình thiết bị. Bảng sau đây hiển thị danh sách các điểm ngắt cố định, cho phép thiết kế, phát triển và thử nghiệm các bố cục ứng dụng có thể thay đổi kích thước (resizable). Những điểm ngắt này được lựa chọn cẩn thận để tạo sự cân bằng giữa tính đơn giản và linh hoạt của bố cục nhằm tối ưu hoá ứng dụng trong các trường hợp đặc biệt.

Chiều rộng

Điểm ngắt

Đại diện thiết bị

Chiều rộng thu gọn

< 600dp

99,96% điện thoại ở chế độ dọc

Chiều rộng trung bình

600dp+

93,73% máy tính bảng ở chế độ dọc

Chiều rộng được mở rộng

840dp+

97,22% máy tính bảng trong landscapeLarge ở chế độ ngang khi chưa gập

a247a843310d061a.png

Trong ứng dụng Sports, bạn muốn hiển thị một ngăn duy nhất, tức là hiển thị danh sách các môn thể thao trên điện thoại, dành cho thiết bị có chiều rộng dưới 600dp. Để hiển thị cả hai ngăn trên máy tính bảng, chiều rộng kết hợp phải lớn hơn 840dp. Bạn có thể sử dụng chiều rộng 550dp cho thành phần con đầu tiên, thành phần hiển thị tuần hoàn (recycler view) và 300dp cho thành phần con thứ hai, FragmentContainerView.

  1. Trong fragment_sports_list.xml, thay đổi chiều rộng bố cục của RecyclerView thành 550dp và chiều rộng của FragmentContainerView thành 300dp.
<androidx.recyclerview.widget.RecyclerView
   ...
   android:layout_width="550dp"
   .../>

<androidx.fragment.app.FragmentContainerView
   ...
   android:layout_width="300dp"
   .../>
  1. Chạy ứng dụng trên trình mô phỏng theo hồ sơ người dùng máy tính bảng, sau đó chạy trên trình mô phỏng theo hồ sơ người dùng điện thoại.

ad148a96d7487e66.png

Lưu ý rằng hai ngăn đều được hiển thị trên máy tính bảng. Bạn sẽ sửa chiều rộng của ngăn thứ hai trên máy tính bảng ở bước sau.

  1. Chạy ứng dụng trên trình mô phỏng theo hồ sơ người dùng điện thoại.

a6be6d199d2975ac.png

Thêm layout_weight

Trong nhiệm vụ này, bạn sẽ chỉnh sửa giao diện người dùng trên máy tính bảng và điều chỉnh để ngăn thứ hai chiếm toàn bộ phần không gian còn lại.

SlidingPaneLayout hỗ trợ phân tách phần không gian còn lại sau khi đo lường bằng cách sử dụng tham số bố cục layout_weight trên các thành phần hiển thị con nếu các thành phần này không chồng chéo nhau. Tham số này chỉ áp dụng cho chiều rộng.

  1. Trong fragment_sports_list.xml, thêm layout_weight vào FragmentContainerView và gán giá trị 1 cho thành phần này. Bây giờ, ngăn thứ hai sẽ được mở rộng để lấp đầy phần không gian còn lại sau khi tính toán chiều rộng cho ngăn danh sách.
android:layout_weight="1"
  1. Chạy ứng dụng.

ce3a93fe501ee5dc.png

Xin chúc mừng! Bạn đã thêm thành công SlidingPaneLayout. Nhưng mọi việc vẫn chưa xong. Bạn phải triển khai tính năng điều hướng quay lại và cập nhật ngăn thứ hai khi chọn một môn thể thao từ danh sách. Bạn sẽ triển khai các tính năng này trong nhiệm vụ sau.

8. Hoán đổi ngăn chi tiết

Chạy ứng dụng trên trình mô phỏng theo hồ sơ người dùng máy tính bảng. Chọn một môn thể thao trong danh sách các môn thể thao. Lưu ý rằng ứng dụng sẽ chuyển đến ngăn chi tiết.

8fedee8d4837909.png

Bạn sẽ khắc phục vấn đề này trong nhiệm vụ này. Hiện tại, nội dung trong ngăn kép đang được cập nhật cho môn thể thao được chọn, sau đó ứng dụng sẽ điều hướng đến NewsDetailsFragment.

  1. Trong tệp SportsListFragment, trong hàm onViewCreated(), tìm các dòng sau đây và chuyển tới màn hình chi tiết.
// Navigate to the details screen
val action = SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
  1. Thay thế các dòng trên bằng mã sau:
binding.slidingPaneLayout.openPane()

Gọi phương thức openPane() trên SlidingPaneLayout để hoán đổi ngăn thứ hai với ngăn đầu tiên. Thao tác này không tạo ra bất kỳ hiệu ứng nào nếu cả hai ngăn đều hiển thị, chẳng hạn như trên máy tính bảng.

  1. Chạy ứng dụng trên trình mô phỏng điện thoại và máy tính bảng. Lưu ý rằng nội dung của ngăn kép đang được cập nhật theo đúng yêu cầu.

b0d3c8c263be15f8.png

Trong nhiệm vụ tiếp theo, bạn sẽ thêm vào ứng dụng tính năng điều hướng quay lại tùy chỉnh.

9. Thêm tính năng điều hướng quay lại tùy chỉnh

Đối với các thiết bị nhỏ hơn, ngăn danh sách và ngăn chi tiết chồng chéo lên nhau, bạn phải đảm bảo rằng nút quay lại trên hệ thống sẽ đưa người dùng từ ngăn chi tiết trở lại ngăn danh sách. Bạn có thể thực hiện điều này qua việc cung cấp tính năng điều hướng quay lại tuỳ chỉnh và kết nối OnBackPressedCallback với trạng thái hiện tại của SlidingPaneLayout.

Tính năng điều hướng quay lại

Tính năng điều hướng quay lại là khả năng người dùng di chuyển ngược lại thông qua lịch sử màn hình truy cập trước đó. Tất cả thiết bị Android đều cung cấp một nút Quay lại cho loại điều hướng này. Tuỳ vào thiết bị Android của người dùng, nút này có thể là nút vật lý hoặc nút phần mềm.

Tính năng điều hướng quay lại tuỳ chỉnh

Android duy trì một ngăn xếp lui các đích đến khi người dùng di chuyển trong toàn bộ ứng dụng. Điều này thường cho phép Android điều hướng chính xác đến các đích đến trước khi nhấn nút Quay lại. Tuy nhiên, trong một số trường hợp, có thể ứng dụng cần phải triển khai thao tác Quay lại để mang lại trải nghiệm người dùng tốt nhất.

Ví dụ: khi sử dụng WebView như trình duyệt Chrome, bạn có thể ghi đè hành vi của nút Quay lại mặc định, cho phép người dùng quay lại thông qua lịch sử duyệt web thay vì các màn hình trước đó trong ứng dụng.

Tương tự, bạn cần cung cấp tính năng điều hướng quay lại tuỳ chỉnh cho SlidingPaneLayout và điều hướng ứng dụng từ ngăn chi tiết trở lại ngăn danh sách.

Triển khai tính năng điều hướng quay lại tuỳ chỉnh

Để triển khai tính năng điều hướng quay lại tuỳ chỉnh trong ứng dụng Sports, bạn cần phải:

  • Định nghĩa hàm gọi lại (callback) tuỳ chỉnh để xử lý thao tác nhấn phím quay lại. Lớp này sẽ ghi đè phương thức OnBackPressedCallback.
  • Đăng ký và thêm thực thể của hàm gọi lại này.

Trước tiên, hãy định nghĩa hàm gọi lại tuỳ chỉnh.

  1. Trong tệp SportsListFragment, thêm một lớp mới bên dưới định nghĩa lớp SportsListFragment. Đặt tên lớp này là SportsListOnBackPressedCallback.
  2. Truyền vào một thực thể private của SlidingPaneLayout dưới dạng tham số hàm khởi tạo.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
)
  1. Mở rộng lớp này từ lớp OnBackPressedCallback. Lớp OnBackPressedCallback xử lý các hàm gọi lại onBackPressed. Bạn sẽ khắc phục lỗi tham số hàm khởi tạo trong chốc lát.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback()

Hàm khởi tạo cho OnBackPressedCallback lấy giá trị boolean làm trạng thái kích hoạt ban đầu. Chỉ khi một hàm gọi lại được kích hoạt (tức là isEnabled() trả về giá trị true), trình điều phối sẽ gọi phương thức handleOnBackPressed() của hàm gọi lại để xử lý sự kiện cho nút Quay lại.

  1. Truyền slidingPaneLayout.isSlideable*&& slidingPaneLayout.isOpen* dưới dạng tham số hàm khởi tạo đến OnBackPressedCallback. Trạng thái Boolean isSlideable chỉ có giá trị true (đúng) nếu ngăn thứ hai có thể trượt được trên một màn hình nhỏ hơn và là ngăn duy nhất đang hiển thị. Giá trị của isOpen sẽ là true nếu ngăn thứ hai – ngăn nội dung đang mở hoàn toàn.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen)

Mã này đảm bảo rằng hàm gọi lại chỉ được kích hoạt trên các thiết bị màn hình nhỏ hơn và khi ngăn nội dung đang mở.

  1. Để khắc phục lỗi chưa triển khai phương thức, nhấp vào bóng đèn màu đỏ 5fdf362480bfe665.png và chọn Implement members (Triển khai thành viên).
  2. Nhấp OK trong cửa sổ bật lên Implement members (Triển khai thành viên) để ghi đè phương thức handleOnBackPressed.

Lớp của bạn sẽ có dạng như sau:

class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen) {
   /**
    * Callback for handling the [OnBackPressedDispatcher.onBackPressed] event.
    */
   override fun handleOnBackPressed() {
       TODO("Not yet implemented")
   }
}
  1. Bên trong hàm handleOnBackPressed(), xoá lệnh TODO rồi thêm mã sau để đóng ngăn nội dung và quay trở lại ngăn danh sách.
slidingPaneLayout.closePane()

Theo dõi các sự kiện của SlidingPaneLayout

Ngoài việc xử lý các sự kiện nhấn nút quay lại, bạn phải nghe và theo dõi các sự kiện liên quan đến ngăn trượt. Khi trượt ngăn nội dung, hàm gọi lại sẽ được bật hoặc tắt tương ứng. Bạn sẽ dùng PanelSlideListener để thực hiện việc này.

Giao diện SlidingPaneLayout.PanelSlideListener chứa 3 phương thức trừu tượng onPanelSlide(), onPanelOpened()onPanelClosed(). Các phương thức này sẽ được gọi khi thực hiện các thao tác trượt, mở, đóng ngăn chi tiết.

  1. Mở rộng lớp SportsListOnBackPressedCallback từ SlidingPaneLayout.PanelSlideListener.
  2. Để khắc phục lỗi, triển khai 3 phương thức sau. Nhấp vào bóng đèn màu đỏ rồi chọn Implement members (Triển khai thành viên) trong Android Studio.

ad52135eecbee09f.png

  1. Lớp SportsListOnBackPressedCallback của bạn sẽ có dạng như sau:
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
  SlidingPaneLayout.PanelSlideListener{

   override fun handleOnBackPressed() {
       slidingPaneLayout.closePane()
   }

   override fun onPanelSlide(panel: View, slideOffset: Float) {
       TODO("Not yet implemented")
   }

   override fun onPanelOpened(panel: View) {
       TODO("Not yet implemented")
   }

   override fun onPanelClosed(panel: View) {
       TODO("Not yet implemented")
   }
}
  1. Xoá lệnh TODO.
  2. Bật hàm gọi lại OnBackPressedCallback khi mở (hiển thị) ngăn chi tiết. Bạn có thể thực hiện việc này bằng cách gọi hàm setEnabled() và truyền vào giá trị true. Viết mã sau bên trong onPanelOpened():
setEnabled(true)
  1. Có thể đơn giản hoá mã ở trên bằng cú pháp truy cập thuộc tính.
override fun onPanelOpened(panel: View) {
   isEnabled = true
}
  1. Tương tự, đặt isEnabled thành false khi người dùng đóng ngăn chi tiết.
override fun onPanelClosed(panel: View) {
   isEnabled = false
}
  1. Bước cuối cùng để hoàn tất hàm gọi lại là thêm lớp trình nghe SportsListOnBackPressedCallback vào danh sách trình nghe để nhận thông báo về sự kiện trượt trên ngăn chi tiết. Thêm khối lệnh init vào lớp SportsListOnBackPressedCallback. Bên trong khối lệnh init, gọi phương thức slidingPaneLayout.addPanelSlideListener(), truyền vào tham số this.
init {
   slidingPaneLayout.addPanelSlideListener(this)
}

Lớp SportsListOnBackPressedCallback hoàn thiện sẽ có dạng như sau:

class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
  SlidingPaneLayout.PanelSlideListener{

   init {
       slidingPaneLayout.addPanelSlideListener(this)
   }

   override fun handleOnBackPressed() {
       slidingPaneLayout.closePane()
   }

   override fun onPanelSlide(panel: View, slideOffset: Float) {
   }

   override fun onPanelOpened(panel: View) {
       isEnabled = true
   }

   override fun onPanelClosed(panel: View) {
       isEnabled = false
   }
}

Đăng ký hàm gọi lại

Để hàm gọi lại có thể hoạt động, hãy đăng ký hàm gọi lại thông qua trình điều phối, OnBackPressedDispatcher.

Lớp cơ sở cho FragmentActivity cho phép bạn kiểm soát hành vi của nút Quay lại bằng cách sử dụng OnBackPressedDispatcher. OnBackPressedDispatcher kiểm soát cách gửi các sự kiện Nút quay lại đến một hoặc nhiều đối tượng OnBackPressedCallback.

Hãy thêm hàm gọi lại thông qua phương thức addCallback(). Phương thức này sử dụng một tham số là LifecycleOwner. Điều này đảm bảo rằng OnBackPressedCallback chỉ được thêm khi LifecycleOwner có giá trị là Lifecycle.State.STARTED. Hoạt động hoặc mảnh này cũng xoá các hàm gọi lại đã đăng ký khi LifecycleOwner liên kết với các hàm gọi lại này bị huỷ. Điều này giúp ngăn chặn các trường hợp rò rỉ bộ nhớ và đảm bảo sử dụng phù hợp các hàm gọi lại trong các mảnh hoặc chủ sở hữu vòng đời khác có thời gian hoạt động ngắn hơn.

Phương thức addCallback() cũng lấy thực thể của lớp gọi lại làm tham số thứ hai. Bạn sẽ đăng ký lớp gọi lại theo các bước sau:

  1. Trong tệp SportsListFragment, bên trong hàm onViewCreated(), ngay bên dưới phần khai báo biến liên kết, hãy tạo một thực thể của SlidingPaneLayout và gán giá trị binding.slidingPaneLayout cho thực thể này.
val slidingPaneLayout = binding.slidingPaneLayout
  1. Trong tệp SportsListFragment, bên trong hàm onViewCreated(), ngay bên dưới phần khai báo của slidingPaneLayout, hãy thêm mã sau:
// Connect the SlidingPaneLayout to the system back button.
requireActivity().onBackPressedDispatcher.addCallback(
   viewLifecycleOwner,
   SportsListOnBackPressedCallback(slidingPaneLayout)
)

Mã ở trên sử dụng addCallback(), truyền vào viewLifecycleOwner và một thực thể của SportsListOnBackPressedCallback. Hàm gọi lại này chỉ hoạt động trong vòng đời của mảnh này.

  1. Đã đến lúc chạy ứng dụng trên trình mô phỏng theo hồ sơ người dùng điện thoại và xem chức năng hoạt động của nút quay lại tuỳ chỉnh.

33967fa8fde5b902.gif

10. Chế độ khoá

Theo mặc định, khi ngăn danh sách và ngăn chi tiết chồng lên nhau trên màn hình nhỏ hơn, chẳng hạn như điện thoại, người dùng có thể vuốt theo cả hai hướng, thoải mái chuyển đổi giữa hai ngăn ngay cả khi không sử dụng tính năng di chuyển bằng cử chỉ. Bạn có thể khoá hoặc mở khoá ngăn chi tiết bằng cách thiết lập chế độ khoá của SlidingPaneLayout.

  1. Trong trình mô phỏng theo hồ sơ người dùng điện thoại, thử vuốt ngăn chi tiết ra khỏi màn hình.
  2. Bạn cũng có thể vuốt trong ngăn chi tiết. Hãy tự làm thử điều này.
  3. Đây không phải là một tính năng mong muốn trong ứng dụng Sports. Bạn nên khoá SlidingPaneLayout để ngăn cử chỉ vuốt vào và vuốt ra từ người dùng. Để thực hiện điều này, trong phương thức onViewCreated(), bên dưới định nghĩa slidingPaneLayout, hãy đặt lockMode thành LOCK_MODE_LOCKED:
slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Để tìm hiểu thêm về các chế độ khoá khác, vui lòng tham khảo tài liệu này.

  1. Chạy lại ứng dụng một lần nữa. Bạn sẽ thấy rằng ngăn chi tiết bây giờ đã được khoá.

Chúc mừng bạn đã thêm SlidingPaneLayout vào ứng dụng của mình!

11. Mã giải pháp

Mã giải pháp cho lớp học lập trình này nằm trong dự án và mô-đun được hiển thị bên dưới.

  1. Chuyển đến trang kho lưu trữ GitHub được cung cấp cho dự án.
  2. Xác minh rằng tên chi nhánh khớp với tên chi nhánh được chỉ định trong lớp học lập trình. Ví dụ: trong ảnh chụp màn hình sau đây, tên nhánh là main (chính).

1e4c0d2c081a8fd2.png

  1. Trên trang GitHub cho dự án này, hãy nhấp vào nút Code (Mã nguồn), một cửa sổ bật lên sẽ hiện ra.

1debcf330fd04c7b.png

  1. Trong cửa sổ bật lên, nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
  2. Xác định vị trí của tệp trên máy tính (có thể trong thư mục Tải xuống (Tệp đã tải xuống)).
  3. Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.

Mở dự án trong Android Studio

  1. Khởi động Android Studio.
  2. Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open (Mở).

d8e9dbdeafe9038a.png

Lưu ý: Nếu Android Studio đã mở sẵn thì hãy chuyển sang chọn tuỳ chọn File (Tệp) > Open (Mở) trên trình đơn.

8d1fda7396afe8e5.png

  1. Trong trình duyệt tệp, hãy chuyển đến vị trí của thư mục dự án chưa giải nén (có thể nằm trong thư mục Downloads (Tệp đã tải xuống)).
  2. Nhấp đúp vào thư mục dự án đó.
  3. Chờ Android Studio mở dự án.
  4. Nhấp vào nút Run (Chạy) 8de56cba7583251f.png để tạo bản dựng và chạy ứng dụng. Đảm bảo ứng dụng được xây dựng như mong đợi.

12. Tìm hiểu thêm