·9 min read

My AdMob ad covered the game on WebView 133

A bottom banner sat 24 dp too high and covered two game buttons, but only where Android System WebView was older than 140. Capacitor 8 and AdMob plugin 8.1 each made room for the navigation bar, and the fix was one version bump, measured to the pixel.

capacitorandroidadmobgamedev

Contents

I opened ASMR Logo Craft Block on an emulator after a day of edits and two things were wrong. The AdMob banner at the bottom of the screen sat on top of the game: the Hint and Clue buttons were half under it. And the game no longer filled the screen. A strip of light grey showed above it and another below it, and the white status-bar icons had vanished against the grey.

I assumed my last few commits had broken it. They had not, and finding that out took longer than the fix.

The banner was the part that could not wait. A banner on top of controls a player taps every level is the placement AdMob's policies are written against: it produces accidental clicks, and invalid clicks are how an AdMob account gets limited or suspended. I did not want to wait and find out.

The setup

  • Capacitor 8 (@capacitor/android 8.5.2), with the game drawn edge to edge under the system bars
  • @capacitor-community/admob 8.1.0, one anchored banner at BOTTOM_CENTER, size ADAPTIVE_BANNER
  • Two Android 16 emulators (API 36), both Pixel 9 Pro profiles, running the same debug APK

The game gets its ads from a shared package that every one of my games uses (the arrangement is described in One engine, many games). That package listens for the banner's size and tells the page how much room to leave at the bottom.

The false lead: my own commits

The changes that day included a new version of the shared ads package and a Gradle rule that forced a newer hsdp library under Google's ads SDK. Both looked guilty. Neither was:

  • the ads JavaScript was byte-identical between the two package versions
  • the Gradle rule pulled in the same AndroidX libraries as before
  • the other uncommitted files touched the economy and cloud saves, not layout

What gave it away was the second emulator. Same APK, same Android version, and the game drew edge to edge with the banner exactly where it belonged. So I asked both what WebView they had:

== emulator-5554 sdk=36
  Current WebView package (name, version): (com.google.android.webview, 151.0.7922.199)
== emulator-5556 sdk=36
  Current WebView package (name, version): (com.google.android.webview, 133.0.6943.137)

The good one had been updated by the Play Store. The broken one still carried the WebView its system image shipped with.

Why WebView 140 matters to Capacitor 8

Capacitor 8's SystemBars plugin decides at runtime whether the page may draw under the status and navigation bars. Line 194 of SystemBars.java:

boolean shouldPassthroughInsets =
    getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;

WEBVIEW_VERSION_WITH_SAFE_AREA_FIX is 140. On 140 or newer, the page gets the full window and real env(safe-area-inset-*) values to lay itself out with. Below 140, Capacitor pads the whole view by the bar heights instead and reports every inset to the page as 0.

That padding is where the grey strips came from: the window behind the WebView, in the theme's default light grey, showing above and below it. With white status-bar icons, the icons disappear. That half is cosmetic and has a cosmetic fix, below.

Why the banner sat 24 dp too high

The AdMob plugin also makes room for the navigation bar. In 8.1.0, BannerExecutor.java lines 133–149:

// set Safe Area only for Android 15+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
    View rootView = activitySupplier.get().getWindow().getDecorView();
    rootView.setOnApplyWindowInsetsListener((v, insets) -> {
        int bottomInset = insets.getSystemWindowInsetBottom();
        int topInset = insets.getSystemWindowInsetTop();

        if ("TOP_CENTER".equals(adOptions.position)) {
            mAdViewLayoutParams.setMargins(margins[0], margins[1] + topInset, margins[2], margins[3]);
        } else {
            mAdViewLayoutParams.setMargins(margins[0], margins[1], margins[2], margins[3] + bottomInset);
        }

        mAdViewLayout.setLayoutParams(mAdViewLayoutParams);
        return insets;
    });
}

It reads the raw window inset and lifts the banner by it. On a new WebView that is correct, because the page also extends under the navigation bar and reserves the same amount. On an old one, Capacitor has already lifted everything by the navigation bar, so the banner is lifted twice.

I read the page's own numbers over the WebView debugger on the broken emulator:

"--ad-space-bottom=calc(67px + 0px)"
"--safe-area-inset-bottom=0px"
"app": [0, 809]

The page reserved the banner's 67 px and nothing for the bar, because Capacitor told it the bar was 0. The navigation bar on that emulator is 72 physical pixels, 24 dp at 3x. By the code above, that is how far the banner sat over the page, and on screen it was enough to cover Hint and Clue. I measured the fixed layout precisely; the broken one I only saw on screen, so the 24 dp is a calculation rather than a measurement.

Two conditions have to meet for this: Android 15 or newer, where the 8.1 listener runs, and a WebView older than 140. I hit it on an emulator image. I do not know how many real phones are in that state. The Play Store updates the WebView on its own, but I have no data on how many devices are behind.

AdMob ad sizes are not a constant

The 67 px in that output is the other half of the story. Most layout bugs around banners start from the assumption that a banner is 50 dp tall, because that is the size people remember. The plugin's own enum lists the fixed AdMob ad sizes:

