Điều khiển và tạo ảnh động cho bàn phím phần mềm

Thử dùng Compose
Jetpack Compose là bộ công cụ giao diện người dùng được đề xuất cho Android. Tìm hiểu cách sử dụng bàn phím trong Compose.

Bằng cách sử dụng WindowInsetsCompat, ứng dụng của bạn có thể truy vấn và kiểm soát bàn phím ảo (còn gọi là IME) tương tự như cách ứng dụng tương tác với các thanh hệ thống. Ứng dụng của bạn cũng có thể sử dụng WindowInsetsAnimationCompat để tạo hiệu ứng chuyển đổi liền mạch khi bàn phím phần mềm được mở hoặc đóng.

Hình 1. Hai ví dụ về hiệu ứng chuyển đổi đóng-mở bàn phím phần mềm.

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

Trước khi thiết lập chế độ điều khiển và ảnh động cho bàn phím phần mềm, hãy định cấu hình ứng dụng của bạn để hiển thị tràn viền. Điều này cho phép nó xử lý phần lồng ghép cửa sổ hệ thống, chẳng hạn như các thanh hệ thống và bàn phím ảo.

Kiểm tra chế độ hiển thị của phần mềm bàn phím

Sử dụng WindowInsets để kiểm tra chế độ hiển thị của bàn phím phần mềm.

Kotlin

val insets = ViewCompat.getRootWindowInsets(view) ?: return
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

Java

WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;

Ngoài ra, bạn có thể dùng ViewCompat.setOnApplyWindowInsetsListener để theo dõi các thay đổi về khả năng hiển thị của bàn phím phần mềm.

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
  val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
  val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
  insets
}

Java

ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
  boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
  int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
  return insets;
});

Đồng bộ hoá ảnh động với bàn phím phần mềm

Khi người dùng nhấn vào một trường nhập văn bản, bàn phím sẽ trượt vào vị trí từ cuối màn hình, như minh hoạ trong ví dụ sau:

Hình 2. Ảnh động bàn phím được đồng bộ hoá.
  • Ví dụ được gắn nhãn "Không đồng bộ" trong hình 2 cho thấy hành vi mặc định trong Android 10 (API cấp 29), trong đó trường văn bản và nội dung của ứng dụng sẽ xuất hiện ngay lập tức thay vì đồng bộ hoá với ảnh động của bàn phím – hành vi này có thể gây khó chịu về mặt thị giác.

  • Trong Android 11 (API cấp 30) trở lên, bạn có thể dùng WindowInsetsAnimationCompat để đồng bộ hoá quá trình chuyển đổi của ứng dụng với bàn phím trượt lên và xuống từ cuối màn hình. Điều này trông mượt mà hơn, như minh hoạ trong ví dụ có nhãn "Đồng bộ hoá" trong hình 2.

Định cấu hình WindowInsetsAnimationCompat.Callback bằng khung hiển thị cần đồng bộ hoá với ảnh động bàn phím.

Kotlin

ViewCompat.setWindowInsetsAnimationCallback(
  view,
  object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
    // Override methods.
  }
)

Java

ViewCompat.setWindowInsetsAnimationCallback(
    view,
    new WindowInsetsAnimationCompat.Callback(
        WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
    ) {
      // Override methods.
    });

Có một số phương thức cần ghi đè trong WindowInsetsAnimationCompat.Callback, cụ thể là onPrepare(), onStart(), onProgress()onEnd(). Bắt đầu bằng cách gọi onPrepare() trước khi có bất kỳ thay đổi nào về bố cục.

onPrepare được gọi khi một ảnh động phần lồng ghép bắt đầu và trước khi các khung hiển thị được bố trí lại do ảnh động. Bạn có thể dùng nó để lưu trạng thái bắt đầu, trong trường hợp này là toạ độ dưới cùng của khung hiển thị.

Hình ảnh minh hoạ toạ độ dưới cùng của trạng thái bắt đầu của khung hiển thị gốc.
Hình 3. Sử dụng onPrepare() để ghi lại trạng thái bắt đầu.

Đoạn mã sau đây cho thấy một lệnh gọi mẫu đến onPrepare:

Kotlin

