Nhúng hoạt động nâng cao

1. Giới thiệu

Chức năng nhúng hoạt động có trong Android 12L (API cấp 32) cho phép các ứng dụng dựa trên hoạt động hiển thị cùng lúc nhiều hoạt động trên màn hình lớn để tạo bố cục hai ngăn (chẳng hạn như danh sách-chi tiết).

Lớp học lập trình Tạo bố cục danh sách-chi tiết bằng chức năng nhúng hoạt động và Material Design đã đề cập đến cách sử dụng XML hoặc lệnh gọi API Jetpack WindowManager để tạo một bố cục danh sách-chi tiết.

Lớp học lập trình này sẽ giới thiệu cho bạn một số tính năng mới phát hành dùng để nhúng hoạt động, giúp cải thiện hơn nữa trải nghiệm trong ứng dụng trên các thiết bị có màn hình lớn. Các tính năng này gồm có mở rộng ngăn, ghim hoạt động và làm mờ hộp thoại toàn màn hình.

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

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

Cách:

  • Bật tính năng mở rộng ngăn
  • Triển khai tính năng ghim hoạt động bằng một trong các cửa sổ phân chia
  • Sử dụng tính năng làm mờ hộp thoại toàn màn hình

Những gì bạn cần

  • Phiên bản Android Studio mới
  • Trình mô phỏng hoặc điện thoại Android chạy Android 15
  • Trình mô phòng hoặc máy tính bảng Android màn hình lớn (ít nhất chiều rộng phải trên 600 dp)

2. Thiết lập

Tải ứng dụng mẫu

Bước 1: Sao chép kho lưu trữ

Sao chép kho lưu trữ Git của các lớp học lập trình màn hình lớn:

git clone https://github.com/android/large-screen-codelabs

hoặc tải xuống và huỷ lưu trữ tệp zip của lớp học lập trình màn hình lớn:

Tải mã nguồn xuống

Bước 2: Kiểm tra các tệp nguồn của lớp học lập trình

Chuyển đến thư mục activity-embedding-advanced.

Bước 3: Mở dự án của lớp học lập trình

Trong Android Studio, hãy mở dự án Kotlin hoặc Java

Danh sách tệp cho thư mục hoạt động trong tệp repo và zip.

Thư mục activity-embedding-advanced trong tệp repo và tệp zip chứa hai dự án Android Studio: một bằng ngôn ngữ Kotlin, một bằng ngôn ngữ Java. Hãy mở dự án tuỳ chọn. Các đoạn mã của lớp học lập trình này được cung cấp bằng cả hai ngôn ngữ.

Tạo thiết bị ảo

Nếu bạn không có điện thoại, máy tính bảng cỡ nhỏ hoặc máy tính bảng cỡ lớn chạy Android API cấp 35 trở lên, hãy mở Trình quản lý thiết bị trong Android Studio và tạo bất cứ thiết bị ảo nào sau đây mà bạn cần:

  • Điện thoại — Pixel 8, API cấp 35 trở lên
  • Máy tính bảng – Pixel Tablet, API cấp 35 trở lên

3. Chạy ứng dụng

Ứng dụng mẫu hiện danh sách các mục. Khi người dùng chọn một mục, ứng dụng sẽ hiện thông tin về mục đó.

Ứng dụng này bao gồm ba hoạt động:

  • ListActivity – Chứa danh sách các mục trong RecyclerView
  • DetailActivity – Hiện thông tin về một mục trong danh sách khi mục đó được chọn từ danh sách
  • SummaryActivity — Hiện thông tin tóm tắt khi mục trong danh sách Tóm tắt được chọn

Tiếp tục từ lớp học lập trình trước

Trong lớp học lập trình Tạo bố cục danh sách-chi tiết bằng chức năng nhúng hoạt động và Material Design, chúng tôi đã dùng chức năng nhúng hoạt động để phát triển một ứng dụng có chế độ xem danh sách-chi tiết, trong đó cả dải điều hướng và thanh điều hướng dưới cùng đều hỗ trợ cho hoạt động điều hướng.

  1. Chạy ứng dụng trên một máy tính bảng cỡ lớn hoặc trình mô phỏng Pixel ở chế độ dọc. Bạn sẽ thấy màn hình danh sách chính và một thanh điều hướng ở dưới cùng.

74906232acad76f.png

  1. Xoay máy tính bảng sang ngang. Màn hình sẽ phân chia, hiện danh sách ở một bên và thông tin chi tiết ở bên còn lại. Thanh điều hướng ở dưới cùng sẽ được thay bằng một dải điều hướng dọc.

dc6a7d1c02c49cd4.png

Các tính năng mới có chức năng nhúng hoạt động

Bạn đã sẵn sàng nâng cấp bố cục hai ngăn chưa? Trong lớp học lập trình này, chúng ta sẽ thêm một số tính năng mới thú vị để nâng cao trải nghiệm của người dùng. Sau đây là những gì chúng ta sẽ xây dựng:

  1. Hãy cùng tạo các ngăn động nhé! Chúng ta sẽ triển khai tính năng mở rộng ngăn, nhờ đó người dùng có thể đổi kích thước (hoặc mở rộng) các ngăn để có một chế độ xem tuỳ chỉnh.

