WindowInsetsCompat を使用すると、アプリはシステムバーを操作するのと同様の方法で、画面キーボード(IME とも呼ばれます)をクエリして制御できます。アプリでは、WindowInsetsAnimationCompat を使用して、ソフトウェア キーボードの開閉時にシームレスなトランジションを作成することもできます。
前提条件
ソフトウェア キーボードの制御とアニメーションを設定する前に、アプリをエッジ ツー エッジで表示するように構成します。これにより、システムバーや画面キーボードなどのシステム ウィンドウ インセットを処理できます。
デフォルトの同期インセット アニメーション
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 の「同期されていない」というラベルの付いた例は、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 は、インセット アニメーションが開始されるとき、およびアニメーションによってビューが再レイアウトされる前に呼び出されます。これを使用して、開始状態(この場合はビューの下部座標)を保存できます。
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 コールバックが設定されている場合は、この時点で呼び出されます。ここで、ビュー プロパティの最終状態を保存することをおすすめします。
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 に達して元のレイアウト位置に戻ります。
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 をオーバーライドできます。このメソッドは、アニメーションの終了後に呼び出されます。このタイミングで一時的な変更をクリーンアップすることをおすすめします。
参考情報
- GitHub の WindowInsetsAnimation をご覧ください。