WebView のウィンドウ インセットについて

WebView は、レイアウト ビューポート (ページサイズ)と ビジュアル ビューポート(ユーザーが 実際に表示するページの部分)の 2 つのビューポートを使用してコンテンツの配置を管理します。レイアウト ビューポートは通常静的ですが、ユーザーがズームやスクロールを行った場合、またはシステム UI 要素(ソフトウェア キーボードなど)が表示された場合は、ビジュアル ビューポートが動的に変化します。

機能の互換性

ウィンドウ インセットの WebView サポートは、ウェブ コンテンツの動作をネイティブ Android アプリの期待どおりにするために、時間の経過とともに進化してきました。

Milestone 追加された機能 範囲
M136 CSS safe-area-insets による displayCutout()systemBars() のサポート。 全画面表示の WebView のみ。
M139 ビジュアル ビューポートのサイズ変更による ime()(インプット メソッド エディタ、つまりキーボード)のサポート。 すべての WebView。
M144 displayCutout()systemBars() のサポート。 すべての WebView(全画面表示の状態に関係なく)。

詳細については、WindowInsetsCompat をご覧ください。

コア メカニズム

WebView は、次の 2 つの主要なメカニズムを使用してインセットを処理します。

  • セーフエリア(displayCutoutsystemBars): WebView は、これらのディメンションを CSS safe-area-inset-* 変数を介してウェブ コンテンツに転送します。これにより、デベロッパーは、独自のインタラクティブな要素(ナビゲーション バーなど)がノッチやステータスバーで隠れないようにすることができます。

  • インプット メソッド エディタ(IME)を使用したビジュアル ビューポートのサイズ変更: M139 以降では、インプット メソッド エディタ(IME)がビジュアル ビューポートのサイズを直接変更します。このサイズ変更メカニズムも、WebView とウィンドウの交差に基づいています。たとえば、Android のマルチタスク モードで、WebView の下部がウィンドウの下部より 200dp 下に伸びている場合、ビジュアル ビューポートは WebView のサイズより 200dp 小さくなります。このビジュアル ビューポートのサイズ変更(IME と WebView-Window の交差の両方)は、WebView の下部にのみ適用されます。 このメカニズムでは、左、右、上のオーバーラップのサイズ変更はサポートされていません。つまり、これらのエッジにドッキングされたキーボードが表示されても、ビジュアル ビューポートのサイズ変更はトリガーされません。

以前は、ビジュアル ビューポートは固定されたままで、入力フィールドがキーボードの後ろに隠れてしまうことがよくありました。ビューポートのサイズを変更することで、ページの表示部分がデフォルトでスクロール可能になり、ユーザーは隠れたコンテンツにアクセスできるようになります。

境界とオーバーラップのロジック

WebView は、システム UI 要素(バー、ディスプレイ カットアウト、キーボード)が WebView の画面の境界と直接オーバーラップしている場合にのみ、ゼロ以外のインセット値を受け取る必要があります。WebView がこれらの UI 要素とオーバーラップしていない場合(たとえば、WebView が画面の中央に配置されていて、システムバーに触れていない場合)、これらのインセットはゼロとして受け取る必要があります。

このデフォルトのロジックをオーバーライドして、オーバーラップに関係なく完全なシステム ディメンションをウェブ コンテンツに提供するには、setOnApplyWindowInsetsListener メソッドを使用し、リスナーから変更されていない元の windowInsets オブジェクトを返します。完全なシステム ディメンションを提供することで、WebView の現在の位置に関係なく、ウェブ コンテンツをデバイスのハードウェアに合わせることができるため、デザインの一貫性を確保できます。これにより、WebView が移動したり、画面の端に触れるように拡大したりする際に、スムーズな移行が実現します。

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(myWebView) { _, windowInsets ->
    // By returning the original windowInsets object, we override the default
    // behavior that zeroes out system insets (like system bars or display
    // cutouts) when they don't directly overlap the WebView's screen bounds.
    windowInsets
}

Java

ViewCompat.setOnApplyWindowInsetsListener(myWebView, (v, windowInsets) -> {
  // By returning the original windowInsets object, we override the default
  // behavior that zeroes out system insets (like system bars or display
  // cutouts) when they don't directly overlap the WebView's screen bounds.
  return windowInsets;
});

サイズ変更イベントを管理する

キーボードの表示によってビジュアル ビューポートのサイズが変更されるようになったため、ウェブコードでサイズ変更イベントが発生する頻度が高くなる可能性があります。デベロッパーは、要素のフォーカスをクリアすることで、コードがこれらのサイズ変更イベントに反応しないようにする必要があります。そうすることで、フォーカスが失われ、キーボードが閉じられるループが発生し、ユーザーが入力できなくなります。

  1. ユーザーが入力要素にフォーカスします。
  2. キーボードが表示され、サイズ変更イベントがトリガーされます。
  3. ウェブサイトのコードは、サイズ変更に応じてフォーカスをクリアします。
  4. フォーカスが失われたため、キーボードが閉じます。

この動作を軽減するには、ウェブ側のリスナーを確認して、ビューポートの変更によって blur() JavaScript 関数やフォーカスをクリアする動作が誤ってトリガーされないようにします。

