Android system Bar immersive perfect compatibility solution

Android system Bar immersive perfect compatibility solution

introduction

Since Android 5.0, Android has introduced immersive system bars (status bars and navigation bars), and the visual effects of Android have been further improved. Major app manufacturers are also using immersive effects in most scenarios. However, due to the serious fragmentation of Android, the system bar effects of each version may be different, which often requires developers to make compatible adaptations. In order to simplify the use of immersive system bars and unify the effect differences caused by differences in models and versions, this article will introduce the composition of system bars and immersive adaptation solutions.

background

Problem 1: Background color cannot be set in immersive mode

For systems with Android 5.0 or higher, during the onCreate of the Activity, set the window property:

 window . addFlags ( WindowManager . LayoutParams . FLAG_TRANSLUCENT_STATUS )
window . addFlags ( WindowManager . LayoutParams . FLAG_TRANSLUCENT_NAVIGATION )

You can turn on the immersive system bar, the effect is as follows:

Android 5.0 Immersive Status Bar

Android 5.0 Immersive Navigation Bar

However, after setting the immersive mode, the colors originally set by window.statusBarColor and window.statusBarColor are also unavailable, which means that customizing the color of the semi-transparent system bar is not supported.

Problem 2: The navigation bar cannot be fully transparent

The system default status bar and navigation bar both have a semi-transparent mask. Although the color cannot be set, the status bar can be made fully transparent by setting the following code:

 window . clearFlags ( WindowManager . LayoutParams . FLAG_TRANSLUCENT_STATUS )
window . decorView . systemUiVisibility = ( View . SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View . SYSTEM_UI_FLAG_LAYOUT_STABLE )
window . addFlags ( WindowManager . LayoutParams . FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS )
window . statusBarColor = Color . TRANSPARENT

The effect is as follows:

Android 10.0 Immersive Fully Transparent Status Bar

Try setting the navigation bar to be fully transparent in a similar way:

 window . clearFlags ( WindowManager . LayoutParams . FLAG_TRANSLUCENT_NAVIGATION )
window . decorView . systemUiVisibility = ( View . SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View . SYSTEM_UI_FLAG_LAYOUT_STABLE or View . SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION )
window . addFlags ( WindowManager . LayoutParams . FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS )
window . navigationBarColor = Color . TRANSPARENT

But I found that the semi-transparent background of the navigation bar still cannot be removed:

Question 3: Bright system bar version differences

For systems with Android 6.0 or higher, if the background is light, you can set the status bar and navigation bar text color to dark, that is, the navigation bar and status bar are light (only Android 8.0 and above support navigation bar text color modification):

 window . decorView . systemUiVisibility =
View . SYSTEM_UI_FLAG_LAYOUT_STABLE or View . SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

window . decorView . systemUiVisibility =
window . decorView . systemUiVisibility or if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . O ) View . SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0

The effect is as follows:

Android 8.0 bright status bar

Android 8.0 bright navigation bar

However, after turning on immersive mode on the light system bar, the dark navigation icon on the navigation bar does not work in 8.0 to 9.0 systems, while the dark navigation icon can be displayed in versions 10.0 and above:

Android 8.0 bright immersive bright navigation bar

Android 10.0 bright immersive bright navigation bar

Problem Analysis

Problem 1: Background color cannot be set in immersive mode

Looking at the source code, I found that when setting the background color of the status bar and navigation bar, it cannot be immersive:

Problem 2: The navigation bar cannot be fully transparent

When the navigation bar is set to transparent color (Color.TRANSPARENT), the navigation bar will become translucent. When other colors are set, it is normal. For example, if the color is set to 0x700F7FFF, the display effect is as follows:

Android 10.0 Immersive Navigation Bar

Why does this happen? By debugging into the source code, I found that there is a logic in the onApplyThemeResource method of the activity:

 // Get the primary color and update the TaskDescription for this activity
