aj3423
aj3423

Reputation: 2611

How to change the status bar color in Android 15+?

For Android 14 or below, it can be done with: window.statusBarColor = Color.RED, after upgrading to Android 15, this is deprecated and doesn't work.

In Android Studio, it recommends to use:

Draw proper background behind WindowInsets.Type.statusBars() instead.

But I can't find any sample about how to use the WindowInsets to change the statusbar color.

Please note that I don't want to set the color that based on the light/dark theme, I just want to use custom color like RED/GREEN.

Upvotes: 22

Views: 12373

Answers (10)

Shreyash Pattewar
Shreyash Pattewar

Reputation: 939

In Android 15 (API level 35+), directly setting the status bar color using window.statusBarColor is deprecated. Instead, you are encouraged to draw the background color for the status bar using the WindowInsets API. Here's how you can achieve this:


Code Example

import android.graphics.Color
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowInsets

fun setStatusBarColor(window: Window, color: Int) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { // Android 15+
        window.decorView.setOnApplyWindowInsetsListener { view, insets ->
            val statusBarInsets = insets.getInsets(WindowInsets.Type.statusBars())
            view.setBackgroundColor(color)

            // Adjust padding to avoid overlap
            view.setPadding(0, statusBarInsets.top, 0, 0)
            insets
        }
    } else {
        // For Android 14 and below
        window.statusBarColor = color
    }
}

Alternative: Using android:windowOptOutEdgeToEdgeEnforcement

If you don't want to handle WindowInsets immediately, you can opt out of edge-to-edge enforcement by adding the following attribute to your AndroidManifest.xml:

<application
    android:windowOptOutEdgeToEdgeEnforcement="true"
    ...>
    ...
</application>

Or, apply it at the activity level:

<activity
    android:name=".YourActivity"
    android:windowOptOutEdgeToEdgeEnforcement="true"
    ...>
</activity>

Why Not Use This Long-Term?

While this method restores the behavior of window.statusBarColor, it is a temporary solution:

  • The android:windowOptOutEdgeToEdgeEnforcement attribute is marked for deprecation in future Android SDK versions.
  • It’s recommended to adopt edge-to-edge design and use WindowInsets to manage the status bar's appearance properly.

Recommendation

Use WindowInsets for a forward-compatible solution. Only use android:windowOptOutEdgeToEdgeEnforcement as a short-term fix while you transition to edge-to-edge layouts.

Upvotes: 10

Young Lee
Young Lee

Reputation: 51

In my case, my activity content was complex, so accessing it through Custom-Theme(Jetpack compose) method was not suitable. @Shouheng Wang's suggestion (I found what he suggested here Blankj's github too.) worked for me, but his suggestion needs some modifications. First, I changed it to Kotlin code. And I don't recommend 'resources.getIdentifier' because it's slow or heavy. My suggestions for improvement are as follows:

Step 1.

Create a 'BarUtils.kt' file. And fill in the following code:

object BarUtils {
   private const val TAG_STATUS_BAR = "TAG_STATUS_BAR"

   // for Lollipop (SDK 21+)
   fun transparentStatusBar(window: Window) {
       window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
       window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
       val option =
       View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
       val vis = window.decorView.systemUiVisibility
       window.decorView.systemUiVisibility = option or vis
       window.statusBarColor = Color.TRANSPARENT
   }

  // for the latest SDK (24+)

   fun applyStatusBarColor (window: Window, color: Int, isDecor: Boolean, getStatusBarHeight: Int): View {
       val parent = if (isDecor) window.decorView as ViewGroup else (window.findViewById<View>(R.id.content) as ViewGroup)
       var fakeStatusBarView = parent.findViewWithTag<View>(TAG_STATUS_BAR)
       if (fakeStatusBarView != null) {
           if (fakeStatusBarView.visibility == View.GONE) {
              fakeStatusBarView.visibility = View.VISIBLE
           }
          fakeStatusBarView.setBackgroundColor(color)
       } else {
          fakeStatusBarView = **createStatusBarView** (window, color, getStatusBarHeight)
          parent.addView(fakeStatusBarView)
       }
       return fakeStatusBarView
   }

   private fun createStatusBarView (window: Window, color: Int, **getStatusBarHeight**: Int): View {
       val statusBarView = View(window.context)
       statusBarView.layoutParams = ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.MATCH_PARENT, **getStatusBarHeight**
       )
       statusBarView.setBackgroundColor(color)
       statusBarView.tag = TAG_STATUS_BAR
       return statusBarView
   }
}

Step 2:

Then insert the following code into your Activity to get the height of the status bar, 'getStatusBarHeight'.

