The introduction

Since Android version 5.0, Android has brought an immersive system bar (status bar and navigation bar). Android’s visual effects have been further improved, and major app manufacturers have also used immersive effects in most scenarios. However, due to the serious fragmentation of Android, the bar effect of each version of the system may be different, leading to the need for compatibility and adaptation. In order to simplify the use of system BAR immersion and unify the effect differences caused by different models and versions, this paper will introduce the composition and immersion adaptation scheme of system BAR.

background

Problem 1: Background color cannot be set in immersion mode

For Android 5.0 or higher, set the Activity’s onCreate attribute to the window:

window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
Copy the code

The immersive system BAR can be opened, and the effect is as follows:

Android 5.0 immersive status bar

Android 5.0 Immersive navigation bar

However, after setting immersion, the colors originally set by window.statusbarColor and window.statusbarColor are also not available, which means that custom translucent bar colors are not supported.

Problem two: Unable to fully transparent navigation bar

The default status bar and navigation bar have a translucent mask. Although there is no support for setting color, you can make the status bar 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
Copy the code

The effect is as follows:

Fully transparent status bar for Android 10.0 immersion

Try making the navigation bar 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
Copy the code

But found that the navigation bar translucent background is still unable to remove:

Problem three: The bar version of bright color system is different

For Android 6.0 or later, if the background is light, you can set the text color of the status bar and navigation bar to dark, that is, the text color of the navigation bar and status bar to light (Only Android 8.0 and later support the text color change of the navigation bar) :

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
Copy the code

The effect is as follows:

Android 8.0 bright color status bar

Android 8.0 bright color navigation bar

However, after immersion is enabled on the basis of bar, the dark navigation icon in the navigation bar does not take effect in system 8.0 to 9.0, while versions 10.0 and later can display the dark navigation icon:

Android 8.0 Light Immersion Light color navigation bar

Android 10.0 Light Immersion Light navigation bar

Problem analysis

Problem 1: Background color cannot be set in immersion mode

Setting the background color of the status bar and navigation bar is not available for immersion:

Problem two: Unable to fully transparent navigation bar

When you set the Color to color.transparent, the navigation bar will become translucent. When you set the Color to 0x700F7FFF, the navigation bar will be displayed as follows:

Android 10.0 immersive navigation bar

The activity’s onApplyThemeResource method contains the following logic:

// 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);
    }
}
Copy the code

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

Problem three: The bar version of bright color system is different

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

Solve immersion compatibility issues

For problem 2, the navigation bar cannot be fully transparent. As can be seen from the code in the analysis of the above problem, it will be replaced with a translucent mask only when the color of the navigation bar is set to pure transparent (0). So, we can change the color of the pure transparent case to 0x01000000, which can also achieve a near-pure transparent effect:

As for the first problem, it is difficult to set the background color of the system bar under immersion in a conventional way. For the third problem, it is necessary to adapt each version separately in the conventional way, 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?

The Layout Inspector shows 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 the decorView to control the color of the status bar. So, can we hide the two views of the system and replace them with a custom view?

Therefore, for better compatibility and better management of the status bar and navigation bar, we can hide the navigationBarBackground and statusBarBackground of the system and replace them with a custom view. Instead of FLAG_TRANSLUCENT_STATUS and FLAG_TRANSLUCENT_NAVIGATION.

Implement an immersive status bar

  1. Add a custom status bar. Add it to the decorView by creating a view whose height is equal to the height of the status bar:
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)
}
Copy the code
  1. Hide the system status bar. As a result of the activity inonCreateThe status bar view (statusBarBackground) is not created, so it cannot be hidden directly. This can be added on the decorViewOnHierarchyChangeListenerListen to capture statusBarBackground:
(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
    override fun onChildViewAdded(parent: View?, child: View?) {
        if (child?.id == android.R.id.statusBarBackground) {
            child.scaleX = 0f
        }
    }

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

Note: To hide the child, set scaleX to 0, so why not set visibility to GONE? This is because visibility will be reset to VISIBLE later when the topic is applied (onApplyThemeResource).

When hidden, the translucent status bar is not displayed, but the top is blank:

The Layout Inspector detects that the first element of the decorView (content view) contains a padding:

Therefore, it can be removed 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()
    }
}
Copy the code

Note: You need to listen for layout changes in the view, otherwise the layout will be changed later.

Implement an immersive navigation bar

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

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 = 0f } else if (child?.id == android.R.id.statusBarBackground) { child.scaleX = 0f } } override fun onChildViewRemoved(parent: View?, child: View?) { } }) }Copy the code

Note: onChildViewAdded method, because only set once OnHierarchyChangeListener, need to consider the status bar and the navigation bar at the same time.

You can replace the navigation bar with a custom View, but there is a problem. Because navigationBarHeight is fixed, if the user switches the navigation bar style, the height of the navigation bar will not be reset when the user returns to the app. To make the navigation bar visible, set its color to 0x7F00FF7F:

As can be seen from the figure, the height does not change after the navigation bar is switched. NavigationBarBackground OnLayoutChangeListener is set to monitor the height of the navigationBarBackground, and is associated with the view via liveData.

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 = 0f child.addOnLayoutChangeListener { _,  _, top, _, bottom, _, _, _, _ -> heightLiveData.value = bottom - top } } else if (child?.id == android.R.id.statusBarBackground) { child.scaleX = 0f  } } override fun onChildViewRemoved(parent: View?, child: View?) { } }) }Copy the code

You can use the following methods to change the height of the navigation bar:

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

/**
 * 必须在Activity的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 = 0f
                }
            }

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

/**
 * 必须在Activity的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 = 0f
                    bringToFront()

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

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

/**
 * 当设置了immersiveStatusBar时,如需使用状态栏,可调佣该函数
 */
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
Copy the code

extension

Dialog box adaptation

Sometimes Dialog is needed to display a prompt Dialog box, loading Dialog box, etc. When a Dialog box is displayed, even if the activity is set to the dark color of the status bar and navigation bar text, then the status bar and navigation bar text color turns white, as shown below:

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

The 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 = 0f } } 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 = 0f } else if (child?.id == android.R.id.statusBarBackground) { child.scaleX = 0f } } override fun onChildViewRemoved(parent: View?, child: View?) { } }) }Copy the code

The effect is as follows:

Quick to use

The Activity immersion

ImmersiveStatusBar () // immersiveNavigationBar() // immersiveNavigationBar setLightStatusBar(true) // setLightStatusBar background (text is dark) SetLightNavigationBar (true) setLightNavigationBar(true) setLightNavigationBar(true) setStatusBarColor(color) setLightNavigationBarColor (color Set the navigation bar background navigationBarHeightLiveData. Observe (this)} {/ / to monitor the navigation bar height changeCopy the code

Dialog immersion

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()
Copy the code

Effect of the Demo

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

Join us

As bytedance’s image team, we are currently developing a number of products, including Photoediting, CapCut, Light Color, Wake image and Faceu. Our business covers diversified image creation scenes. By June 2021, photoediting, Light Color Camera and CapCut have repeatedly topped the list of free apps in APP Store at home and abroad. And continue to maintain high growth. Join us to create the world’s most popular image creation products.

Club recruit delivery link: job.toutiao.com/s/NFYMcaq

The school recruit delivery link: job.toutiao.com/s/NkecFwb

Learn more jobs 👉 bytedance. Feishu. Cn/docx/doxcnM…