TypedArray a = theme . obtainStyledAttributes (
com . android . internal . R . styleable . ActivityTaskDescription );
if ( mTaskDescription . getPrimaryColor () == 0 ) {
int colorPrimary = a . getColor (
com . android . internal . R . styleable . ActivityTaskDescription_colorPrimary , 0 );
if ( colorPrimary != 0 && Color . alpha ( colorPrimary ) == 0xFF ) {
mTaskDescription .setPrimaryColor ( colorPrimary ) ;
}
}

That is to say, if the navigation bar color is set to 0 (purely transparent), it will be changed to the built-in color: ActivityTaskDescription_colorPrimary, so a gray mask effect will appear.

Question 3: Bright system bar version differences

By looking at the source code, it is found that, similar to setting the background color of the status bar and navigation bar, setting the navigation bar icon color cannot be immersive:

Solve immersive compatibility issues

As for the second problem that the navigation bar cannot be fully transparent, it can be seen from the code in the above problem analysis that the navigation bar will be replaced with a semi-transparent mask only when the navigation bar color is set to pure transparency (0). Then, we can change the color of the pure transparency to 0x01000000, which can also achieve a nearly pure transparency effect:

For the first problem, it is difficult to set the background color of the system bar in immersive mode through conventional methods. For the third problem, conventional methods need to adapt each version separately, which is more difficult for domestic mobile phones.

In order to solve the compatibility problem and better manage the status bar and navigation bar, can we implement the background View of the status bar and navigation bar ourselves?

From the Layout Inspector, we can see that the navigation bar and status bar are essentially a view:

When the activity is created, two views (navigationBarBackground and statusBarBackground) are created and added to decorView, so that the color of the status bar can be controlled. So, is it possible to hide these two system views and replace them with custom views?

Therefore, in order to improve compatibility and better manage the status bar and navigation bar, we can hide the system's navigationBarBackground and statusBarBackground and replace them with custom views instead of setting them through FLAG_TRANSLUCENT_STATUS and FLAG_TRANSLUCENT_NAVIGATION.

Implementing an immersive status bar

Add a custom status bar by creating a view with a height equal to the status bar's height and adding it to decorView:

 View ( window . context ). apply {
id = R . id . status_bar_view
val params = FrameLayout . LayoutParams ( FrameLayout . LayoutParams . MATCH_PARENT , statusHeight )
params . gravity = Gravity . TOP
layoutParams = params
( window . decorView as ViewGroup ). addView ( this )
}

Hide the system status bar. Since the activity does not create the status bar view (statusBarBackground) during onCreate, it cannot be hidden directly. Here you can capture the statusBarBackground by adding an OnHierarchyChangeListener listener to decorView:

 ( window . decorView as ViewGroup ) . setOnHierarchyChangeListener ( object : ViewGroup . OnHierarchyChangeListener {
override fun onChildViewAdded ( parent : View ? , child : View ? ) {
if ( child ?. id == android . R . id . statusBarBackground ) {
child . scaleX = 0 f
}
}

override fun onChildViewRemoved ( parent : View ? , child : View ? ) {
}
})

Note: Here, setting the child's scaleX to 0 will hide it, so why can't we set visibility to GONE? This is because when the theme is applied later (onApplyThemeResource), the system will set visibility back to VISIBLE.

After hiding, the translucent status bar is not displayed, but there will be a blank space at the top:

Through Layout Inspector, we can see that the first element of decorView (content view) has a padding:

Therefore, we can remove it by setting paddingTop to 0:

 val view = ( window . decorView as ViewGroup ). getChildAt ( 0 )
view . addOnLayoutChangeListener { v , _ , _ , _ , _ , _ , _ , _ , _ - >
if ( view . paddingTop > 0 ) {
view . setPadding ( 0 , 0 , 0 , view . paddingBottom )
val content = findViewById < View > ( android . R . id . content )
content.requestLayout ( )
}
}

Note: You need to monitor the layout changes of the view here, otherwise it will be modified later after it is set at the beginning.

Implementing an immersive navigation bar

