控制螢幕鍵盤並製作動畫

試試 Compose
Jetpack Compose 是 Android 推薦的 UI 工具包。瞭解如何在 Compose 中使用鍵盤。

使用 WindowInsetsCompat,應用程式可以查詢及控制螢幕小鍵盤 (也稱為 IME),與系統資訊列的互動方式類似。應用程式也可以使用 WindowInsetsAnimationCompat ,在軟體鍵盤開啟或關閉時建立無縫轉場效果。

圖 1. 軟體鍵盤開啟/關閉轉換的兩個範例。

必要條件

設定軟體鍵盤的控制項和動畫前,請先將應用程式設定為無邊框模式。這樣一來,系統就能處理系統視窗插邊,例如系統資訊列和螢幕小鍵盤。

預設同步插邊動畫

從 API 級別 37.2 以上版本開始,使用預設視窗版面配置調整項目的應用程式 (例如使用 WindowInsetsController.show(WindowInsets.Type.ime()) 顯示 IME 時的 SOFT_INPUT_ADJUST_RESIZESOFT_INPUT_ADJUST_PAN),不必實作自訂 WindowInsetsAnimationCompat.Callback,就能享有系統層級的逐格動畫同步功能。啟用後,這項功能會自動在每個影格上套用插邊,並觸發版面配置傳遞,在鍵盤移動時建立平滑的轉場效果。

如要啟用這項行為,請在 AndroidManifest.xml 檔案中,將下列 <property> 標記新增至活動或應用程式:

<property
    android:name="android.window.PROPERTY_COMPAT_ALLOW_SYNCHRONIZED_INSETS_ANIMATION"
    android:value="true" />

在主執行緒外執行插邊動畫

根據預設,註冊 WindowInsetsAnimationCompat.Callback 會強制在應用程式的主執行緒上執行視窗插邊動畫。如果應用程式在動畫期間持續更新及同步處理檢視區塊版面配置 (例如在 onProgress 方法中),就必須這麼做。不過,如果應用程式只需要監聽生命週期轉換 (例如 IME 開始或結束動畫時),不需要逐一更新檢視區塊影格,在主執行緒上執行可能會造成不必要的負擔和動畫延遲,特別是當主執行緒忙碌時。

從 API 37.2 以上版本開始,您可以使用 ViewTreeObserver.WindowInsetsAnimationListener 觀察這些轉場效果。由於這個接聽程式不會接收逐格進度更新 (沒有 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 中標示為「已同步」的範例所示,這樣看起來會更平滑。

WindowInsetsAnimatorCompat

設定 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。您可以使用這項功能,將所有檢視區塊屬性設為版面配置變更的最終狀態。如果您已為任何檢視區塊設定 OnApplyWindowInsetsListener 回呼,系統此時就會呼叫該回呼。建議您現在儲存檢視區塊屬性的最終狀態。

圖片:顯示檢視區塊的結束狀態底部座標
圖 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。動畫結束後,系統會呼叫這個方法。現在是清除所有暫時變更的好時機。

其他資源