Reputation: 2611
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
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:
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
}
}
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>
While this method restores the behavior of window.statusBarColor
, it is a temporary solution:
android:windowOptOutEdgeToEdgeEnforcement
attribute is marked for deprecation in future Android SDK versions.WindowInsets
to manage the status bar's appearance properly.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
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:
If you forget enableEdgeToEdge(), the StatusBar color will not appear correctly.
A crash occurs if it is located before the activity's setContentView().
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'.)
The 'currentNightMode' part is for dark mode.
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
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
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
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
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
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
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
Reputation: 83
To achieve something like this, you will need to:
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
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