The customization of the navigation bar is similar to the status bar, but there are some differences. First create a custom view and add it to decorView, then hide the original system navigationBarBackground:

 window . decorView . findViewById ( R . id . navigation_bar_view ) ? : View ( window . context ). apply {
id = R . id . navigation_bar_view
val resourceId = resources . getIdentifier ( navigation_bar_height , dimen , android )
val navigationBarHeight = if ( resourceId > 0 ) resources . getDimensionPixelSize ( resourceId ) else 0
val params = FrameLayout . LayoutParams ( FrameLayout . LayoutParams . MATCH_PARENT , navigationBarHeight )
params . gravity = Gravity . BOTTOM
layoutParams = params
( window . decorView as ViewGroup ). addView ( this )

( window . decorView as ViewGroup ) . setOnHierarchyChangeListener ( object : ViewGroup . OnHierarchyChangeListener {
override fun onChildViewAdded ( parent : View ? , child : View ? ) {
if ( child ?. id == android . R . id . navigationBarBackground ) {
child . scaleX = 0 f
} else if ( child ?. id == android . R . id . statusBarBackground ) {
child . scaleX = 0 f
}
}

override fun onChildViewRemoved ( parent : View ? , child : View ? ) {
}
})
}

Note: In the onChildViewAdded method here, because OnHierarchyChangeListener can only be set once, both the status bar and the navigation bar need to be considered.

In this way, the navigation bar can be replaced with a custom view, but there is a problem. Since the navigationBarHeight is fixed, if the user switches the style of the navigation bar and then returns to the app, the height of the navigation bar will not be readjusted. In order to make the navigation bar clear, set its color to 0x7F00FF7F:

As can be seen from the figure, the height of the navigation bar does not change after switching. To solve this problem, you need to set OnLayoutChangeListener for navigationBarBackground to monitor changes in the navigation bar height and associate it with the view through liveData. The code is as follows:

 val heightLiveData = MutableLiveData < Int > ()
heightLiveData . value = 0
window . decorView . setTag ( R . id . navigation_height_live_data , heightLiveData )

val navigationBarView = window . decorView . findViewById ( R . id . navigation_bar_view ) ? : View ( window . context ). apply {
id = R . id . navigation_bar_view
val params = FrameLayout . LayoutParams ( FrameLayout . LayoutParams . MATCH_PARENT , heightLiveData . value ? : 0 )
params . gravity = Gravity . BOTTOM
layoutParams = params
( window . decorView as ViewGroup ). addView ( this )

if ( this @ immersiveNavigationBar is FragmentActivity ) {
heightLiveData . observe ( this @ immersiveNavigationBar ) {
val lp = layoutParams
lp . height = heightLiveData . value ? : 0
layoutParams = lp
}
}

( window . decorView as ViewGroup ) . setOnHierarchyChangeListener ( object : ViewGroup . OnHierarchyChangeListener {
override fun onChildViewAdded ( parent : View ? , child : View ? ) {
if ( child ?. id == android . R . id . navigationBarBackground ) {
child . scaleX = 0 f

child . addOnLayoutChangeListener { _ , _ , top , _ , bottom , _ , _ , _ , _ - >
heightLiveData . value = bottom - top
}
} else if ( child ?. id == android . R . id . statusBarBackground ) {
child . scaleX = 0 f
}
}

override fun onChildViewRemoved ( parent : View ? , child : View ? ) {
}
})
}

The above method can solve the problem of customized navigation bar height after switching navigation bar style:

Complete code

 @ file : Suppress ( "DEPRECATION" )

package com . bytedance . heycan . systembar . activity

import android.app.Activity
import android . graphics . Color
import android . os . Build
import android . util . Size
import android . view . Gravity
import android . view . View
import android . view . ViewGroup
import android . view . WindowManager
import android . widget . FrameLayout
import androidx . fragment . app . FragmentActivity
import androidx . lifecycle . LiveData
import androidx . lifecycle . MutableLiveData
import com . bytedance . heycan . systembar . R

