Sep 10, 2023

Transparent Android Status Bar in Expo

Introduction

So you've got your app to be edge-to-edge but still have that ugly status bar background scrim that refuses to go away? Here's the actual solution.

A Broken Solution

If you've looked this issue up in the past then you've likely stumbled upon answers such as setting FLAG_LAYOUT_NO_LIMITS, but this breaks Samsung One UI's virtual navbars as they have no block height for safe area view's to reference.

// In Activity's onCreate() for instance
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    Window w = getWindow();
    w.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
A continue button being overlapped with the Android virtual navigation buttons

The Solution

  • Tell your app to draw behind the system bars within the Activity onCreate: WindowCompat.setDecorFitsSystemWindows(getWindow(), false)
  • Set the system bar colours to be transparent <item name=”android:statusBarColor”>@android:color/transparent</item>
  • Set androidStatusBar.translucent to false in app.config.js/app.config.js Translucent does not include transparent in Android

Obviously this isn't very Expo friendly, we're editing native Android files here, so let's turn it into a config plugin.

import { ConfigPlugin, withMainActivity, withAndroidStyles, AndroidConfig } from "@expo/config-plugins"
import { addImports } from "@expo/config-plugins/build/android/codeMod"
import { mergeContents } from "@expo/config-plugins/build/utils/generateCode"
import { ExpoConfig } from "@expo/config-types"

const TRANSPARENT_WINDOW_ID = "transparent-window-mainActivity-onCreate"

export const withAndroidTransparentWindow: ConfigPlugin = (config) => {
    config = withAndroidStyles(config, (config) => {
        config.modResults = AndroidConfig.Styles.assignStylesValue(config.modResults, {
            add: true,
            parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(),
            name: "android:statusBarColor",
            value: "@android:color/transparent",
        })

        return config
    })

    return withMainActivity(config, (config) => {
        const isJava = config.modResults.language === "java"
        const LE = isJava ? ";" : ""

        config.modResults.contents = addImports(config.modResults.contents, ["androidx.core.view.WindowCompat", "android.os.Build"], isJava)

        config.modResults.contents = mergeContents({
            src: config.modResults.contents,
            anchor: /(?<=^.*super\.onCreate.*$)/m,
            offset: 1,
            comment: "//",
            tag: TRANSPARENT_WINDOW_ID,
            newSrc: `
                if (Build.VERSION.SDK_INT > 28) {
                    WindowCompat.setDecorFitsSystemWindows(getWindow(), false)${LE}
                }
            `,
        }).contents

        return config
    })
}

export default withAndroidTransparentWindow

This will modify our android folder when running expo prebuild so that we're not in a bare workflow; this config plugin has been written in TypeScript and will therefore need compiling before referencing in the app config.