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: 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
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 } }
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 ) }
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 ) }
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
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 () immersiveNavigationBar ()
setLightStatusBar ( true ) setLightNavigationBar ( true )
setStatusBarColor ( color ) setNavigationBarColor ( color )
navigationBarHeightLiveData . observe ( this ) { } 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: |