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

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 nhiều loại thiết bị, 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 cho việc phát hành chính thức (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ị.

Ứ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 đoạn mã xuống Android Studio và chạy đoạn 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.
  • Đoạn mã ban đầu của ứng dụng Sports.

Tải mã khởi đầu xuố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 đầu để bạn có thể mở rộng đoạn mã đó 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 đầu 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 dòng mã còn lạ lẫm mà bạn sẽ tìm hiểu trong các lớp sau.

Để lấy đoạn 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 đã được cung cấp để lấy đoạn mã 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 (Nhân bản). Android Studio bắt đầu tìm nạp mã của bạn.
  2. Đợi Android Studio tìm nạp xong.
  3. Chọn chính xác mô-đun cho mã khởi đầu, mã nguồn ứng dụng hoặc đoạn 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 (bằng biểu tượng Biểu tượng này cho thấy 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) để có thể thấy rõ các đoạn mã và Android Studio 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 ban đầu

Ứ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ã ban đầu

Mã khởi đầu bạn tải xuống có bố cục màn hình chi tiết và màn hình danh sách được thiết kế sẵn cho 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 để tận dụng không gian 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 tóm 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 và 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 đã quen thuộc 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 văn bản ứ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ữ những 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 nhập trực tiếp vào mã nguồn.

SportsViewModel.kt

Đây là ViewModel dùng chung cho ứng dụng. ViewModel này được chia sẻ bởi 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 mà người dùng đang 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.
  • Đoạn 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 với toàn bộ danh sách các môn thể thao trong data/SportsData.kt. _currentSport được khởi chạy với 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 hành động nhấp được truyền vào. Hầu hết các 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 danh sách này bằng 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() đí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 đầu 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 tạo trình mô phỏng, bạn sẽ chạy mã khởi đầu của ứng dụng Sports (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ị Android ảo). 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 để 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 các giá trị này vào lúc khác.
<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, sau đó 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 ở chế độ ngang Màn hình lớn bên trong ở 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 khung hiển thị con nếu các khung 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, hãy thêm layout_weight vào FragmentContainerView rồi 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 thông tin

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 tuỳ chỉnh.

9. Thêm tính năng điều hướng quay lại tuỳ 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 bằng cách 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 nhật ký 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 lệnh 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 lệnh gọi lại này.

Trước tiên, hãy định nghĩa lệnh 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 lệnh gọi lại onBackPressed. Bạn sẽ sớm khắc phục được lỗi tham số hàm khởi tạo.
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 lệnh gọi lại được kích hoạt (tức là isEnabled() trả về giá trị true), trình điều phối mới gọi handleOnBackPressed() của lệnh 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 sẽ chỉ có giá trị true 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. 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 lệnh 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, hãy nhấp vào bóng đèn màu đỏ 5fdf362480bfe665.png rồi chọn Implement members (Triển khai thành viên).
  2. Nhấp vào 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 ngăn nội dung trượt, lệnh 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 là 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 lệnh 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 giá trị true vào. 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 lệnh 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ý lệnh gọi lại

Để lệnh gọi lại có thể hoạt động, hãy đăng ký lệnh 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 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.

Thêm lệnh gọi lại bằng phương thức addCallback(). Phương thức này sử dụng một 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 lệnh gọi lại đã đăng ký khi LifecycleOwner liên kết với các lệnh gọi lại đó 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 việc sử dụng phù hợp các lệnh 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 sử dụng 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. Lệnh 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ả 2 hướng, thoải mái chuyển đổi giữa 2 ngăn ngay cả khi không sử dụng tính năng thao tác 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 của 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. Đoạn 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 hiện 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 nhánh khớp với tên 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.

1e4c0d2c081a8fd2.png

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

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 (thường nằm trong thư mục Downloads (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ở thì chuyển sang chọn tuỳ chọn File (Tệp) > Open (Mở) trong 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 (thường 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 và chạy ứng dụng. Đảm bảo ứng dụng được tạo như mong đợi.

12. Tìm hiểu thêm