原理是:利用getWindowVisibleDisplayFrame方法,获取Android窗口可见区域的Rect,这个Rect剔除了状态栏与导航栏,并且在有虚拟键盘遮挡的时候,会剔除这个遮挡区域。
接着,Unity的safeArea也剔除了状态栏与导航栏,且不会剔除虚拟键盘遮挡——那么,safeArea.height - getWindowVisibleDisplayFrame.height,就是虚拟键盘的高度。
public static float GetKeyboardHeight()
{
#if UNITY_ANDROID && !UNITY_EDITOR
using var unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
using var activity = unityPlayerClass.GetStatic<AndroidJavaObject>("currentActivity");
using var window = activity.Call<AndroidJavaObject>("getWindow");
using var decorView = window.Call<AndroidJavaObject>("getDecorView");
using var rect = new AndroidJavaObject("android.graphics.Rect");
decorView.Call("getWindowVisibleDisplayFrame", rect);
return Screen.safeArea.height - rect.Call<int>("height");
#else
return TouchScreenKeyboard.area.height;
#endif
}
在实际中的问题是,虚拟键盘有动画,getWindowVisibleDisplayFrame获取有延迟,所以需要不断调用GetKeyboardHeight(),大概20帧左右,才能获取虚拟键盘高度的变化——于是Android的Java对象,就会反复创建与释放。
一个解决方案是,使用协程,即只在高度变化时才返回(利用IEnumerator的Current,也可以用回调函数),如下:
/// <summary>
/// Waits until the keyboard height is different from the [oldHeight], and return the new height with [IEnumerator.Current].
/// By using Coroutine to reduce the call and dispose of Java objects.
/// </summary>
public static IEnumerator GetKeyboardHeight(float oldHeight)
{
#if UNITY_ANDROID && !UNITY_EDITOR
using var unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
using var activity = unityPlayerClass.GetStatic<AndroidJavaObject>("currentActivity");
using var window = activity.Call<AndroidJavaObject>("getWindow");
using var decorView = window.Call<AndroidJavaObject>("getDecorView");
using var rect = new AndroidJavaObject("android.graphics.Rect");
while (true)
{
decorView.Call("getWindowVisibleDisplayFrame", rect);
var keyboardHeight = Screen.safeArea.height - rect.Call<int>("height");
if (oldHeight != keyboardHeight)
{
yield return keyboardHeight;
yield break;
}
yield return null;
}
#else
yield return TouchScreenKeyboard.area.height;
#endif
}
再给出一个,可以响应虚拟键盘不同状态的版本, 而safeArea.height也可以放到循环检测外面。
/// <summary>
/// Waits until the keyboard height is different from the [oldHeight], and return the new height with [IEnumerator.Current].
/// If [IEnumerator.Current] is 0.0f, the keyboard needs to be closed.
/// By using Coroutine to reduce the call and dispose of Java objects.
/// </summary>
public static IEnumerator GetKeyboardHeight(float oldHeight, TouchScreenKeyboard keyboard)
{
#if UNITY_ANDROID //&& !UNITY_EDITOR
using var unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
using var activity = unityPlayerClass.GetStatic<AndroidJavaObject>("currentActivity");
using var window = activity.Call<AndroidJavaObject>("getWindow");
using var decorView = window.Call<AndroidJavaObject>("getDecorView");
using var rect = new AndroidJavaObject("android.graphics.Rect");
var safeAreaHeight = Screen.safeArea.height;
while (true)
{
switch (keyboard.status)
{
case TouchScreenKeyboard.Status.Visible:
decorView.Call("getWindowVisibleDisplayFrame", rect);
var keyboardHeight = safeAreaHeight - rect.Call<int>("height");
if (oldHeight != keyboardHeight)
{
yield return keyboardHeight;
yield break;
}
break;
case TouchScreenKeyboard.Status.Done:
case TouchScreenKeyboard.Status.Canceled:
case TouchScreenKeyboard.Status.LostFocus:
yield return 0.0f;
yield break;
}
yield return null;
}
#else
yield return TouchScreenKeyboard.area.height;
#endif
}