2ec5f7fd6df5d8cd.gif

  1. Hãy đem đến cho người dùng khả năng sắp xếp theo mức độ ưu tiên! Với tính năng Ghim hoạt động, người dùng có thể đảm bảo những công việc quan trọng nhất sẽ luôn hiện trên màn hình.

980d0033972737ed.gif

  1. Bạn cần tập trung vào một công việc cụ thể? Chúng tôi sẽ thêm tính năng làm mờ toàn màn hình để làm mờ đi một chút các yếu tố gây xao nhãng, đồng thời giúp người dùng tập trung vào những việc quan trọng nhất.

2d3455e0f8901f95.png

4. Mở rộng ngăn

Khi sử dụng bố cục hai ngăn trên một màn hình lớn, trong nhiều trường hợp, người dùng cần tập trung vào một ngăn trong khi vẫn hiện ngăn còn lại trên màn hình. Ví dụ: đọc bài viết ở một bên còn bên kia hiện danh sách cuộc trò chuyện. Thường thì người dùng muốn đổi kích thước các ngăn để có thể tập trung vào một hoạt động.

Để làm được việc này, chức năng nhúng hoạt động sẽ thêm một API mới nhằm cho phép người dùng thay đổi tỷ lệ phân chia và tuỳ chỉnh quá trình chuyển đổi kích thước.

Thêm phần phụ thuộc

Trước tiên, hãy thêm WindowManager 1.4 vào tệp build.gradle.

Lưu ý: Một số tính năng trong thư viện này chỉ hoạt động trên Android 15 (API cấp 35) trở lên.

build.gradle

 implementation 'androidx.window:window:1.4.0-alpha02'

Tuỳ chỉnh đường phân chia cửa sổ

Tạo một thực thể DividerAttributes và thêm thực thể đó vào SplitAttributes. Đối tượng này định cấu hình hành vi chung của bố cục phân chia. Bạn có thể sử dụng các thuộc tính màu, chiều rộng và phạm vi kéo của DividerAttributes để nâng cao trải nghiệm người dùng.

Cách tuỳ chỉnh đường phân chia:

  1. Kiểm tra cấp độ API của Tiện ích WindowManager. Vì tính năng mở rộng ngăn chỉ có trên API cấp 6 trở lên, nên các tính năng mới còn lại cũng vậy.
  2. Tạo DividerAttributes: Để tạo kiểu cho đường phân chia giữa các ngăn, hãy tạo một đối tượng DividerAttributes. Đối tượng này giúp bạn đặt:
  • color: Thay đổi màu của đường phân chia cho phù hợp với giao diện của ứng dụng hoặc tạo sự tách biệt trực quan.
  • widthDp: Điều chỉnh độ rộng của đường phân chia để tăng độ hiển thị hoặc để có giao diện tinh tế hơn.
  1. Thêm vào SplitAttributes: Khi bạn tuỳ chỉnh xong đường phân chia, hãy thêm đường đó vào đối tượng DividerAttributes.
  2. Đặt phạm vi kéo (không bắt buộc): Bạn cũng có thể kiểm soát khoảng cách mà người dùng có thể kéo đường phân chia để đổi kích thước ngăn.
  • DRAG_RANGE_SYSTEM_DEFAULT: Dùng giá trị đặc biệt này để giúp hệ thống xác định được một phạm vi kéo phù hợp dựa trên kiểu dáng và kích thước màn hình của thiết bị.
  • Giá trị tuỳ chỉnh (từ 0,33 đến 0,66): Đặt phạm vi kéo của riêng bạn để giới hạn mức độ mà người dùng có thể đổi kích thước các ngăn. Lưu ý rằng nếu người dùng kéo quá giới hạn này, bố cục phân chia sẽ bị tắt.

Thay thế splitAttributes bằng mã sau.

SplitManager.kt

val splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder()
   .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
   .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)

if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
   splitAttributesBuilder.setDividerAttributes(
       DividerAttributes.DraggableDividerAttributes.Builder()
           .setColor(getColor(context, R.color.divider_color))
           .setWidthDp(4)
           .setDragRange(
               DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
           .build()
   )
}
val splitAttributes: SplitAttributes = splitAttributesBuilder.build()

SplitManager.java

SplitAttributes.Builder splitAttributesBuilder = new SplitAttributes.Builder()
        .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
        .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT);

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    splitAttributesBuilder.setDividerAttributes(
            new DividerAttributes.DraggableDividerAttributes.Builder()
                    .setColor(ContextCompat.getColor(context, R.color.divider_color))
                    .setWidthDp(4)
                    .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
                    .build()
    );
}
SplitAttributes splitAttributes = splitAttributesBuilder.build();

Tạo divider_color.xml trong thư mục res/color với nội dung sau đây.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:color="#669df6" />
</selector>

