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

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

เมื่อใช้ 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" />

แสดงภาพเคลื่อนไหวของส่วนแทรกนอกเทรดหลัก

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

ตั้งแต่ระดับ API 37.2 ขึ้นไป คุณสามารถใช้ ViewTreeObserver.WindowInsetsAnimationListener เพื่อสังเกตการเปลี่ยนสถานะเหล่านี้แทนได้ เนื่องจาก Listener นี้ไม่ได้รับการอัปเดตความคืบหน้าแบบเฟรมต่อเฟรม (ไม่มี Callback 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 เมื่อภาพเคลื่อนไหวส่วนแทรกเริ่มต้นและก่อนที่จะมีการจัดวางมุมมองใหม่เนื่องจากภาพเคลื่อนไหว คุณใช้เพื่อบันทึกสถานะเริ่มต้นได้ ซึ่งในกรณีนี้คือพิกัดด้านล่างของมุมมอง

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

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

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