インセット処理を実装する

WebView のデフォルト設定は、ほとんどのアプリで自動的に機能します。ただし、アプリでカスタム レイアウトを使用している場合(たとえば、ステータスバーやキーボードを考慮して独自のパディングを追加している場合)は、次の方法を使用して、ウェブ コンテンツとネイティブ UI の連携を改善できます。ネイティブ UI がパディング をコンテナに適用する場合は、WindowInsetsに基づいて、パディングが二重にならないように、WebView に到達する前にこれらのインセット を正しく管理する必要があります。

パディングが二重になるのは、ネイティブ レイアウトとウェブ コンテンツが同じインセットのディメンションを適用し、冗長な間隔が生じる場合です。たとえば、ステータスバーが 40 ピクセルのスマートフォンを考えてみましょう。ネイティブ ビューと WebView の両方に 40 ピクセルのインセットが表示されます。両方に 40 ピクセルのパディングが追加されるため、ユーザーには上部に 80 ピクセルのギャップが表示されます。

ゼロにする方法

パディングが二重にならないようにするには、ネイティブ ビューがパディングにインセットのディメンションを使用した後に、新しい WindowInsets オブジェクトで Insets.NONE を使用してそのディメンションをゼロにリセットしてから、変更したオブジェクトをビュー階層の下に渡して WebView に渡す必要があります。

親ビューにパディングを適用する場合は、通常、WindowInsetsCompat.CONSUMED ではなく Insets.NONE を設定してゼロにする方法を使用する必要があります。WindowInsetsCompat.CONSUMED を返すと、特定の状況では機能する可能性があります。ただし、アプリのハンドラがインセットを変更したり、独自のパディングを追加したりすると、問題が発生する可能性があります。ゼロにする方法には、このような制限はありません。

インセットをゼロにしてゴースト パディングを回避する

アプリが以前に消費されていないインセットを渡したときにインセットを消費した場合や、インセットが変更された場合(キーボードが非表示になるなど)、インセットを消費すると、WebView が必要な更新通知を受け取ることができなくなります。これにより、WebView が以前の状態のゴーストパディングを保持する可能性があります(たとえば、キーボードが非表示になった後もキーボードのパディングを保持する)。

次の例は、アプリと WebView の間のインタラクションが中断された状態を示しています。

  1. 初期状態: アプリは最初に、消費されていないインセット(displayCutout()systemBars() など)を WebView に渡します。WebView は、内部でウェブ コンテンツにパディングを適用します。
  2. 状態の変更とエラー: アプリの状態が変化し(キーボードが非表示になるなど)、アプリが WindowInsetsCompat.CONSUMED を返すことで、結果として得られるインセットを処理することを選択した場合。
  3. 通知がブロックされる: インセットを消費すると、Android システムがビュー階層の下に WebView に必要な更新通知を送信できなくなります。
  4. ゴースト パディング: WebView が更新を受け取らないため、以前の状態のパディングが保持され、ゴースト パディングが発生します(たとえば、キーボードが非表示になった後もキーボードのパディングが保持される)。

代わりに、WindowInsetsCompat.Builderを使用して、オブジェクトを子ビューに渡す前に、処理されたタイプを ゼロに設定します。これにより、WebView は、これらの特定のインセットがすでに考慮されていることを認識し、通知がビュー階層の下に継続されるようにします。

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, windowInsets ->
    // 1. Identify the inset types you want to handle natively
    val types = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()

    // 2. Extract the dimensions and apply them as padding to the native container
    val insets = windowInsets.getInsets(types)
    view.setPadding(insets.left, insets.top, insets.right, insets.bottom)

    // 3. Return a new WindowInsets object with the handled types set to NONE (zeroed).
    // This informs the WebView that these areas are already padded, preventing
    // double-padding while still allowing the WebView to update its internal state.
    WindowInsetsCompat.Builder(windowInsets)
        .setInsets(types, Insets.NONE)
        .build()
}

Java

ViewCompat.setOnApplyWindowInsetsListener(rootView, (view, windowInsets) -> {
  // 1. Identify the inset types you want to handle natively
  int types = WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout();

  // 2. Extract the dimensions and apply them as padding to the native container
  Insets insets = windowInsets.getInsets(types);
  rootView.setPadding(insets.left, insets.top, insets.right, insets.bottom);

  // 3. Return a new Insets object with the handled types set to NONE (zeroed).
  // This informs the WebView that these areas are already padded, preventing
  // double-padding while still allowing the WebView to update its internal
  // state.
  return new WindowInsetsCompat.Builder(windowInsets)
    .setInsets(types, Insets.NONE)
    .build();
});

オプトアウト方法

これらの最新の動作を無効にして、従来のビューポート処理に戻すには、次の操作を行います。

  1. インセットをインターセプトする: setOnApplyWindowInsetsListener を使用するか、 onApplyWindowInsetsWebView サブクラスでオーバーライドします。

  2. インセットをクリアする: 最初から消費されたインセットのセット(WindowInsetsCompat.CONSUMED など)を返します。この操作により、インセット通知が WebView にまったく伝播されなくなり、最新のビューポートのサイズ変更が無効になり、WebView が最初のビジュアル ビューポートのサイズを保持するようになります。