ควบคุมและทำให้แป้นพิมพ์ของซอฟต์แวร์เคลื่อนไหว

ลองใช้วิธีแบบ Compose
Jetpack Compose เป็นชุดเครื่องมือ UI ที่แนะนำสำหรับ Android ดูวิธีใช้แป้นพิมพ์ในโหมดเขียน

การใช้ WindowInsetsCompat แอปจะค้นหาและควบคุมแป้นพิมพ์บนหน้าจอ (หรือที่เรียกว่า IME) ได้ในลักษณะเดียวกับ วิธีที่โต้ตอบกับแถบระบบ แอปยังใช้ WindowInsetsAnimationCompat เพื่อสร้างการเปลี่ยนผ่านที่ราบรื่นเมื่อเปิดหรือปิดแป้นพิมพ์ซอฟต์แวร์ได้ด้วย

รูปที่ 1 ตัวอย่าง 2 รายการของคีย์บอร์ดซอฟต์แวร์ การเปลี่ยนจากเปิดเป็นปิด

สิ่งที่ต้องมีก่อน

ก่อนตั้งค่าการควบคุมและภาพเคลื่อนไหวสำหรับแป้นพิมพ์ซอฟต์แวร์ ให้กำหนดค่าแอปเพื่อแสดงแบบขอบจรดขอบ ซึ่งช่วยให้จัดการระยะขอบหน้าต่างของระบบ เช่น แถบระบบและแป้นพิมพ์บนหน้าจอได้

ภาพเคลื่อนไหวการแทรกที่ซิงค์เริ่มต้น

ตั้งแต่ API ระดับ 37.2 ขึ้นไป แอปพลิเคชันที่ใช้การปรับเลย์เอาต์หน้าต่างเริ่มต้น (เช่น SOFT_INPUT_ADJUST_RESIZE หรือ SOFT_INPUT_ADJUST_PAN เมื่อแสดง IME โดยใช้ WindowInsetsController.show(WindowInsets.Type.ime())) โดยไม่ได้ใช้ WindowInsetsAnimationCompat.Callback ที่กำหนดเองจะได้รับประโยชน์ จากภาพเคลื่อนไหวแบบเฟรมต่อเฟรมที่ซิงค์ระดับระบบ เมื่อเปิดใช้ ฟีเจอร์นี้จะใช้ระยะขอบด้านในกับทุกเฟรมโดยอัตโนมัติและทริกเกอร์การส่งผ่านเลย์เอาต์ ซึ่งจะสร้างการเปลี่ยนผ่านที่ราบรื่นเมื่อแป้นพิมพ์เคลื่อนที่

หากต้องการเปิดใช้ลักษณะการทำงานนี้ ให้เพิ่มแท็ก <property> ต่อไปนี้ลงในกิจกรรมหรือ แอปพลิเคชันในไฟล์ AndroidManifest.xml

<property
    android:name="android.window.PROPERTY_COMPAT_ALLOW_SYNCHRONIZED_INSETS_ANIMATION"
    android:value="true" />

เรียกใช้ภาพเคลื่อนไหวของ Insets นอกเทรดหลัก

โดยค่าเริ่มต้น การลงทะเบียน WindowInsetsAnimationCompat.Callback จะบังคับให้ ภาพเคลื่อนไหวของระยะขอบด้านในของหน้าต่างทำงานในเทรดหลักของแอป ซึ่งจำเป็นหากแอปของคุณ อัปเดตและซิงโครไนซ์เลย์เอาต์มุมมองอย่างต่อเนื่อง ระหว่างภาพเคลื่อนไหว (เช่น ในเมธอด onProgress) อย่างไรก็ตาม หากแอปของคุณ ต้องการเพียงแค่ฟังการเปลี่ยนสถานะวงจร (เช่น เมื่อ IME เริ่มหรือ สิ้นสุดการเคลื่อนไหว) และไม่จำเป็นต้องอัปเดตมุมมองทีละเฟรม การทำงานใน เทรดหลักอาจทำให้เกิดค่าใช้จ่ายที่ไม่จำเป็นและการกระตุกของภาพเคลื่อนไหว โดยเฉพาะอย่างยิ่งหาก เทรดหลักทำงานอื่นอยู่