/**
* Created by dengchunguo on 2021/4/25
*/
fun Activity . setLightStatusBar ( isLightingColor : Boolean ) {
val window = this . window
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . M ) {
if ( isLightingColor ) {
window . decorView . systemUiVisibility =
View . SYSTEM_UI_FLAG_LAYOUT_STABLE or View . SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
window . decorView . systemUiVisibility = View . SYSTEM_UI_FLAG_LAYOUT_STABLE
}
}
}

fun Activity . setLightNavigationBar ( isLightingColor : Boolean ) {
val window = this . window
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . M && isLightingColor ) {
window . decorView . systemUiVisibility =
window . decorView . systemUiVisibility or if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . O ) View . SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0
}
}

/**
* Must be called during Activity's onCreate
*/
fun Activity . immersiveStatusBar () {
val view = ( window . decorView as ViewGroup ). getChildAt ( 0 )
view . addOnLayoutChangeListener { v , _ , _ , _ , _ , _ , _ , _ , _ - >
val lp = view . layoutParams as FrameLayout . LayoutParams
if ( lp . topMargin > 0 ) {
lp . topMargin = 0
v . layoutParams = lp
}
if ( view . paddingTop > 0 ) {
view . setPadding ( 0 , 0 , 0 , view . paddingBottom )
val content = findViewById < View > ( android . R . id . content )
content.requestLayout ( )
}
}

val content = findViewById < View > ( android . R . id . content )
content . setPadding ( 0 , 0 , 0 , content . paddingBottom )

window . decorView . findViewById ( R . id . status_bar_view ) ? : View ( window . context ). apply {
id = R . id . status_bar_view
val params = FrameLayout . LayoutParams ( FrameLayout . LayoutParams . MATCH_PARENT , statusHeight )
params . gravity = Gravity . TOP
layoutParams = params
( window . decorView as ViewGroup ). addView ( this )

( window . decorView as ViewGroup ) . setOnHierarchyChangeListener ( object : ViewGroup . OnHierarchyChangeListener {
override fun onChildViewAdded ( parent : View ? , child : View ? ) {
if ( child ?. id == android . R . id . statusBarBackground ) {
child . scaleX = 0 f
}
}

override fun onChildViewRemoved ( parent : View ? , child : View ? ) {
}
})
}
setStatusBarColor ( Color . TRANSPARENT )
}

/**
* Must be called during Activity's onCreate
*/
fun Activity . immersiveNavigationBar ( callback : (() - > Unit ) ? = null ) {
val view = ( window . decorView as ViewGroup ). getChildAt ( 0 )
view . addOnLayoutChangeListener { v , _ , _ , _ , _ , _ , _ , _ , _ - >
val lp = view . layoutParams as FrameLayout . LayoutParams
if ( lp . bottomMargin > 0 ) {
lp . bottomMargin = 0
v . layoutParams = lp
}
if ( view . paddingBottom > 0 ) {
view . setPadding ( 0 , view . paddingTop , 0 , 0 )
val content = findViewById < View > ( android . R . id . content )
content.requestLayout ( )
}
}

val content = findViewById < View > ( android . R . id . content )
content . setPadding ( 0 , content . paddingTop , 0 , - 1 )

val heightLiveData = MutableLiveData < Int > ()
heightLiveData . value = 0
window . decorView . setTag ( R . id . navigation_height_live_data , heightLiveData )
callback ?. invoke ()

window . decorView . findViewById ( R . id . navigation_bar_view ) ? : View ( window . context ). apply {
id = R . id . navigation_bar_view
val params = FrameLayout . LayoutParams ( FrameLayout . LayoutParams . MATCH_PARENT , heightLiveData . value ? : 0 )
params . gravity = Gravity . BOTTOM
layoutParams = params
( window . decorView as ViewGroup ). addView ( this )

if ( this @ immersiveNavigationBar is FragmentActivity ) {
heightLiveData . observe ( this @ immersiveNavigationBar ) {
val lp = layoutParams
lp . height = heightLiveData . value ? : 0
layoutParams = lp
}
}

( window . decorView as ViewGroup ) . setOnHierarchyChangeListener ( object : ViewGroup . OnHierarchyChangeListener {
override fun onChildViewAdded ( parent : View ? , child : View ? ) {
if ( child ?. id == android . R . id . navigationBarBackground ) {
child . scaleX = 0 f
bringToFront ()

child . addOnLayoutChangeListener { _ , _ , top , _ , bottom , _ , _ , _ , _ - >
heightLiveData . value = bottom - top
}
} else if ( child ?. id == android . R . id . statusBarBackground ) {
child . scaleX = 0 f
}
}

override fun onChildViewRemoved ( parent : View ? , child : View ? ) {
}
})
}
setNavigationBarColor ( Color . TRANSPARENT )
}