if (Build.VERSION.SDK_INT >= 24) {
   enableEdgeToEdge()
   ViewCompat.setOnApplyWindowInsetsListener(maBinding.root) { v, windowInsets ->
      val currentNightMode = (resources.configuration.uiMode
                 and Configuration.UI_MODE_NIGHT_MASK)
      val insets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
      v.run {
          updateLayoutParams<ViewGroup.MarginLayoutParams> {
             topMargin = insets.top
             when (currentNightMode) {
                  Configuration.UI_MODE_NIGHT_NO -> {
                     BarUtils.applyStatusBarColor(window, Color.WHITE,true, topMargin)
                  }
                  Configuration.UI_MODE_NIGHT_YES -> {
                     BarUtils.applyStatusBarColor(window, Color.BLACK,true, topMargin)
                  }
                  Configuration.UI_MODE_NIGHT_UNDEFINED -> {
                     BarUtils.applyStatusBarColor(window, Color.WHITE,true, topMargin)
                  }
              }
          }
      }
      windowInsets
      //WindowInsetsCompat.CONSUMED
      //<---here- for 'bottom app bar' option
  }      
}  else {
    BarUtils.transparentStatusBar(window)
}

note:

  1. If you forget enableEdgeToEdge(), the StatusBar color will not appear correctly.

  2. A crash occurs if it is located before the activity's setContentView().

  3. If you do not use v.run {}, 'ViewCompat' changes the height of the activity StatusBar. Don't forget that this code only retrieves the value of 'getStatusBarHeight'. (For the same reason, 'windowInsets' was used instead of 'WindowInsetsCompat.CONSUMED'.)

  4. The 'currentNightMode' part is for dark mode.

  5. Finally, for older devices, if you want to remove the 'bottom app bar' or adjust its option, you will need to insert the following code in the 'here-bottom app bar' position of the code above.

WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).let {
      it.hide(WindowInsetsCompat.Type.navigationBars())
      //If you don't add the line below, the 'bottom app bar' will move appearing and disappearing.
      it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
 }

Upvotes: 0

praveensmedia
praveensmedia

Reputation: 1

If you are targeting Android 15 and using Jetpack compose use this in your Activity. Top of setContent{} block

WindowCompat.getInsetsController(window, window.decorView)
            .isAppearanceLightStatusBars = isSystemInDarkTheme()

Update: You need to modify status bar colors according to your custom theme. isMyAppInDarkTheme is a mutable stateflow where i update where i'm using app level dark or light theme. I tested in Android 15 and below

 val isMyAppInDarkTheme by isMyAppInDarkTheme.collectAsState()
        LaunchedEffect (key1 = isMyAppInDarkTheme){
            WindowCompat.getInsetsController(window, window.decorView)
                .isAppearanceLightStatusBars = !isMyAppInDarkTheme
        }

Upvotes: 0

Shouheng Wang
Shouheng Wang

Reputation: 648

Here is the code snippet I used in my project, which has a better compatibility between various Android versions. It support minimum API version is Android 19.

In this code, it will make the system statusbar transparent at first. And then add a view with same height of the status bar. Then change the color of the 'fake status bar' instead of real status bar.

public class BarUtils {

    private static final String TAG_STATUS_BAR = "TAG_STATUS_BAR";

    public static View setStatusBarColor(@NonNull final Activity activity, @ColorInt final int color) {
        return setStatusBarColor(activity, color, false);
    }

    public static View setStatusBarColor(@NonNull final Activity activity, @ColorInt final int color, final boolean isDecor) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return null;
        transparentStatusBar(activity.getWindow());
        return applyStatusBarColor(activity.getWindow(), color, isDecor);
    }

    public static void transparentStatusBar(final Window window) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
            int vis = window.getDecorView().getSystemUiVisibility();
            window.getDecorView().setSystemUiVisibility(option | vis);
            window.setStatusBarColor(Color.TRANSPARENT);
        } else {
            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }

    private static View applyStatusBarColor(final Window window, final int color, boolean isDecor) {
        ViewGroup parent = isDecor ?
                (ViewGroup) window.getDecorView() :
                (ViewGroup) window.findViewById(android.R.id.content);
        View fakeStatusBarView = parent.findViewWithTag(TAG_STATUS_BAR);
        if (fakeStatusBarView != null) {
            if (fakeStatusBarView.getVisibility() == View.GONE) {
                fakeStatusBarView.setVisibility(View.VISIBLE);
            }
            fakeStatusBarView.setBackgroundColor(color);
        } else {
            fakeStatusBarView = createStatusBarView(window.getContext(), color);
            parent.addView(fakeStatusBarView);
        }
        return fakeStatusBarView;
    }

    private static View createStatusBarView(final Context context, final int color) {
        View statusBarView = new View(context);
        statusBarView.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(context)));
        statusBarView.setBackgroundColor(color);
        statusBarView.setTag(TAG_STATUS_BAR);
        return statusBarView;
    }

    public static int getStatusBarHeight(Context context) {
        Resources resources = context.getResources();
        int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
        return resources.getDimensionPixelSize(resourceId);
    }
}

Upvotes: 4

Luiz Alegria
Luiz Alegria

Reputation: 310

val isBeforeVanillaIceCream
    get() = Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE    