ตั้งแต่ API ระดับ 37.2 ขึ้นไป คุณสามารถใช้ ViewTreeObserver.WindowInsetsAnimationListener เพื่อสังเกตการเปลี่ยนผ่านเหล่านี้แทนได้ เนื่องจาก Listener นี้ไม่ได้รับการอัปเดตความคืบหน้าแบบทีละเฟรม (ไม่มีการเรียกกลับ onProgress) ระบบจึงเรียกใช้ภาพเคลื่อนไหวขอบในในเทรดภาพเคลื่อนไหวเฉพาะแทนที่จะเป็นเทรดหลักได้ ซึ่งจะทำให้การเปลี่ยนผ่านราบรื่นยิ่งขึ้น

Kotlin

val listener = object : ViewTreeObserver.WindowInsetsAnimationListener {
  override fun onPrepare(animation: WindowInsetsAnimation) {
    // Handle preparation before animation starts
  }

  override fun onEnd(animation: WindowInsetsAnimation) {
    // Clean up temporary changes
  }
}

// Add the listener
view.viewTreeObserver.addWindowInsetsAnimationListener(listener)

// Remove the listener when no longer needed
view.viewTreeObserver.removeWindowInsetsAnimationListener(listener)

Java

ViewTreeObserver.WindowInsetsAnimationListener listener =
    new ViewTreeObserver.WindowInsetsAnimationListener() {
      @Override
      public void onPrepare(@NonNull WindowInsetsAnimation animation) {
        // Handle preparation before animation starts
      }

      @Override
      public void onEnd(@NonNull WindowInsetsAnimation animation) {
        // Clean up temporary changes
      }
    };

// Add the listener
view.getViewTreeObserver().addWindowInsetsAnimationListener(listener);

// Remove the listener when no longer needed
view.getViewTreeObserver().removeWindowInsetsAnimationListener(listener);

ตรวจสอบระดับการมองเห็นซอฟต์แวร์แป้นพิมพ์

ใช้ WindowInsets เพื่อตรวจสอบระดับการมองเห็นของคีย์บอร์ด ซอฟต์แวร์

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;

หรือคุณจะใช้ ViewCompat.setOnApplyWindowInsetsListener เพื่อสังเกตการเปลี่ยนแปลงระดับการมองเห็นของแป้นพิมพ์ซอฟต์แวร์ก็ได้

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

ซิงค์ภาพเคลื่อนไหวกับแป้นพิมพ์ซอฟต์แวร์

เมื่อผู้ใช้แตะช่องป้อนข้อความ แป้นพิมพ์จะเลื่อนจากด้านล่างของหน้าจอมายังตำแหน่งที่ต้องการ ดังที่แสดงในตัวอย่างต่อไปนี้

รูปที่ 2 ภาพเคลื่อนไหวของแป้นพิมพ์ที่ซิงค์กัน
  • ตัวอย่างที่ติดป้ายกำกับว่า "ไม่ได้ซิงค์" ในรูปที่ 2 แสดงลักษณะการทำงานเริ่มต้น ใน Android 10 (API ระดับ 29) ซึ่งช่องข้อความและเนื้อหาของแอป จะเลื่อนเข้าที่แทนที่จะซิงค์กับภาพเคลื่อนไหวของแป้นพิมพ์ ซึ่งเป็นลักษณะการทำงานที่อาจทำให้เกิดความไม่สอดคล้องกันทางภาพ

  • ใน Android 11 (API ระดับ 30) ขึ้นไป คุณสามารถใช้ WindowInsetsAnimationCompat เพื่อซิงค์การเปลี่ยนผ่านของแอปกับ การเลื่อนแป้นพิมพ์ขึ้นและลงจากด้านล่างของหน้าจอ ซึ่งจะดู ราบรื่นขึ้น ดังตัวอย่างที่มีป้ายกำกับว่า "ซิงค์แล้ว" ในรูปที่ 2