/**
* When immersiveStatusBar is set, if you need to use the status bar, you can call this function
*/
fun Activity . fitStatusBar ( fit : Boolean ) {
val content = findViewById < View > ( android . R . id . content )
if ( fit ) {
content . setPadding ( 0 , statusHeight , 0 , content . paddingBottom )
} else {
content . setPadding ( 0 , 0 , 0 , content . paddingBottom )
}
}

fun Activity . fitNavigationBar ( fit : Boolean ) {
val content = findViewById < View > ( android . R . id . content )
if ( fit ) {
content . setPadding ( 0 , content . paddingTop , 0 , navigationBarHeightLiveData . value ? : 0 )
} else {
content . setPadding ( 0 , content . paddingTop , 0 , - 1 )
}
if ( this is FragmentActivity ) {
navigationBarHeightLiveData . observe ( this ) {
if ( content . paddingBottom != - 1 ) {
content . setPadding ( 0 , content . paddingTop , 0 , it )
}
}
}
}

val Activity . isImmersiveNavigationBar : Boolean
get () = window . attributes . flags and WindowManager . LayoutParams . FLAG_TRANSLUCENT_NAVIGATION != 0

val Activity . statusHeight : Int
get () {
val resourceId =
resources . getIdentifier ( "status_bar_height" , "dimen" , "android" )
if ( resourceId > 0 ) {
return resources . getDimensionPixelSize ( resourceId )
}
return 0
}

val Activity . navigationHeight : Int
get () {
return navigationBarHeightLiveData . value ? : 0
}

val Activity . screenSize : Size
get () {
return if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . R ) {
Size ( windowManager . currentWindowMetrics . bounds . width (), windowManager . currentWindowMetrics . bounds . height ())
} else {
Size ( windowManager . defaultDisplay . width , windowManager . defaultDisplay . height )
}
}

fun Activity . setStatusBarColor ( color : Int ) {
val statusBarView = window . decorView . findViewById < View ? > ( R . id . status_bar_view )
if ( color == 0 && Build . VERSION . SDK_INT < Build . VERSION_CODES . M ) {
statusBarView ? .setBackgroundColor ( STATUS_BAR_MASK_COLOR )
} else {
statusBarView ? .setBackgroundColor ( color )
}
}

fun Activity . setNavigationBarColor ( color : Int ) {
val navigationBarView = window . decorView . findViewById < View ? > ( R . id . navigation_bar_view )
if ( color == 0 && Build . VERSION . SDK_INT <= Build . VERSION_CODES . M ) {
navigationBarView ? .setBackgroundColor ( STATUS_BAR_MASK_COLOR )
} else {
navigationBarView ? .setBackgroundColor ( color )
}
}

@Suppress ( "UNCHECKED_CAST" )
val Activity . navigationBarHeightLiveData : LiveData < Int >
get () {
var liveData = window . decorView . getTag ( R . id . navigation_height_live_data ) as ? LiveData < Int >
if ( liveData == null ) {
liveData = MutableLiveData ()
window . decorView . setTag ( R . id . navigation_height_live_data , liveData )
}
return liveData
}

val Activity . screenWidth : Int get () = screenSize . width

val Activity . screenHeight : Int get () = screenSize . height

private const val STATUS_BAR_MASK_COLOR = 0x7F000000

Extensions

Dialog box adaptation

Sometimes you need to use Dialog to display a prompt dialog box, loading dialog box, etc. When a dialog box is displayed, even if the activity is set to a dark status bar and navigation bar text color, the text color of the status bar and navigation bar will turn white again, as shown below:

