เมื่อใช้ WindowInsetsCompat แอปจะค้นหาและควบคุมแป้นพิมพ์บนหน้าจอ (หรือที่เรียกว่า IME) ได้คล้ายกับวิธีที่แอปโต้ตอบกับแถบระบบ แอปของคุณยังใช้ WindowInsetsAnimationCompat เพื่อสร้างการเปลี่ยนที่ราบรื่นเมื่อเปิดหรือปิดแป้นพิมพ์เสมือนได้ด้วย
สิ่งที่ต้องมีก่อน
ก่อนตั้งค่าการควบคุมและภาพเคลื่อนไหวสำหรับแป้นพิมพ์เสมือน ให้กำหนดค่าแอปเพื่อแสดงผลแบบไร้ขอบ ซึ่งจะช่วยให้จัดการส่วนแทรกของหน้าต่างระบบ เช่น แถบระบบและแป้นพิมพ์บนหน้าจอได้
ภาพเคลื่อนไหวส่วนแทรกที่ซิงค์เริ่มต้น
ตั้งแต่ระดับ 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 แสดงลักษณะการทำงานเริ่มต้นใน 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 เมื่อภาพเคลื่อนไหวส่วนแทรกเริ่มต้นและก่อนที่จะมีการจัดวางมุมมองใหม่เนื่องจากภาพเคลื่อนไหว คุณใช้เพื่อบันทึกสถานะเริ่มต้นได้
ซึ่งในกรณีนี้คือพิกัดด้านล่างของมุมมอง
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 ดังกล่าว ณ จุดนี้ นี่เป็นเวลาที่เหมาะสมในการบันทึกสถานะสิ้นสุดของพร็อพเพอร์ตี้มุมมอง
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 ไปยังตำแหน่งเลย์เอาต์เดิม
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 ได้ ระบบจะเรียกใช้เมธอดนี้หลังจากภาพเคลื่อนไหวสิ้นสุดลง ตอนนี้เป็นเวลาที่เหมาะสมในการล้างการเปลี่ยนแปลงชั่วคราว
แหล่งข้อมูลเพิ่มเติม
- WindowInsetsAnimation ใน GitHub