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

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

機能の互換性

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

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

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

コア メカニクス

WebView は、主に次の 2 つのメカニズムでインセットを処理します。

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

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

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

境界と重複のロジック

WebView は、システム UI 要素(バー、ディスプレイ カットアウト、キーボード)が WebView の画面境界と直接重なっている場合にのみ、ゼロ以外のインセット値を受け取るべきです。WebView がこれらの UI 要素と重ならない場合(たとえば、WebView が画面の中央に配置され、システムバーに触れない場合)、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 を使用するか、WebView サブクラスで onApplyWindowInsets をオーバーライドします。

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