ソフトウェア キーボードを制御してアニメーション化する

Compose をお試しください
Jetpack Compose は、Android で推奨される UI ツールキットです。Compose でキーボードを操作する方法について説明します。

WindowInsetsCompat を使用すると、アプリは、システムバーを操作する場合と同様に、画面キーボード(IME とも呼ばれます)をクエリして制御できます。アプリで WindowInsetsAnimationCompat を使用して、ソフトウェア キーボードが開閉されるときにシームレスな遷移を作成することもできます。

図 1. ソフトウェア キーボードの開閉遷移の 2 つの例。

前提条件

ソフトウェア キーボードの制御とアニメーションを設定する前に、エッジ ツー エッジを表示するようにアプリを構成します。これにより、システムバーや画面キーボードなどのシステム ウィンドウ インセットを処理できます。

デフォルトの同期インセット アニメーション

API レベル 37.2 以降では、カスタム WindowInsetsAnimationCompat.Callback を実装せずに、デフォルトのウィンドウ レイアウト調整(WindowInsetsController.show(WindowInsets.Type.ime()) を使用して IME を表示する際の SOFT_INPUT_ADJUST_RESIZE や SOFT_INPUT_ADJUST_PAN など)を使用するアプリは、システムレベルの同期されたフレーム単位のアニメーションを利用できます。この機能を有効にすると、すべてのフレームにインセットが自動的に適用され、レイアウトパスがトリガーされるため、キーボードの移動に合わせてスムーズな遷移が作成されます。

この動作を有効にするには、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 の「同期」の例に示すように、この方がスムーズに見えます。

キーボード アニメーションと同期するビューを使用して 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 をオーバーライドできます。このメソッドは、アニメーションの終了後に呼び出されます。このタイミングで、一時的な変更をクリーンアップすることをおすすめします。

参考情報