Size dp
BANNER 320×50
LARGE_BANNER 320×100
FULL_BANNER 468×60
MEDIUM_RECTANGLE 300×250
LEADERBOARD 728×90
ADAPTIVE_BANNER full width, height chosen by the SDK

ADAPTIVE_BANNER is the plugin's default and the one I use, and its height depends on the screen. On the two emulators in this story:

Emulator Width AdView, physical px Banner height
Pixel 9 Pro AVD, 3x 1280 px 1278×201 67 dp
Android 16 QPR2 AVD, 2.625x 1080 px 1078×168 64 dp

Neither is 50. A page that hard-codes 50 dp of room will be overlapped on both.

The second thing worth knowing is that the banner is a native view laid over the WebView. The WebView keeps its full height and the window never learns that part of it is covered. In Picture Word Connect that showed up as the home screen's button panel running under the banner: the "short screen" rule checked the window's height, which had not changed. The fix there was to lay screens out against the room the page actually has, and let the logo be the one thing allowed to shrink.

So the page has to be told. The plugin fires bannerAdSizeChanged with the real height, and the shared package turns it into a CSS variable the layout reads:

AdMob.addListener('bannerAdSizeChanged', ({ height }) => {
  document.documentElement.style.setProperty(
    '--ad-space-bottom',
    `calc(${height}px + env(safe-area-inset-bottom, 0px))`,
  );
});
.app { position: fixed; top: 0; left: 0; right: 0; bottom: var(--ad-space-bottom, 0px); }

That part was right all along. It reserved what the plugin said the banner took plus what Capacitor said the bar took. The plugin just disagreed with Capacitor about the bar.

The fix: AdMob plugin 8.2.1

@capacitor-community/admob 8.2.0 and 8.2.1 had been published two days earlier. 8.2.1 moves the listener off the decor view and onto the banner's own layout, and it reads the insets left after Capacitor has dealt with them. Its comment says it plainly:

// Use the insets remaining after Capacitor's parent layout has handled them.
// Replacing the decor-view listener would override SystemBars' safe-area handling.

On WebView 140 and newer, that leaves the navigation-bar inset in place, and the banner sits above the bar exactly as before. Below 140, Capacitor has already consumed it, the margin is 0, and the page's calc(67px + 0px) now matches. I read the 8.2.1 diff, not 8.2.0, so I can't say whether 8.2.0 alone was enough.

What changed:

  • the game: @capacitor-community/admob from ^8.0.0 to ^8.2.1
  • the shared ads package: its peer range raised to >=8.2.1 <9.0.0, so no game can install it next to an older plugin and get the overlap back
  • the Android theme: a window background in the game's own colour, so the strips an old WebView leaves are purple like the game rather than light grey, and the white icons read again
<!-- Behind the system bars where the WebView cannot draw under them:
     before WebView 140, Capacitor lays the page out between the bars. -->
<item name="android:windowBackground">@color/window_background</item>

Measuring the fix

Rather than trust a screenshot, I compared where the page ends with where the native banner starts. The page side comes from the WebView's DevTools socket, which a debug build exposes:

PID=$(adb shell pidof <package>)
adb forward tcp:9333 localabstract:webview_devtools_remote_$PID

A small script then sends Runtime.evaluate to read .app's bounding box and devicePixelRatio. The native side is adb shell dumpsys activity top, which lists every view with its bounds. On WebView 133, after the update:

com.getcapacitor.CapacitorWebView{… 0,0-1280,2628 …}
android.widget.RelativeLayout{… 0,2427-1278,2628}
com.google.android.gms.ads.AdView{… 0,0-1278,201}

The page's .app ended at 809 CSS px, and 809 × 3 = 2427, the exact row where the banner's layout begins. It held on the home screen, on the board, and after an interstitial had come and gone.

On a WebView 151 emulator the page reported calc(64px + 24px) for the bottom, .app ended at 826.29 CSS px at a ratio of 2.625, which is pixel 2169, and the banner's layout ran from 2169 to 2337, where the navigation bar starts. Same fit, with the bar under the banner instead of under the page.

Getting that second measurement was the slow part. My Play-updated emulator was signed in to an account that owns the game's No Ads purchase, and the game restored it, so there was no banner to measure. A fresh Android 16 QPR2 image (android-36.1, Google APIs, no Play account) fixed the purchase problem. I had expected an image that new to ship a WebView of 140 or later; it came with 134. I pulled the WebView APK, 268 MB, off the Play-updated emulator and installed it on the new one:

adb install -r base.apk
adb shell cmd webviewupdate set-webview-implementation com.google.android.webview

dumpsys webviewupdate then reported 151.0.7922.199. I kept that emulator: it is the only one I have that shows a real banner on a current WebView without a purchase in the way.

Takeaway

When a banner layout bug shows up on one device and not another, check the WebView version before you check your commits. adb shell dumpsys webviewupdate takes a second. With Capacitor 8, 140 is the line where the page's insets change meaning, and every piece of native code that also handles insets has to agree with it.

Never assume a banner's height. Read it from the SDK's size event and leave that much room.

And measure the overlap rather than looking for it. A banner 24 dp too high is easy to miss in a screenshot of a busy game screen. The numbers either meet at the same pixel or they don't.

More posts

New writing when there is something worth saying. By email or by RSS, whichever you prefer.