Chạy ứng dụng!

Vậy là xong. Hãy tạo và chạy ứng dụng mẫu này.

Bạn sẽ thấy tính năng mở rộng ngăn và có thể kéo để thay đổi kích thước.

2ec5f7fd6df5d8cd.gif

Thay đổi tỷ lệ phân chia trong các phiên bản cũ

Lưu ý quan trọng về khả năng tương thích: Tính năng mở rộng ngăn chỉ có trên Tiện ích WindowManager 6 trở lên, tức là bạn cần có Android 15 (API cấp 35) trở lên.

Tuy nhiên, bạn sẽ vẫn muốn đem đến trải nghiệm tốt cho người dùng trên các phiên bản Android cũ.

Để làm điều này, trên Android 14 (API cấp 34) trở xuống, bạn có thể điều chỉnh tỷ lệ phân chia động bằng cách sử dụng lớp SplitAttributesCalculator. Qua đó giúp bạn duy trì được khả năng kiểm soát người dùng ở một mức nào đó đối với bố cục, ngay cả khi không có tính năng mở rộng ngăn.

a36f8ba4226353c5.gif

Bạn có muốn biết cách tận dụng các tính năng này không? Chúng tôi sẽ trình bày mọi bí quyết thú vị nhất và mẹo của người trong ngành ở phần "Các phương pháp hay nhất".

5. Ghim hoạt động

Bạn đã bao giờ muốn giữ cố định một phần của chế độ xem chia đôi màn hình trong khi thoải mái di chuyển ở phần còn lại chưa? Hãy nghĩ đến trường hợp đọc một bài viết dài ở một bên trong khi vẫn có thể tương tác với nội dung khác của ứng dụng ở bên còn lại.

Đó là lúc bạn cần đến tính năng ghim hoạt động! Tính năng này giúp bạn ghim một trong các cửa sổ đã phân chia để cửa sổ đó vẫn xuất hiện trên màn hình ngay cả khi bạn thao tác trong cửa sổ còn lại. Nhờ vậy mà người dùng có được trải nghiệm đa nhiệm tập trung và hiệu quả hơn.

Thêm nút ghim

Trước tiên, hãy thêm một nút vào DetailActivity.. Ứng dụng sẽ ghim DetailActivity này khi người dùng nhấp vào nút đó.

Thực hiện các thay đổi sau đối với activity_detail.xml:

  1. Thêm một mã nhận dạng vào ConstraintLayout
android:id="@+id/detailActivity"
  1. Thêm một nút ở cuối bố cục
<androidx.appcompat.widget.AppCompatButton
      android:id="@+id/pinButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/pin_this_activity"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>
  1. Giới hạn phần dưới của TextView với phần đầu của nút
app:layout_constraintBottom_toTopOf="@id/pinButton"

Xoá dòng này trong TextView.

app:layout_constraintBottom_toBottomOf="parent"

Dưới đây là mã XML hoàn chỉnh cho tệp bố cục activity_detail.xml, bao gồm cả nút PIN THIS ACTIVITY (GHIM HOẠT ĐỘNG NÀY) mà chúng ta vừa thêm:

<androidx.constraintlayout.widget.ConstraintLayout
    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:id="@+id/detailActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".DetailActivity">

  <TextView
      android:id="@+id/textViewItemDetail"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="36sp"
      android:textColor="@color/obsidian"
      app:layout_constraintBottom_toTopOf="@id/pinButton"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  <androidx.appcompat.widget.AppCompatButton
      android:id="@+id/pinButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/pin_this_activity"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Thêm chuỗi pin_this_activity vào res/values/strings.xml.

<string name="pin_this_activity">PIN THIS ACTIVITY</string>

Kết nối nút ghim

  1. Khai báo biến: Trong tệp DetailActivity.kt, hãy khai báo một biến để lưu giữ thông tin tham chiếu đến nút PIN THIS ACTIVITY (GHIM HOẠT ĐỘNG NÀY):

DetailActivity.kt

private lateinit var pinButton: Button

DetailActivity.java

private Button pinButton;
  1. Tìm nút trong bố cục và thêm một lệnh gọi lại setOnClickListener().

DetailActivity.kt / onCreate

pinButton = findViewById(R.id.pinButton)
pinButton.setOnClickListener {
 pinActivityStackExample(taskId)
}

DetailActivity.java / onCreate()

Button pinButton = findViewById(R.id.pinButton);
pinButton.setOnClickListener( (view) => {
        pinActivityStack(getTaskId());

});
  1. Tạo một phương thức mới có tên là pinActivityStackExample trong lớp DetailActivity. Chúng ta sẽ triển khai logic ghim thực tế tại đây.

DetailActivity.kt

private fun pinActivityStackExample(taskId: Int) {

 val splitAttributes: SplitAttributes = SplitAttributes.Builder()
   .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
   .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
   .build()

 val pinSplitRule = SplitPinRule.Builder()
   .setSticky(true)
   .setDefaultSplitAttributes(splitAttributes)
   .build()

 SplitController.getInstance(applicationContext).pinTopActivityStack(taskId, pinSplitRule)
}

