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

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

機能の互換性

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

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

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

コアメカニクス

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

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

  • インプット メソッド エディタ(IME)を使用したビジュアル ビューポートのサイズ変更: M139 以降、インプット メソッド エディタ(IME)はビジュアル ビューポートのサイズを直接変更します。このサイズ変更メカニズムも、WebView とウィンドウの交差に基づいています。たとえば、Android のマルチタスク モードで、WebView の下部がウィンドウの下部から 200 dp 伸びている場合、ビジュアル ビューポートは WebView のサイズより 200 dp 小さくなります。このビジュアル ビューポートのサイズ変更(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 ピクセルのギャップが表示されます。

ゼロ化アプローチ

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

親ビューにパディングを適用する場合は、通常、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 は初期のビジュアル ビューポート サイズを保持するようになります。