var startBottom = 0f

override fun onPrepare(
  animation: WindowInsetsAnimationCompat
) {
  startBottom = view.bottom.toFloat()
}

Java

float startBottom;

@Override
public void onPrepare(
    @NonNull WindowInsetsAnimationCompat animation
) {
  startBottom = view.getBottom();
}

onStart được gọi khi một ảnh động phần lồng ghép bắt đầu. Bạn có thể dùng đối tượng này để đặt tất cả các thuộc tính khung hiển thị thành trạng thái cuối của các thay đổi về bố cục. Nếu bạn đã đặt lệnh gọi lại OnApplyWindowInsetsListener cho bất kỳ khung hiển thị nào, thì lệnh gọi lại đó sẽ được gọi tại thời điểm này. Đây là thời điểm thích hợp để lưu trạng thái cuối cùng của các thuộc tính khung hiển thị.

Hình ảnh minh hoạ toạ độ dưới cùng của trạng thái cuối cùng của khung hiển thị
Hình 4. Sử dụng onStart() để ghi lại trạng thái cuối.

Đoạn mã sau đây cho thấy một lệnh gọi mẫu đến onStart:

Kotlin

var endBottom = 0f

override fun onStart(
  animation: WindowInsetsAnimationCompat,
  bounds: WindowInsetsAnimationCompat.BoundsCompat
): WindowInsetsAnimationCompat.BoundsCompat {
  // Record the position of the view after the IME transition.
  endBottom = view.bottom.toFloat()

  return bounds
}

Java

float endBottom;

@NonNull
@Override
public WindowInsetsAnimationCompat.BoundsCompat onStart(
    @NonNull WindowInsetsAnimationCompat animation,
    @NonNull WindowInsetsAnimationCompat.BoundsCompat bounds
) {
  endBottom = view.getBottom();
  return bounds;
}

onProgress được gọi khi phần lồng ghép thay đổi trong quá trình chạy ảnh động, vì vậy, bạn có thể ghi đè phần này và được thông báo trên mọi khung hình trong quá trình tạo ảnh động cho bàn phím. Cập nhật các thuộc tính của khung hiển thị để khung hiển thị chuyển động đồng bộ với bàn phím.

Đến đây là hoàn tất tất cả các thay đổi về bố cục. Ví dụ: nếu bạn dùng View.translationY để dịch chuyển khung hiển thị, giá trị sẽ giảm dần cho mỗi lần gọi phương thức này và cuối cùng đạt đến 0 cho vị trí bố cục ban đầu.

Hình 5. Sử dụng onProgress() để đồng bộ hoá các ảnh động.

Đoạn mã sau đây cho thấy một lệnh gọi mẫu đến onProgress:

Kotlin

override fun onProgress(
  insets: WindowInsetsCompat,
  runningAnimations: MutableList<WindowInsetsAnimationCompat>
): WindowInsetsCompat {
  // Find an IME animation.
  val imeAnimation = runningAnimations.find {
    it.typeMask and WindowInsetsCompat.Type.ime() != 0
  } ?: return insets

  // Offset the view based on the interpolated fraction of the IME animation.
  view.translationY =
    (startBottom - endBottom) * (1 - imeAnimation.interpolatedFraction)

  return insets
}

Java

@NonNull
@Override
public WindowInsetsCompat onProgress(
    @NonNull WindowInsetsCompat insets,
    @NonNull List<WindowInsetsAnimationCompat> runningAnimations
) {
  // Find an IME animation.
  WindowInsetsAnimationCompat imeAnimation = null;
  for (WindowInsetsAnimationCompat animation : runningAnimations) {
    if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
      imeAnimation = animation;
      break;
    }
  }
  if (imeAnimation != null) {
    // Offset the view based on the interpolated fraction of the IME animation.
    view.setTranslationY((startBottom - endBottom)

        *   (1 - imeAnimation.getInterpolatedFraction()));
  }
  return insets;
}

Bạn có thể ghi đè onEnd (không bắt buộc). Phương thức này được gọi sau khi ảnh động kết thúc. Đây là thời điểm thích hợp để xoá mọi thay đổi tạm thời.

Tài nguyên khác