-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Expand file tree
/
Copy pathSystemUiUtils.kt
More file actions
333 lines (297 loc) · 11.7 KB
/
SystemUiUtils.kt
File metadata and controls
333 lines (297 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package com.reactnativenavigation.utils
import android.app.Activity
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.FrameLayout
import androidx.annotation.ColorInt
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import kotlin.math.abs
import kotlin.math.ceil
object SystemUiUtils {
private const val STATUS_BAR_HEIGHT_M = 24
internal const val STATUS_BAR_HEIGHT_TRANSLUCENCY = 0.65f
private var statusBarHeight = -1
const val DEFAULT_NAV_BAR_COLOR = Color.BLACK
private const val THREE_BUTTON_NAV_BAR_OPACITY = 0.8f
private var statusBarBackgroundView: View? = null
private var statusBarBackgroundActivity: java.lang.ref.WeakReference<Activity>? = null
private var navBarBackgroundView: View? = null
@JvmStatic
var isEdgeToEdgeActive = false
private set
private var isThreeButtonNav = false
private var lastExplicitNavBarColor: Int? = null
@JvmStatic
fun getStatusBarHeight(activity: Activity?): Int {
val res = if (statusBarHeight > 0) {
statusBarHeight
} else {
statusBarHeight = activity?.let {
val rectangle = Rect()
val window: Window = activity.window
window.decorView.getWindowVisibleDisplayFrame(rectangle)
val statusBarHeight: Int = rectangle.top
val contentView = window.findViewById<View>(Window.ID_ANDROID_CONTENT)
contentView?.let {
val contentViewTop = contentView.top
abs(contentViewTop - statusBarHeight)
}
} ?: STATUS_BAR_HEIGHT_M
statusBarHeight
}
return res
}
@JvmStatic
fun saveStatusBarHeight(height: Int) {
statusBarHeight = height
}
@JvmStatic
fun getStatusBarHeightDp(activity: Activity?): Int {
return UiUtils.pxToDp(activity, getStatusBarHeight(activity).toFloat()).toInt()
}
// region Setup
/**
* Initializes view-based system bar backgrounds for edge-to-edge.
* Call from Activity.onPostCreate after the navigator content layout is set.
*
* Status bar: reuses the system's android:id/statusBarBackground DecorView child
* when available. On API 35+ with EdgeToEdge, this view may not exist, so a
* manual view is created in the content layout, sized by status bar insets.
* Navigation bar: creates a view in [contentLayout] sized by WindowInsets,
* since the system's navigationBarBackground is not available with EdgeToEdge.
*/
@JvmStatic
fun setupSystemBarBackgrounds(activity: Activity, contentLayout: ViewGroup) {
setupStatusBarBackground(activity)
setupNavigationBarBackground(contentLayout)
}
private fun setupStatusBarBackground(activity: Activity) {
if (statusBarBackgroundView != null) return
val sbView = activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)
if (sbView != null) {
statusBarBackgroundView = sbView
} else {
statusBarBackgroundActivity = java.lang.ref.WeakReference(activity)
}
}
private fun ensureStatusBarBackgroundView(): View? {
statusBarBackgroundView?.let { return it }
val activity = statusBarBackgroundActivity?.get() ?: return null
val contentLayout = activity.findViewById<ViewGroup>(android.R.id.content) ?: return null
val view = View(activity)
val params = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, 0, Gravity.TOP
)
contentLayout.addView(view, params)
statusBarBackgroundView = view
statusBarBackgroundActivity = null
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val sbHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
val lp = v.layoutParams
if (lp.height != sbHeight) {
lp.height = sbHeight
v.layoutParams = lp
}
insets
}
view.requestApplyInsets()
return view
}
private fun setupNavigationBarBackground(contentLayout: ViewGroup) {
if (navBarBackgroundView != null) return
val view = View(contentLayout.context).apply {
setBackgroundColor(Color.BLACK)
}
val params = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, 0, Gravity.BOTTOM
)
contentLayout.addView(view, params)
navBarBackgroundView = view
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val navBarHeight = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
val tappableHeight = insets.getInsets(WindowInsetsCompat.Type.tappableElement()).bottom
val wasThreeButton = isThreeButtonNav
isThreeButtonNav = tappableHeight > 0
if (isThreeButtonNav != wasThreeButton) {
val color = lastExplicitNavBarColor ?: getDefaultNavBarColor()
v.setBackgroundColor(color)
}
val lp = v.layoutParams
if (lp.height != navBarHeight) {
lp.height = navBarHeight
v.layoutParams = lp
}
insets
}
view.requestApplyInsets()
}
/**
* Returns the default navigation bar color, applying 80% opacity for 3-button navigation.
* Gesture navigation gets a fully opaque color since the bar is minimal.
*/
@JvmStatic
fun getDefaultNavBarColor(): Int {
if (!isThreeButtonNav) return DEFAULT_NAV_BAR_COLOR
val alpha = (THREE_BUTTON_NAV_BAR_OPACITY * 255).toInt()
return Color.argb(alpha, Color.red(DEFAULT_NAV_BAR_COLOR), Color.green(DEFAULT_NAV_BAR_COLOR), Color.blue(DEFAULT_NAV_BAR_COLOR))
}
/**
* Returns true when the system statusBarBackground view was not found during setup,
* meaning a manual view will be lazily created on the first setStatusBarColor call.
* Use this to decide whether to apply a theme-based initial status bar color.
*/
@JvmStatic
fun needsManualStatusBarBackground(): Boolean = statusBarBackgroundActivity != null
/**
* Marks edge-to-edge as active. Call after EdgeToEdge.enable() in the activity.
* This flag controls whether navigation bar insets are forwarded to SafeAreaView
* and whether the view-based nav bar background is used for color changes.
*/
@JvmStatic
fun activateEdgeToEdge() {
isEdgeToEdgeActive = true
}
/**
* Clears references to system bar background views.
* Call from Activity.onDestroy to avoid leaking views across activity recreation.
*/
@JvmStatic
fun tearDown() {
statusBarBackgroundView = null
statusBarBackgroundActivity = null
navBarBackgroundView = null
isEdgeToEdgeActive = false
isThreeButtonNav = false
lastExplicitNavBarColor = null
statusBarHeight = -1
}
// endregion
// region Status Bar
@JvmStatic
fun setStatusBarColorScheme(window: Window?, view: View, isDark: Boolean) {
window?.let {
WindowInsetsControllerCompat(window, view).isAppearanceLightStatusBars = isDark
}
}
@JvmStatic
fun setStatusBarTranslucent(window: Window?) {
getStatusBarColor(window)?.let { currentColor ->
setStatusBarColor(window, currentColor, true)
}
}
@JvmStatic
fun isTranslucent(window: Window?): Boolean {
val color = getStatusBarColor(window) ?: return false
return Color.alpha(color) < 255
}
@JvmStatic
fun clearStatusBarTranslucency(window: Window?) {
getStatusBarColor(window)?.let { currentColor ->
setStatusBarColor(window, currentColor, false)
}
}
@JvmStatic
fun setStatusBarColor(window: Window?, @ColorInt color: Int, translucent: Boolean) {
val colorAlpha = Color.alpha(color)
val alpha = if (translucent && colorAlpha == 255) STATUS_BAR_HEIGHT_TRANSLUCENCY else colorAlpha / 255.0f
val opaqueColor = Color.argb(
ceil(alpha * 255).toInt(),
Color.red(color),
Color.green(color),
Color.blue(color)
)
applyStatusBarColor(window, opaqueColor)
}
/**
* Sets the status bar background color, lazily creating a manual view on API 35+
* if the system view wasn't available at setup time. Use this for explicit app-level
* color requests (e.g. from MainActivity or NavigationActivity).
*/
@JvmStatic
fun setStatusBarColor(window: Window?, color: Int) {
val view = ensureStatusBarBackgroundView()
if (view != null) {
view.setBackgroundColor(color)
} else {
@Suppress("DEPRECATION")
window?.statusBarColor = color
}
}
private fun applyStatusBarColor(window: Window?, color: Int) {
statusBarBackgroundView?.setBackgroundColor(color) ?: run {
@Suppress("DEPRECATION")
window?.statusBarColor = color
}
}
/**
* Gets the current status bar background color.
* Reads from the view-based background when available,
* falls back to the deprecated window API on older configurations.
*/
@JvmStatic
fun getStatusBarColor(window: Window?): Int? {
statusBarBackgroundView?.let { view ->
(view.background as? ColorDrawable)?.let { return it.color }
}
@Suppress("DEPRECATION")
return window?.statusBarColor
}
@JvmStatic
fun hideStatusBar(window: Window?, view: View) {
window?.let {
WindowInsetsControllerCompat(window, view).let { controller ->
controller.hide(WindowInsetsCompat.Type.statusBars())
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
@JvmStatic
fun showStatusBar(window: Window?, view: View) {
window?.let {
WindowInsetsControllerCompat(window, view).show(WindowInsetsCompat.Type.statusBars())
}
}
// endregion
// region Navigation Bar
@JvmStatic
fun hideNavigationBar(window: Window?, view: View) {
window?.let {
WindowInsetsControllerCompat(window, view).let { controller ->
controller.hide(WindowInsetsCompat.Type.navigationBars())
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
@JvmStatic
fun showNavigationBar(window: Window?, view: View) {
window?.let {
WindowInsetsControllerCompat(window, view).show(WindowInsetsCompat.Type.navigationBars())
}
}
/**
* Sets the navigation bar background color and icon appearance.
* Uses the view-based background when available (edge-to-edge),
* falls back to the deprecated window API on older configurations.
*/
@JvmStatic
fun setNavigationBarBackgroundColor(window: Window?, color: Int, lightColor: Boolean) {
lastExplicitNavBarColor = color
window?.let {
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars = lightColor
}
if (isEdgeToEdgeActive) {
navBarBackgroundView?.setBackgroundColor(color)
} else {
@Suppress("DEPRECATION")
window?.navigationBarColor = color
}
}
// endregion
}