My application uses the app theme that inherits Theme.MaterialComponents.Light.NoActionBar
. When user selects a list item and enters the contextual action mode, I want to change the action bar to dark grey color. I am using following code to achieve same:
In themes.xml
:
<item name="windowActionModeOverlay">true</item>
<item name="actionModeStyle">@style/Widget.App.ActionMode</item>
<item name="actionModeCloseDrawable">@drawable/ic_close_24dp</item>
<item name="actionBarTheme">@style/ThemeOverlay.MaterialComponents.Dark.ActionBar</item>
In styles.xml
:
<style name="Widget.App.ActionMode" parent="Widget.AppCompat.ActionMode">
<item name="background">@color/grey_100</item>
</style>
In my fragment:
val callback = object: ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
requireActivity().menuInflater.inflate(R.menu.contextual_action_bar, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {return false}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {return false}
override fun onDestroyActionMode(mode: ActionMode?) {}
}
val actionMode = requireActivity().startActionMode(callback)
actionMode?.title = "1 selected"
With this code, I get following result:
How can we change this code so that it also changes the status bar background and icon colors? Following is the example of how many apps including Google Files app does this:
Notice how it correctly changes status bar background color as well as icon colors accordingly.
I have already tried this answer but it does not result in a smooth transition as many users have commented there. Is there any standard way to achieve the desired behaviour?
There are two requests when the contextual action bar (CAB) is show/hidden:
You can animate the change of the status bar color using ArgbEvaluator
with an adjusted duration that tends to the CAB duration (with trial and error it's near 300 msec; I have no documentation clue for the exact value, but you can adjust that to your needs):
fun switchStatusColor(colorFrom: Int, colorTo: Int, duration: Long) {
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo)
colorAnimation.duration = duration // milliseconds
colorAnimation.addUpdateListener { animator ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
window.statusBarColor = animator.animatedValue as Int
}
colorAnimation.start()
}
And this need to be called with the appropriate colors within onCreateActionMode
& onDestroyActionMode
:
val callback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
menuInflater.inflate(R.menu.contextual_action_bar, menu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
switchStatusColor(
window.statusBarColor,
ContextCompat.getColor(this@MainActivity, R.color.grey_100), 300
)
}
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
switchStatusColor(
window.statusBarColor,
ContextCompat.getColor(this@MainActivity, R.color.white), 300
)
}
}
}
For API level below 30 (Android R), use systemUiVisibility
, and WindowInsetsController
for API level 30 and above:
For some reason the WindowInsetsController
didn't work for me, even by using the below 4 versions, but thankfully the old flag works, so I kept it in API level > 30:
private fun switchStatusBarIconLight(isLight: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.setSystemBarsAppearance(
if (isLight) APPEARANCE_LIGHT_STATUS_BARS else 0,
APPEARANCE_LIGHT_STATUS_BARS
)
WindowInsetsControllerCompat(
window,
window.decorView
).isAppearanceLightStatusBars =
isLight
ViewCompat.getWindowInsetsController(window.decorView)?.apply {
isAppearanceLightStatusBars = isLight
}
WindowCompat.getInsetsController(
window,
window.decorView
)?.isAppearanceLightStatusBars = isLight
window.decorView.systemUiVisibility =
if (isLight) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0 // Deprecated in API level 30 // but only works than the above
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (isLight) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0 // Deprecated in API level 30
}
}
So, the working demo:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Setting up the status bar when the app starts
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
window.statusBarColor = ContextCompat.getColor(this@MainActivity, R.color.white)
switchStatusBarIconLight(true)
val callback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
menuInflater.inflate(R.menu.contextual_action_bar, menu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
switchStatusColor(
window.statusBarColor,
ContextCompat.getColor(this@MainActivity, R.color.grey_100), 300
)
}
switchStatusBarIconLight(false)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
switchStatusColor(
window.statusBarColor,
ContextCompat.getColor(this@MainActivity, R.color.white), 300
)
}
switchStatusBarIconLight(true)
}
}
}
*
* Animate switching the color of the status bar from the colorFrom color to the colorTo color
* duration: animation duration.
* */
private fun switchStatusColor(colorFrom: Int, colorTo: Int, duration: Long) {
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo)
colorAnimation.duration = duration // milliseconds
colorAnimation.addUpdateListener { animator ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
window.statusBarColor = animator.animatedValue as Int
}
colorAnimation.start()
}
/*
* Switch the dark mode of the status bar icons
* When isLight is true, the status bar icons will turn light
* */
private fun switchStatusBarIconLight(isLight: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.setSystemBarsAppearance(
if (isLight) APPEARANCE_LIGHT_STATUS_BARS else 0,
APPEARANCE_LIGHT_STATUS_BARS
)
WindowInsetsControllerCompat(
window,
window.decorView
).isAppearanceLightStatusBars =
isLight
ViewCompat.getWindowInsetsController(window.decorView)?.apply {
isAppearanceLightStatusBars = isLight
}
WindowCompat.getInsetsController(
window,
window.decorView
)?.isAppearanceLightStatusBars = isLight
window.decorView.systemUiVisibility =
if (isLight) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0 // Deprecated in API level 30 // but only works than the above
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (isLight) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0 // Deprecated in API level 30
}
}
}
Preview:
UPDATE
Instead of animating the status bar to sync with the CAB, you can instead disable the animation. But this requires you to use a customView to the CAB instead of a menu.
There are two places to do that:
When the CAB is shown
Do it whenever you call startSupportActionMode
:
val mode = startSupportActionMode(callback)
ViewCompat.animate(mode?.customView?.parent as View).alpha(0f)
When the CAB is hidden:
Do it in onDestroyActionMode
override fun onDestroyActionMode(mode: ActionMode?) {
// Hiding the CAB
(mode?.customView?.parent as View).visibility = View.GONE
}
The downside is that no animation anymore, and there is a delay to show the CAB because it is just hidden using the alpha, so the animation is still consumed but invisible because of setting the alph. And this requires you to to toggle the status bar color after some delay which is assumed by 300 millisec in the first approach:
val callback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
val customView: View = LayoutInflater.from(this@MainActivity).inflate(
R.layout.custom_contextual_action_bar, null
)
mode?.customView = customView
Handler(Looper.getMainLooper()).postDelayed({
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor =
ContextCompat.getColor(this@MainActivity, R.color.grey_100)
switchStatusBarIconLight(false)
}
}, 300)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
// Hiding the CAB
(mode?.customView?.parent as View).visibility = View.GONE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor =
ContextCompat.getColor(this@MainActivity, R.color.white)
switchStatusBarIconLight(true)
}
}
}
// Call:
val mode = startSupportActionMode(callback)
ViewCompat.animate(mode?.customView?.parent as View).alpha(0f)