Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

ตรวจสอบสถานะคีย์บอร์ด (แสดง/ซ่อน) ใน Android


ทำไมต้องตรวจสอบสถานะคีย์บอร์ด?

  • ปรับ Layout ให้เหมาะสม (เช่น เลื่อนปุ่มขึ้นเมื่อคีย์บอร์ดขึ้น)
  • Track User Behavior (Analytics)
  • Validation: ตรวจสอบก่อน Submit Form
  • Custom Input Handling

วิธีที่ 1: ViewTreeObserver.OnGlobalLayoutListener (Classic, ทุก API)

หลักการ: เปรียบเทียบ Root View Height กับ Visible Display Frame

public class KeyboardUtils {
    public interface OnKeyboardVisibilityListener {
        void onKeyboardVisibilityChanged(boolean isVisible, int keyboardHeight);
    }

    public static void registerKeyboardVisibilityListener(Activity activity, OnKeyboardVisibilityListener listener) {
        final View decorView = activity.getWindow().getDecorView();
        decorView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            private int previousHeight = -1;

            @Override
            public void onGlobalLayout() {
                Rect rect = new Rect();
                decorView.getWindowVisibleDisplayFrame(rect);
                int screenHeight = decorView.getHeight();
                int visibleHeight = rect.bottom - rect.top;
                int keyboardHeight = screenHeight - visibleHeight;

                // Threshold: คีย์บอร์ดสูงกว่า 15-20% ของหน้าจอ = แสดง
                boolean isVisible = keyboardHeight > screenHeight * 0.15;

                // ป้องกัน Callback ซ้ำ (Height ไม่เปลี่ยน)
                if (keyboardHeight != previousHeight) {
                    previousHeight = keyboardHeight;
                    listener.onKeyboardVisibilityChanged(isVisible, keyboardHeight);
                }
            }
        });
    }

    public static void unregisterKeyboardVisibilityListener(Activity activity) {
        activity.getWindow().getDecorView().getViewTreeObserver()
            .removeOnGlobalLayoutListener(listener); // ต้องเก็บ Reference Listener ไว้
    }
}

ใช้งานใน Activity

public class MainActivity extends AppCompatActivity {
    private KeyboardUtils.OnKeyboardVisibilityListener keyboardListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        keyboardListener = (isVisible, height) -> {
            runOnUiThread(() -> {
                if (isVisible) {
                    Log.d("Keyboard", "Keyboard shown, height: " + height);
                    // ปรับ UI: เลื่อนปุ่มขึ้น, Resize View, etc.
                } else {
                    Log.d("Keyboard", "Keyboard hidden");
                    // Reset UI
                }
            });
        };
        KeyboardUtils.registerKeyboardVisibilityListener(this, keyboardListener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        KeyboardUtils.unregisterKeyboardVisibilityListener(this);
    }
}

วิธีที่ 2: WindowInsets (API 20+, แนะนำสำหรับ Modern Android)

ใช้ WindowInsetsCompat และ WindowInsetsAnimation ได้ข้อมูลแม่นยำกว่า รองรับ Animation

Dependency

implementation "androidx.core:core:1.12.0"
implementation "androidx.window:window:1.2.0"

Implementation

import android.view.View;
import android.view.WindowInsets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class KeyboardInsetsListener {
    public interface OnKeyboardChangeListener {
        void onKeyboardShown(int height);
        void onKeyboardHidden();
    }

    public static void attach(View rootView, OnKeyboardChangeListener listener) {
        ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
            WindowInsetsCompat imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
            int keyboardHeight = imeInsets.bottom; // สูงของคีย์บอร์ด (0 ถ้าซ่อน)

            if (keyboardHeight > 0) {
                listener.onKeyboardShown(keyboardHeight);
            } else {
                listener.onKeyboardHidden();
            }
            return insets;
        });
    }
}

ใช้งาน

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    View rootView = findViewById(android.R.id.content); // หรือ CoordinatorLayout
    
    KeyboardInsetsListener.attach(rootView, new KeyboardInsetsListener.OnKeyboardChangeListener() {
        @Override
        public void onKeyboardShown(int height) {
            // Keyboard visible with height
            adjustLayoutForKeyboard(height);
        }

        @Override
        public void onKeyboardHidden() {
            // Keyboard hidden
            resetLayout();
        }
    });
}