DetailActivity.java

private void pinActivityStackExample(int taskId) {
    SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();

    SplitPinRule pinSplitRule = new SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build();

    SplitController.getInstance(getApplicationContext()).pinTopActivityStack(taskId, pinSplitRule);
}

Lưu ý:

  1. Mỗi lần, bạn chỉ có thể ghim một hoạt động. Bỏ ghim hoạt động đang được ghim bằng
unpinTopActivityStack()

trước khi bạn ghim một hoạt động khác.

  1. Để bật tính năng mở rộng ngăn khi ghim hoạt động, hãy gọi
setDividerAttributes()

cho cả

SplitAttributes

mới được tạo.

Thay đổi về tính năng điều hướng quay lại

Với WindowManager 1.4, hành vi của thao tác quay lại đã thay đổi. Nếu sử dụng nút điều hướng, sự kiện quay lại sẽ được gửi tới hoạt động được lấy làm tâm điểm gần đây nhất.

Thao tác bằng nút:

  • Với thao tác điều hướng bằng nút, sự kiện quay lại hiện được gửi nhất quán đến hoạt động được lấy làm tâm điểm gần đây nhất. Nhờ vậy giúp đơn giản hoá hành vi điều hướng quay lại để người dùng dễ dự đoán hơn.

Thao tác bằng cử chỉ:

  • Android 14 (API cấp 34) trở xuống: Thao tác quay lại sẽ gửi sự kiện đến hoạt động nơi thao tác đó xảy ra, điều này có thể dẫn đến hành vi không mong muốn trong các trường hợp chia đôi màn hình.
  • Android 15 (API cấp 35) trở lên:
  • Các hoạt động trong cùng một ứng dụng: Cử chỉ quay lại luôn kết thúc ở hoạt động trên cùng, bất kể hướng vuốt nhằm mang lại một trải nghiệm mang tính hợp nhất hơn.
  • Các hoạt động trong ứng dụng khác nhau (lớp phủ): Sự kiện quay lại sẽ chuyển đến hoạt động được lấy làm tâm điểm gần đây nhấttheo hành vi của thao tác điều hướng bằng nút.

Chạy ứng dụng!

Hãy tạo và chạy ứng dụng mẫu này.

Ghim hoạt động

  • Chuyển đến màn hình DetailActivity.
  • Nhấn vào nút PIN THIS ACTIVITY (GHIM HOẠT ĐỘNG NÀY).

980d0033972737ed.gif

6. Làm mờ hộp thoại toàn màn hình

Mặc dù chức năng nhúng hoạt động hỗ trợ cho bố cục chia đôi màn hình, nhưng hộp thoại trong các phiên bản cũ chỉ làm mờ vùng chứa của hoạt động. Điều này có thể khiến trải nghiệm hình ảnh trở nên rời rạc, đặc biệt là khi bạn muốn hộp thoại xuất hiện ở vị trí trung tâm.

Giải pháp: WindowManager 1.4

  • Đã có chúng tôi giúp bạn khắc phục điều này! Với WindowManager 1.4, theo mặc định, các hộp thoại giờ đây sẽ làm mờ toàn bộ cửa sổ ứng dụng (DimAreaBehavior.Companion.ON_TASK), mang đến cảm giác sống động và tập trung hơn.
  • Bạn cần khôi phục hành vi cũ? Không thành vấn đề! Bạn vẫn có thể chọn chỉ làm mờ vùng chứa của hoạt động bằng ON_ACTIVITY_STACK.

ON_ACTIVITY_STACK

ON_TASK

Sau đây là cách dùng ActivityEmbeddingController để quản lý hành vi làm mờ toàn màn hình:

Lưu ý: Bạn có thể dùng Tiện ích WindowManager phiên bản 5 trở lên để sử dụng tính năng làm mờ hộp thoại toàn màn hình.

SplitManager.kt / createSplit()

with(ActivityEmbeddingController.getInstance(context)) {
   if (WindowSdkExtensions.getInstance().extensionVersion  >= 5) {
       setEmbeddingConfiguration(
           EmbeddingConfiguration.Builder()
               .setDimAreaBehavior(ON_TASK)
               .build()
       )
   }
}

SplitManager.java / createSplit()

ActivityEmbeddingController controller = ActivityEmbeddingController.getInstance(context);
if (WindowSdkExtensions.getInstance().getExtensionVersion()  >= 5) {
    controller.setEmbeddingConfiguration(
        new EmbeddingConfiguration.Builder()
            .setDimAreaBehavior(EmbeddingConfiguration.DimAreaBehavior.ON_TASK)
            .build()
    );
}

Để minh hoạ tính năng làm mờ toàn màn hình, chúng tôi sẽ giới thiệu một hộp thoại cảnh báo nhắc người dùng xác nhận trước khi ghim hoạt động. Khi xuất hiện, hộp thoại này sẽ làm mờ toàn bộ cửa sổ ứng dụng, chứ không chỉ riêng vùng chứa nơi có hoạt động.

