2

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:

action mode

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:

google-files

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?

Tushar Kathuria
  • 645
  • 2
  • 8
  • 22
  • By the looks of the Google files GIF, it doesn't look two separate color changes at all, it looks one(checked it live, its one). For one, What I think of you can either go with a custom `Toolbar` and draw behind the `StatusBar` or second, set `fitSystemWindows` to `false`, set the `Toolbar`'s `paddingTop` to `StatusBar's` height which will basically increase its height to cover the whole area. First one is overkill but gives control, second one is hacky but works. – Lalit Fauzdar Aug 21 '21 at 12:59

3 Answers3

4

There are two requests when the contextual action bar (CAB) is show/hidden:

  1. Toggle status bar icon light mode.
  2. Synchronize the color animation between the CAB & the status bar.

Sync color animation between CAB & status bar

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
            )
        }

    }

}

Toggle status bar icon light mode

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

Another approach

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)

Zain
  • 37,492
  • 7
  • 60
  • 84
  • In the second approach, can you please explain the purpose of animating custom view parent's alpha to 0 ? I tested that it works fine but not sure how :-) – Tushar Kathuria Aug 25 '21 at 14:19
  • 1
    @TusharKathuria I tried to make it hidden at first till the animation is up (setting alpaha to 0 the view is completely transparent, and then it becomes fully opaque whenever the animation is over)... I think `(mode?.customView?.parent as View).alpha = 0f` will work as well. – Zain Aug 25 '21 at 14:24
0

This code snippet will change the Background and Title Color

In the styles.xml

<style name="Widget.App.ActionMode" parent="@style/Widget.AppCompat.ActionMode">
    <item name="background">@color/grey_100</item>
    <item name="titleTextStyle">@style/ActionModeTitleTextStyle</item>
</style>

<style name="ActionModeTitleTextStyle" parent="@style/TextAppearance.AppCompat.Widget.ActionMode.Title">
    <item name="android:textColor">@android:color/white</item>
</style>
  • This question is about how to change status bar colour along with toolbar colour while making sure that transition is smooth. It is not about how to change the title text color – Tushar Kathuria Aug 24 '21 at 05:48
0
  1. You can try with this:

Change status bar color with AppCompat ActionBarActivity

  1. Or try to apply different theme programatically to activity that has applied status bar color.
<resources>
    <style name="AppTheme" parent="AppTheme.Base">
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>
</resources>
I.Step
  • 613
  • 6
  • 22