@Suppress("Deprecation")
fun Activity.changeStatusBarColor(isLightStatusBar: Boolean, @ColorRes color: Int) {
    if (isBeforeVanillaIceCream) {
        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
        windowInsetsController.isAppearanceLightStatusBars = isLightStatusBar
        window.statusBarColor = ContextCompat.getColor(this, color)
    } else {
        val insetsController = WindowInsetsControllerCompat(window, window.decorView)
        insetsController.isAppearanceLightStatusBars = isLightStatusBar
            
        window.decorView.setBackgroundColor(ContextCompat.getColor(this, color))
        window.insetsController?.hide(android.view.WindowInsets.Type.statusBars())
        window.insetsController?.show(android.view.WindowInsets.Type.statusBars())
    }
}

Upvotes: 0

Samuel Wakoli
Samuel Wakoli

Reputation: 300

This is how I approached the issue on my end by using the insets controller:

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable() () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> darkScheme
        else -> lightScheme
    }

    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            val decorView = window.decorView

            // Ensure insets are applied correctly
            WindowCompat.setDecorFitsSystemWindows(window, false)

            // Handle the status bar appearance
            val insetsController = WindowCompat.getInsetsController(window, decorView)
            insetsController.apply {
                isAppearanceLightStatusBars = !darkTheme // Control icon visibility
            }
        }
    }


    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        content = content
    )
}

Let me know if further assistance is needed; Good luck!

Upvotes: 0

Alexander Poleschuk
Alexander Poleschuk

Reputation: 1049

You can use the windowOptOutEdgeToEdgeEnforcement attribute to cancel the edge-to-edge enforcement but it is a temporary solution that will be deprecated and disabled in a future SDK level. So I came up with a custom solution based on ViewCompat.setOnApplyWindowInsetsListener.

At the top of my layout, I added an empty view like this:

<View xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/android15statusBarPlaceHolder"
    app:layout_constraintTop_toTopOf="parent"
    android:visibility="gone"
    android:background="@color/gray"/>

(The view is invisible by default as it's not needed on older Android versions.) The idea behind the view is that it is placed where the status bar is, and the view's height is set to be equal to the height of the status bar. So the status bar gets its color from the view's android:background property. Or instead of android:background, you can use the style property to set different background for light and dark modes.

In my activity, I added this code for Android 15+:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
    val placeHolder: View = parentView.findViewById(R.id.android15statusBarPlaceHolder)
    // make the view visible
    placeHolder.visibility = View.VISIBLE
    ViewCompat.setOnApplyWindowInsetsListener(parentView) { v, insets ->
        val bars = insets.getInsets(
            WindowInsetsCompat.Type.systemBars()
                        or WindowInsetsCompat.Type.displayCutout()
        )
        // set status bar placeholder's height equal to that of the status bar
        placeHolder.updateLayoutParams {
            height = bars.top
        }
        v.updatePadding(
            left = bars.left,
            right = bars.right,
            bottom = bars.bottom
        )

        WindowInsetsCompat.CONSUMED
    }
}

In my code, I use the updateLayoutParams function to set the empty view's height. With this approach, it is not needed to set the top padding. So I pass only three params to the updatePadding function: for left, right, and bottom padding.

Upvotes: 0

Dung Tran
Dung Tran

Reputation: 19

abstract fun setStatusBarColor(color: Int): Unit

has been deprecated in API level 35. If your app targets VANILLA_ICE_CREAM or above, the color will be transparent and cannot be changed. You can see doc

However, you can disable the edge-to-edge feature of Android 15 by using:

<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>

and set the status bar color again using:

<item name="android:statusBarColor">@color/colorPrimaryDark</item>

Upvotes: 1

mars885
mars885

Reputation: 83

To achieve something like this, you will need to:

  1. Draw a composable that takes the space of the system bar the background of which you want to change.
  2. Change the actual background.

To do the first step, it is recommended to use either of the following modifiers to take up the space of the system bar:

As for the second step, a simple Modifier.background(Color.RED) does the job.

Putting it all together, we will have this code:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Box(Modifier.fillMaxSize()) {
                Content()
                // Background for status bar at the top
                Spacer(
                    modifier = Modifier.windowInsetsTopHeight(WindowInsets.statusBars)
                        .fillMaxWidth()
                        .align(Alignment.TopCenter)
                        .background(Color.Red)
                )
            }
        }
    }
}

@Composable
private fun Content() {
    Box(modifier = Modifier.fillMaxSize().background(Color.White)) {
        // Content of the app
    }
}

Note, however, that the Spacer from the code above should come as a last child inside the parent Box for its background to actually be visible on the screen due to the nature of how the Box draws its children.

Upvotes: 1

hollmannlu
hollmannlu

Reputation: 412

I did not manage to change the color yet, but I found a way to opt out of edge-to-edge:

in res/values-v35/styles

        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>

Upvotes: 14

Related Questions