DetailActivity.kt

pinButton.setOnClickListener {
 showAlertDialog(taskId)
}

...
private fun showAlertDialog(taskId: Int) {
 val builder = AlertDialog.Builder(this)
 builder.setTitle(getString(R.string.dialog_title))
 builder.setMessage(getString(R.string.dialog_message))
 builder.setPositiveButton(getString(R.string.button_yes)) { _, _ ->
   if (WindowSdkExtensions.getInstance().extensionVersion  >= 6) {
     pinActivityStackExample(taskId)
   }
 }
 builder.setNegativeButton(getString(R.string.button_cancel)) { _, _ ->
   // Cancel
 }
 val dialog: AlertDialog = builder.create()
 dialog.show()
}

DetailActivity.java

pinButton.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       showAlertDialog(getTaskId());
   }
});

...

private void showAlertDialog(int taskId) {
   AlertDialog.Builder builder = new AlertDialog.Builder(this);
   builder.setTitle(getString(R.string.dialog_title));
   builder.setMessage(getString(R.string.dialog_message));

   builder.setPositiveButton(getString(R.string.button_yes), new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
           if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
               pinActivityStackExample(taskId);
           }
       }
   });
   builder.setNegativeButton(getString(R.string.button_cancel), new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
           // Cancel
       }
   });
   AlertDialog dialog = builder.create();
   dialog.show();
}

Thêm các chuỗi sau vào res/values/strings.xml.

<!-- Dialog information -->
<string name="dialog_title">Activity Pinning</string>
<string name="dialog_message">Confirm to pin this activity</string>
<string name="button_yes">Yes</string>
<string name="button_cancel">Cancel</string>

Chạy ứng dụng!

Hãy tạo và chạy ứng dụng mẫu này.

Nhấp vào nút ghim hoạt động:

  • Một hộp thoại cảnh báo sẽ xuất hiện để nhắc bạn xác nhận thao tác ghim.
  • Chú ý cách toàn bộ màn hình bị làm mờ (kể cả hai ngăn bị phân chia) để dồn sự chú ý vào hộp thoại.

2d3455e0f8901f95.png

7. Các phương pháp hay nhất

Cho phép người dùng tắt bố cục hai ngăn

Để quá trình chuyển đổi sang bố cục mới diễn ra suôn sẻ hơn, hãy cho phép người dùng chuyển đổi giữa chế độ xem hai ngăn và chế độ xem một cột. Để làm được việc này, chúng ta có thể dùng SplitAttributesCalculatorSharedPreferences để lưu trữ các lựa chọn ưu tiên của người dùng.

Thay đổi tỷ lệ phân chia trên Android 14 trở xuống

Chúng ta đã khám phá ra một tính năng mới rất hữu ích giúp người dùng có thể điều chỉnh tỷ lệ phân chia trên Android 15 trở lên, đó là tính năng mở rộng ngăn. Nhưng làm sao để duy trì được mức độ linh hoạt như thế này trên các thiết bị dùng Android phiên bản cũ?

Hãy cùng tìm hiểu xem cách SplitAttributesCalculator có thể giúp chúng ta làm được việc này, cũng như đảm bảo một trải nghiệm nhất quán trên nhiều thiết bị hơn.

Dưới đây là một ví dụ để bạn có thể hiểu rõ hơn:

a87452341434c86d.gif

Tạo màn hình cài đặt

Để bắt đầu, hãy tạo một màn hình cài đặt dành riêng cho phần cấu hình người dùng.

Trong màn hình cài đặt này, chúng ta sẽ tích hợp một nút chuyển để bật hoặc tắt chức năng nhúng hoạt động cho toàn bộ ứng dụng. Ngoài ra, chúng ta sẽ thêm một thanh tiến trình để giúp người dùng điều chỉnh tỷ lệ phân chia bố cục hai ngăn. Xin lưu ý rằng giá trị tỷ lệ phân chia này sẽ chỉ được áp dụng nếu bạn bật nút chuyển nhúng hoạt động.

Sau khi người dùng đặt các giá trị trong SettingsActivity, chúng ta sẽ lưu các giá trị đó trong SharedPreferences để dùng sau ở các vị trí khác trong ứng dụng.

build.gradle

Thêm phần phụ thuộc tuỳ chọn.

implementation 'androidx.preference:preference-ktx:1.2.1' // Kotlin

Hoặc

implementation 'androidx.preference:preference:1.2.1' // Java

SettingsActivity.kt

package com.example.activity_embedding

import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat

class SettingsActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.settings_activity)
    if (savedInstanceState == null) {
      supportFragmentManager
        .beginTransaction()
        .replace(R.id.settings, SettingsFragment())
        .commit()
    }
    supportActionBar?.setDisplayHomeAsUpEnabled(true)
  }

  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    if (item.itemId == android.R.id.home) finishActivity()
    return super.onOptionsItemSelected(item)
  }

  private fun finishActivity() { finish() }

  class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
      setPreferencesFromResource(R.xml.root_preferences, rootKey)
