ทำไมต้องตรวจสอบสถานะคีย์บอร์ด?
- ปรับ 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 | ความแม่นยำ | รองรับ Animation | Complexity |
|---|---|---|---|---|
| GlobalLayoutListener | 1 | ประมาณ (Threshold 15%) | ❌ | Low |
| WindowInsets (Compat) | 20 | แม่นยำ (Pixel Perfect) | ⚠️ (Listener ธรรมดา) | Medium |
| WindowInsetsAnimation.Callback | 30 | แม่นยำ + Real-time | ✅ Full Control | High |
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 อื่นๆ