This is because the status bar and navigation bar colors set for the activity are applied to the activity's window, and the dialog and activity are not the same window, so the dialog also needs to be set separately.

Complete code

 @ file : Suppress ( DEPRECATION )

package com . bytedance . heycan . systembar . dialog

import android . app . Dialog
import android . os . Build
import android . view . View
import android . view . ViewGroup

/**
* Created by dengchunguo on 2021/4/25
*/
fun Dialog . setLightStatusBar ( isLightingColor : Boolean ) {
val window = this . window ? : return
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . M ) {
if ( isLightingColor ) {
window . decorView . systemUiVisibility =
View . SYSTEM_UI_FLAG_LAYOUT_STABLE or View . SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
window . decorView . systemUiVisibility = View . SYSTEM_UI_FLAG_LAYOUT_STABLE
}
}
}

fun Dialog . setLightNavigationBar ( isLightingColor : Boolean ) {
val window = this . window ? : return
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . M && isLightingColor ) {
window . decorView . systemUiVisibility =
window . decorView . systemUiVisibility or if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . O ) View . SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0
}
}

fun Dialog . immersiveStatusBar () {
val window = this . window ? : return
( window . decorView as ViewGroup ) . setOnHierarchyChangeListener ( object : ViewGroup . OnHierarchyChangeListener {
override fun onChildViewAdded ( parent : View ? , child : View ? ) {
if ( child ?. id == android . R . id . statusBarBackground ) {
child . scaleX = 0 f
}
}

override fun onChildViewRemoved ( parent : View ? , child : View ? ) {
}
})
}

fun Dialog . immersiveNavigationBar () {
val window = this . window ? : return
( window . decorView as ViewGroup ) . setOnHierarchyChangeListener ( object : ViewGroup . OnHierarchyChangeListener {
override fun onChildViewAdded ( parent : View ? , child : View ? ) {
if ( child ?. id == android . R . id . navigationBarBackground ) {
child . scaleX = 0 f
} else if ( child ?. id == android . R . id . statusBarBackground ) {
child . scaleX = 0 f
}
}

override fun onChildViewRemoved ( parent : View ? , child : View ? ) {
}
})
}

The effect is as follows:

Quick Use

Activity Immersive

 immersiveStatusBar () // Immersive status bar
immersiveNavigationBar () // Immersive navigation bar

setLightStatusBar ( true ) // Set the light status bar background (text is dark)
setLightNavigationBar ( true ) // Set the light navigation bar background (text is dark)

setStatusBarColor ( color ) // Set the status bar background color
setNavigationBarColor ( color ) // Set the navigation bar background color

navigationBarHeightLiveData . observe ( this ) {
// Listen for changes in the navigation bar height
}

Dialog Immersive

 val dialog = Dialog ( this , R . style . Heycan_SampleDialog )
dialog . setContentView ( R . layout . dialog_loading )
dialog . immersiveStatusBar ()
dialog . immersiveNavigationBar ()
dialog.setLightStatusBar (true )
dialog.setLightNavigationBar (true )
dialog.show ( )

It can achieve a page immersive navigation bar effect similar to iOS:

<<:  iOS 15.4 released: What's new in iOS 15.4

>>:  The latest version of WeChat 8.0.20 is here, with five new features, all of which are very useful

Recommend

Love life and prevent suicide, what can we do?

Follow "Body Code Decoding Bureau" (pub...

Central Bank: Digital RMB will coexist with Alipay and WeChat Pay

Since the digital RMB was proposed, many people h...

Summary of Zhihu promotion: 12 methods of marketing on Zhihu

1. Create topics that attract a lot of attention....

7 social media marketing methods and effectiveness evaluation methods!

Main contents of this article: What is Social Med...

How do you start to create an offline event?

1. To organize a successful offline event , we ne...

A detailed discussion on short video operation strategies (Part 2)

Preface: In the previous article, the author disc...

Create a silky smooth H5 page flip library

background With the popularity of mobile marketin...