กำหนดค่า WindowInsetsAnimationCompat.Callback โดยให้มุมมองซิงค์กับภาพเคลื่อนไหวของแป้นพิมพ์

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.
    });

มีหลายวิธีในการลบล้างใน WindowInsetsAnimationCompat.Callback กล่าวคือ onPrepare() onStart() onProgress() และ onEnd() เริ่มด้วยการเรียกใช้ onPrepare() ก่อนที่จะมีการเปลี่ยนแปลงเลย์เอาต์

onPrepare จะเรียกใช้เมื่อภาพเคลื่อนไหวของ Insets เริ่มต้นและก่อนที่จะมีการจัดวางมุมมองใหม่เนื่องจากภาพเคลื่อนไหว คุณใช้เพื่อบันทึกสถานะเริ่มต้นได้ ซึ่งในกรณีนี้คือพิกัดล่างของวิว

รูปภาพที่แสดงพิกัดด้านล่างของสถานะเริ่มต้นของมุมมองรูท
รูปที่ 3 ใช้ onPrepare() เพื่อ บันทึกสถานะเริ่มต้น

ข้อมูลโค้ดต่อไปนี้แสดงตัวอย่างการเรียกใช้ 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 จะเรียกใช้เมื่อภาพเคลื่อนไหวของระยะขอบเริ่ม คุณใช้เพื่อตั้งค่าพร็อพเพอร์ตี้มุมมองทั้งหมดเป็นสถานะสุดท้ายของการเปลี่ยนแปลงเลย์เอาต์ได้ หากคุณตั้งค่าการเรียกกลับ OnApplyWindowInsetsListenerไว้ในมุมมองใดก็ตาม ระบบจะเรียกใช้ ที่จุดนี้ ตอนนี้เป็นเวลาที่เหมาะในการบันทึกสถานะสุดท้ายของพร็อพเพอร์ตี้มุมมอง

รูปภาพแสดงพิกัดด้านล่างของมุมมองในสถานะสิ้นสุด
รูปที่ 4 การใช้ onStart() เพื่อบันทึก สถานะสุดท้าย

ข้อมูลโค้ดต่อไปนี้แสดงตัวอย่างการเรียกใช้ 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 จะเรียกใช้เมื่อขอบเปลี่ยนเป็นส่วนหนึ่งของการเรียกใช้ภาพเคลื่อนไหว เพื่อให้คุณลบล้างและรับการแจ้งเตือนในทุกเฟรมระหว่างภาพเคลื่อนไหวของคีย์บอร์ดได้ อัปเดตพร็อพเพอร์ตี้ของมุมมองเพื่อให้มุมมองเคลื่อนไหว พร้อมกับแป้นพิมพ์

ตอนนี้การเปลี่ยนแปลงเลย์เอาต์ทั้งหมดเสร็จสมบูรณ์แล้ว เช่น หากคุณใช้ View.translationY เพื่อเปลี่ยนมุมมอง ค่าจะค่อยๆ ลดลงทุกครั้งที่มีการเรียกใช้เมธอดนี้ และในที่สุดจะไปถึง 0 ที่ตำแหน่งเลย์เอาต์เดิม

รูปที่ 5 การใช้ onProgress() เพื่อ ซิงค์ภาพเคลื่อนไหว

ข้อมูลโค้ดต่อไปนี้แสดงตัวอย่างการเรียกใช้ 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;
}

คุณเลือกที่จะลบล้าง onEnd ได้ ระบบจะเรียกใช้เมธอดนี้หลังจากภาพเคลื่อนไหว สิ้นสุดลง ตอนนี้เป็นเวลาที่เหมาะในการล้างการเปลี่ยนแปลงชั่วคราว

แหล่งข้อมูลเพิ่มเติม