findPreference<SwitchPreferenceCompat>("dual_pane")?.setOnPreferenceChangeListener { _, newValue ->
        if (newValue as Boolean) {
          this.activity?.let {
            SharePref(it.applicationContext).setAEFlag(true)
          }
        } else {
          this.activity?.let {
            SharePref(it.applicationContext).setAEFlag(false)
          }
        }
        this.activity?.finish()
        true
      }

      val splitRatioPreference: SeekBarPreference? = findPreference("split_ratio")
      splitRatioPreference?.setOnPreferenceChangeListener { _, newValue ->
        if (newValue is Int) {
          this.activity?.let { SharePref(it.applicationContext).setSplitRatio(newValue.toFloat()/100) }
        }
        true
      }
    }
  }
}

SettingsActivity.java

package com.example.activity_embedding;

import android.os.Bundle;
import android.view.MenuItem;

import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SeekBarPreference;
import androidx.preference.SwitchPreferenceCompat;

public class SettingsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings_activity);
        if (savedInstanceState == null) {
            getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.settings, new SettingsFragment())
                .commit();
        }
        if (getSupportActionBar() != null) {
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            finishActivity();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void finishActivity() {
        finish();
    }

    public static class SettingsFragment extends PreferenceFragmentCompat {
        @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
            setPreferencesFromResource(R.xml.root_preferences, rootKey);

            SwitchPreferenceCompat dualPanePreference = findPreference("dual_pane");
            if (dualPanePreference != null) {
                dualPanePreference.setOnPreferenceChangeListener((preference, newValue) -> {
                    boolean isDualPane = (Boolean) newValue;
                    if (getActivity() != null) {
                        SharePref sharePref = new SharePref(getActivity().getApplicationContext());
                        sharePref.setAEFlag(isDualPane);
                        getActivity().finish();
                    }
                    return true;
                });
            }

            SeekBarPreference splitRatioPreference = findPreference("split_ratio");
            if (splitRatioPreference != null) {
                splitRatioPreference.setOnPreferenceChangeListener((preference, newValue) -> {
                    if (newValue instanceof Integer) {
                        float splitRatio = ((Integer) newValue) / 100f;
                        if (getActivity() != null) {
                            SharePref sharePref = new SharePref(getActivity().getApplicationContext());
                            sharePref.setSplitRatio(splitRatio);
                        }
                    }
                    return true;
                });
            }
        }
    }
}

Thêm settings_activity.xml vào thư mục bố cục

settings_activity.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <FrameLayout
       android:id="@+id/settings"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />
</LinearLayout>

Thêm SettingsActivity vào tệp kê khai.

<activity
   android:name=".SettingsActivity"
   android:exported="false"
   android:label="@string/title_activity_settings" />

Định cấu hình quy tắc phân chia cho SettingsActivity.

SplitManager.kt / createSplit()

val settingActivityFilter = ActivityFilter(
   ComponentName(context, SettingsActivity::class.java),
   null
)
val settingActivityFilterSet = setOf(settingActivityFilter)
val settingActivityRule = ActivityRule.Builder(settingActivityFilterSet)
   .setAlwaysExpand(true)
   .build()
ruleController.addRule(settingActivityRule)

SplitManager.java / createSplit()

Set<ActivityFilter> settingActivityFilterSet = new HashSet<>();
ActivityFilter settingActivityFilter = new ActivityFilter(
        new ComponentName(context, SettingsActivity.class),
        null
);
settingActivityFilterSet.add(settingActivityFilter);
ActivityRule settingActivityRule = new ActivityRule.Builder(settingActivityFilterSet)
        .setAlwaysExpand(true).build();
ruleController.addRule(settingActivityRule);

Dưới đây là mã để lưu chế độ cài đặt của người dùng trong SharedPreferences .

SharedPref.kt

package com.example.activity_embedding

import android.content.Context
import android.content.SharedPreferences

class SharePref(context: Context) {
    private val sharedPreferences: SharedPreferences =
        context.getSharedPreferences("my_app_preferences", Context.MODE_PRIVATE)

    companion object {
        private const val AE_FLAG = "is_activity_embedding_enabled"
        private const val SPLIT_RATIO = "activity_embedding_split_ratio"
        const val DEFAULT_SPLIT_RATIO = 0.3f
    }

    fun setAEFlag(isEnabled: Boolean) {
        sharedPreferences.edit().putBoolean(AE_FLAG, isEnabled).apply()
    }

    fun getAEFlag(): Boolean = sharedPreferences.getBoolean(AE_FLAG, true)

    fun getSplitRatio(): Float = sharedPreferences.getFloat(SPLIT_RATIO, DEFAULT_SPLIT_RATIO)

    fun setSplitRatio(ratio: Float) {
        sharedPreferences.edit().putFloat(SPLIT_RATIO, ratio).apply()
    }
}