วิธีที่ 3: WindowInsetsAnimation.Callback (API 30+ - รองรับ Animation)

รู้สถานะระหว่าง Animation (ขึ้น/ลง)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    rootView.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(
        WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP) {
        @Override
        public void onPrepare(WindowInsetsAnimation animation) {
            // เริ่ม Animation
        }

        @Override
        public WindowInsets onProgress(WindowInsets insets, List runningAnimations) {
            // ระหว่าง Animation - ได้ Insets แบบ Real-time
            WindowInsetsCompat compat = WindowInsetsCompat.toWindowInsetsCompat(insets, rootView);
            int imeHeight = compat.getInsets(WindowInsetsCompat.Type.ime()).bottom;
            // Update UI Real-time
            return insets;
        }

        @Override
        public void onEnd(WindowInsetsAnimation animation) {
            // Animation จบ
        }
    });
}

ตัวอย่างครบถ้วน: ปรับ Layout ตามคีย์บอร์ด

Layout (activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="https://schemas.android.com/apk/res/android"
    android:id="@+id/rootLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/etMessage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="พิมพ์ข้อความ..."
        android:layout_margin="16dp" />

    <Button
        android:id="@+id/btnSend"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        android:text="ส่ง" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

MainActivity.java (ใช้ WindowInsets - Modern)

import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {
    private EditText etMessage;
    private Button btnSend;
    private View rootLayout;
    private int originalBottomMargin;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        etMessage = findViewById(R.id.etMessage);
        btnSend = findViewById(R.id.btnSend);
        rootLayout = findViewById(R.id.rootLayout);

        // เก็บ Margin ต้นฉบับ
        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) btnSend.getLayoutParams();
        originalBottomMargin = params.bottomMargin;

        // WindowInsets Listener
        ViewCompat.setOnApplyWindowInsetsListener(rootLayout, (v, insets) -> {
            WindowInsetsCompat imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
            int keyboardHeight = imeInsets.bottom;

            CoordinatorLayout.LayoutParams btnParams = (CoordinatorLayout.LayoutParams) btnSend.getLayoutParams();
            if (keyboardHeight > 0) {
                // คีย์บอร์ดขึ้น: เพิ่ม Margin Bottom ให้ปุ่มอยู่เหนือคีย์บอร์ด
                btnParams.bottomMargin = keyboardHeight + 16; // +16dp padding
            } else {
                // คีย์บอร์ดลง: คืนค่าเดิม
                btnParams.bottomMargin = originalBottomMargin;
            }
            btnSend.setLayoutParams(btnParams);
            
            return insets;
        });
    }
}

เปรียบเทียบวิธีต่างๆ

วิธีAPI Minความแม่นยำรองรับ AnimationComplexity
GlobalLayoutListener1ประมาณ (Threshold 15%)❌Low
WindowInsets (Compat)20แม่นยำ (Pixel Perfect)⚠️ (Listener ธรรมดา)Medium
WindowInsetsAnimation.Callback30แม่นยำ + Real-time✅ Full ControlHigh

Best Practices

  • ใช้ WindowInsetsCompat สำหรับโปรเจกต์ใหม่ (API 21+)
  • Listener ใน onCreate, Unregister ใน onDestroy (GlobalLayoutListener)
  • Threshold 15-20% สำหรับ GlobalLayoutListener (กัน False Positive จาก Navigation Bar)
  • Test บนอุปกรณ์จริง Emulator บางตัวรายงานความสูงผิด
  • Navigation Bar + Keyboard: ใช้ WindowInsetsCompat.Type.ime() | WindowInsetsCompat.Type.navigationBars()
  • adjustResize ใน Manifest ทำให้ Layout ปรับอัตโนมัติ (แต่ไม่ได้ Callback)

สรุป

  • Legacy/Simple: ViewTreeObserver.OnGlobalLayoutListener + Threshold
  • Modern (API 20+): ViewCompat.setOnApplyWindowInsetsListener + WindowInsetsCompat.Type.ime()
  • Advanced Animation (API 30+): WindowInsetsAnimation.Callback
  • ข้อมูลที่ได้: keyboardHeight (px), isVisible (boolean)
  • ใช้ปรับ UI: Margin, Padding, Translation, Visibility ของ View อื่นๆ