SharedPref.java

package com.example.activity_embedding;

import android.content.Context;
import android.content.SharedPreferences;

public class SharePref {
    private static final String PREF_NAME = "my_app_preferences";
    private static final String AE_FLAG = "is_activity_embedding_enabled";
    private static final String SPLIT_RATIO = "activity_embedding_split_ratio";
    public static final float DEFAULT_SPLIT_RATIO = 0.3f;

    private final SharedPreferences sharedPreferences;

    public SharePref(Context context) {
        this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
    }

    public void setAEFlag(boolean isEnabled) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(AE_FLAG, isEnabled);
        editor.apply();
    }

    public boolean getAEFlag() {
        return sharedPreferences.getBoolean(AE_FLAG, true);
    }

    public float getSplitRatio() {
        return sharedPreferences.getFloat(SPLIT_RATIO, DEFAULT_SPLIT_RATIO);
    }

    public void setSplitRatio(float ratio) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putFloat(SPLIT_RATIO, ratio);
        editor.apply();
    }
}

Bạn cũng cần có một tệp xml bố cục màn hình cho lựa chọn ưu tiên, hãy tạo root_preferences.xml trong res/xml bằng mã sau.

<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:android="http://schemas.android.com/apk/res/android">
   <PreferenceCategory app:title="@string/split_setting_header">

       <SwitchPreferenceCompat
           app:key="dual_pane"
           app:title="@string/dual_pane_title" />

       <SeekBarPreference
           app:key="split_ratio"
           app:title="@string/split_ratio_title"
           android:min="0"
           android:max="100"
           app:defaultValue="50"
           app:showSeekBarValue="true" />
   </PreferenceCategory>
</PreferenceScreen>

Rồi thêm mã sau vào res/values/strings.xml.

<string name="title_activity_settings">SettingsActivity</string>
<string name="split_setting_header">Dual Pane Display</string>
<string name="dual_pane_title">Dual Pane</string>
<string name="split_ratio_title">Split Ratio</string>

Thêm SettingsActivity vào trình đơn

Hãy kết nối SettingsActivity mới tạo với một đích đến điều hướng để người dùng có thể dễ dàng truy cập vào đích đến đó từ giao diện chính của ứng dụng.

  1. Trong tệp ListActivity, hãy khai báo các biến cho thanh điều hướng dưới cùng và dải điều hướng bên trái:

ListActivity.kt

 private lateinit var navRail: NavigationRailView private lateinit var bottomNav: BottomNavigationView

ListActivity.java

 private NavigationRailView navRail;  private BottomNavigationView bottomNav;
  1. Trong phương thức onCreate() của ListActivity, hãy dùng findViewById để kết nối các biến này với các thành phần hiển thị tương ứng trong bố cục;
  2. Thêm OnItemSelectedListener vào cả thanh điều hướng dưới cùng lẫn dải điều hướng để xử lý các sự kiện chọn mục:

ListActivity.kt / onCreate()

navRail  = findViewById(R.id.navigationRailView)
bottomNav = findViewById(R.id.bottomNavigationView)

val menuListener = NavigationBarView.OnItemSelectedListener { item ->
    when (item.itemId) {
        R.id.navigation_home -> {
            true
        }
        R.id.navigation_dashboard -> {
            true
        }
        R.id.navigation_settings -> {
            startActivity(Intent(this, SettingsActivity::class.java))
            true
        }
        else -> false
    }
}

navRail.setOnItemSelectedListener(menuListener)
bottomNav.setOnItemSelectedListener(menuListener)

ListActivity.java / onCreate()

NavigationRailView navRail = findViewById(R.id.navigationRailView);
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigationView);

NavigationBarView.OnItemSelectedListener menuListener = new NavigationBarView.OnItemSelectedListener() {
   @Override
   public boolean onNavigationItemSelected(@NonNull MenuItem item) {
       switch (item.getItemId()) {
           case R.id.navigation_home:
               // Handle navigation_home selection
               return true;
           case R.id.navigation_dashboard:
               // Handle navigation_dashboard selection
               return true;
           case R.id.navigation_settings:
               startActivity(new Intent(ListActivity.this, SettingsActivity.class));
               return true;
           default:
               return false;
       }
   }
};

navRail.setOnItemSelectedListener(menuListener);
bottomNav.setOnItemSelectedListener(menuListener);

Ứng dụng sẽ đọc SharedPreferences và hiển thị ứng dụng ở chế độ phân chia hoặc chế độ SPLIT_TYPE_EXPAND.

  • Khi cấu hình cửa sổ thay đổi, chương trình sẽ kiểm tra xem giới hạn chia đôi cửa sổ đã thoả mãn hay chưa (nếu chiều rộng > 840 dp)
  • Ứng dụng sẽ kiểm tra giá trị SharedPreferences để xem người dùng đã bật chế độ hiển thị cửa sổ chia đôi chưa. Nếu chưa bật, thì ứng dụng sẽ trả về SplitAttribute thuộc loại SPLIT_TYPE_EXPAND.
  • Nếu đã bật chế độ chia đôi cửa sổ, thì ứng dụng sẽ đọc giá trị SharedPreferences để lấy tỷ lệ phân chia. Điều này chỉ áp dụng khi phiên bản WindowSDKExtensions nhỏ hơn 6, vì phiên bản 6 vốn đã hỗ trợ tính năng mở rộng ngăn và bỏ qua chế độ cài đặt tỷ lệ phân chia. Thay vào đó, nhà phát triển có thể cho phép người dùng kéo đường phân chia trên giao diện người dùng.

ListActivity.kt / onCreate()

...

SplitController.getInstance(this).setSplitAttributesCalculator{
       params -> params.defaultSplitAttributes
   if (params.areDefaultConstraintsSatisfied) {
       setWiderScreenNavigation(true)

       if (SharePref(this.applicationContext).getAEFlag()) {
           if (WindowSdkExtensions.getInstance().extensionVersion  < 6) {
               // Read a dynamic split ratio from shared preference.
               val currentSplit = SharePref(this.applicationContext).getSplitRatio()
               if (currentSplit != SharePref.DEFAULT_SPLIT_RATIO) {
                   return@setSplitAttributesCalculator SplitAttributes.Builder()
                       .setSplitType(SplitAttributes.SplitType.ratio(SharePref(this.applicationContext).getSplitRatio()))
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
                       .build()
               }
           }
           return@setSplitAttributesCalculator params.defaultSplitAttributes
       } else {
           SplitAttributes.Builder()
               .setSplitType(SPLIT_TYPE_EXPAND)
               .build()
       }
   } else {
       setWiderScreenNavigation(false)
       SplitAttributes.Builder()
           .setSplitType(SPLIT_TYPE_EXPAND)
           .build()
   }
}

...

ListActivity.java / onCreate()

...
SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
   if (params.areDefaultConstraintsSatisfied()) {
       setWiderScreenNavigation(true);

       SharePref sharedPreference = new SharePref(this.getApplicationContext());
       if (sharedPreference.getAEFlag()) {
           if (WindowSdkExtensions.getInstance().getExtensionVersion()  < 6) {
               // Read a dynamic split ratio from shared preference.
               float currentSplit = sharedPreference.getSplitRatio();
               if (currentSplit != SharePref.DEFAULT_SPLIT_RATIO) {
                   return new SplitAttributes.Builder()
                           .setSplitType(SplitAttributes.SplitType.ratio(sharedPreference.getSplitRatio()))
                           .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
                           .build();
               }
           }
           return params.getDefaultSplitAttributes();
       } else {
           return new SplitAttributes.Builder()
                   .setSplitType(SPLIT_TYPE_EXPAND)
                   .build();
       }
   } else {
       setWiderScreenNavigation(false);
       return new SplitAttributes.Builder()
               .setSplitType(SPLIT_TYPE_EXPAND)
               .build();
   }
});

...

Để kích hoạt SplitAttributesCalculator sau khi thay đổi chế độ cài đặt, chúng ta cần vô hiệu hoá các thuộc tính hiện tại. Để làm điều này, hãy gọi invalidateVisibleActivityStacks() từ ActivityEmbeddingController; trước khi gọi WindowManager 1.4, phương thức này được gọi là

invalidateTopVisibleSplitAttributes.

ListActivity.kt / onResume()

override fun onResume() {
   super.onResume()
   ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks()
}

ListActivity.java / onResume()

@Override
public void onResume() {
    super.onResume();
    ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks();
}

Chạy ứng dụng!

Hãy tạo và chạy ứng dụng mẫu này.

Khám phá các chế độ cài đặt:

  • Chuyển đến màn hình cài đặt.
  • Bật và tắt nút chuyển Bật chế độ Phân chia cửa sổ.
  • Điều chỉnh thanh trượt tỷ lệ phân chia (nếu có trên thiết bị của bạn).

Quan sát các thay đổi về bố cục:

  • Trên các thiết bị chạy Android 14 trở xuống: Bố cục sẽ chuyển đổi giữa chế độ một ngăn và chế độ hai ngăn bằng cách dùng nút chuyển và tỷ lệ phân chia sẽ thay đổi khi bạn điều chỉnh thanh trượt.
  • Trên các thiết bị chạy Android 15 trở lên: Nhờ có tính năng mở rộng ngăn, bạn có thể đổi kích thước các ngăn một cách linh hoạt, bất kể thanh trượt đang ở chế độ cài đặt nào.

8. Xin chúc mừng!

Tốt lắm! Ứng dụng của bạn trở nên hữu ích hơn nhờ việc tích hợp thành công các tính năng mới, mạnh mẽ thông qua chức năng nhúng hoạt động và WindowManager. Giờ đây, người dùng sẽ được tận hưởng trải nghiệm linh hoạt, trực quan và hấp dẫn hơn trên màn hình lớn, bất kể họ dùng phiên bản Android nào.

9. Tìm hiểu thêm