1 /*
<lambda>null2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.compose
18
19 import android.app.Activity
20 import android.content.Context
21 import android.content.ContextWrapper
22 import android.os.Build
23 import android.view.View
24 import android.view.Window
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.Stable
27 import androidx.compose.runtime.remember
28 import androidx.compose.ui.graphics.Color
29 import androidx.compose.ui.graphics.compositeOver
30 import androidx.compose.ui.graphics.luminance
31 import androidx.compose.ui.graphics.toArgb
32 import androidx.compose.ui.platform.LocalView
33 import androidx.compose.ui.window.DialogWindowProvider
34 import androidx.core.view.ViewCompat
35 import androidx.core.view.WindowCompat
36 import androidx.core.view.WindowInsetsCompat
37
38 /**
39 * *************************************************************************************************
40 * This file was forked from
41 * https://github.com/google/accompanist/blob/main/systemuicontroller/src/main/java/com/google/accompanist/systemuicontroller/SystemUiController.kt
42 * and will be removed once it lands in AndroidX.
43 */
44
45 /**
46 * A class which provides easy-to-use utilities for updating the System UI bar colors within Jetpack
47 * Compose.
48 *
49 * @sample com.google.accompanist.sample.systemuicontroller.SystemUiControllerSample
50 */
51 @Stable
52 interface SystemUiController {
53
54 /**
55 * Property which holds the status bar visibility. If set to true, show the status bar,
56 * otherwise hide the status bar.
57 */
58 var isStatusBarVisible: Boolean
59
60 /**
61 * Property which holds the navigation bar visibility. If set to true, show the navigation bar,
62 * otherwise hide the navigation bar.
63 */
64 var isNavigationBarVisible: Boolean
65
66 /**
67 * Property which holds the status & navigation bar visibility. If set to true, show both bars,
68 * otherwise hide both bars.
69 */
70 var isSystemBarsVisible: Boolean
71 get() = isNavigationBarVisible && isStatusBarVisible
72 set(value) {
73 isStatusBarVisible = value
74 isNavigationBarVisible = value
75 }
76
77 /**
78 * Set the status bar color.
79 *
80 * @param color The **desired** [Color] to set. This may require modification if running on an
81 * API level that only supports white status bar icons.
82 * @param darkIcons Whether dark status bar icons would be preferable.
83 * @param transformColorForLightContent A lambda which will be invoked to transform [color] if
84 * dark icons were requested but are not available. Defaults to applying a black scrim.
85 * @see statusBarDarkContentEnabled
86 */
87 fun setStatusBarColor(
88 color: Color,
89 darkIcons: Boolean = color.luminance() > 0.5f,
90 transformColorForLightContent: (Color) -> Color = BlackScrimmed
91 )
92
93 /**
94 * Set the navigation bar color.
95 *
96 * @param color The **desired** [Color] to set. This may require modification if running on an
97 * API level that only supports white navigation bar icons. Additionally this will be ignored
98 * and [Color.Transparent] will be used on API 29+ where gesture navigation is preferred or
99 * the system UI automatically applies background protection in other navigation modes.
100 * @param darkIcons Whether dark navigation bar icons would be preferable.
101 * @param navigationBarContrastEnforced Whether the system should ensure that the navigation bar
102 * has enough contrast when a fully transparent background is requested. Only supported on API
103 * 29+.
104 * @param transformColorForLightContent A lambda which will be invoked to transform [color] if
105 * dark icons were requested but are not available. Defaults to applying a black scrim.
106 * @see navigationBarDarkContentEnabled
107 * @see navigationBarContrastEnforced
108 */
109 fun setNavigationBarColor(
110 color: Color,
111 darkIcons: Boolean = color.luminance() > 0.5f,
112 navigationBarContrastEnforced: Boolean = true,
113 transformColorForLightContent: (Color) -> Color = BlackScrimmed
114 )
115
116 /**
117 * Set the status and navigation bars to [color].
118 *
119 * @see setStatusBarColor
120 * @see setNavigationBarColor
121 */
122 fun setSystemBarsColor(
123 color: Color,
124 darkIcons: Boolean = color.luminance() > 0.5f,
125 isNavigationBarContrastEnforced: Boolean = true,
126 transformColorForLightContent: (Color) -> Color = BlackScrimmed
127 ) {
128 setStatusBarColor(color, darkIcons, transformColorForLightContent)
129 setNavigationBarColor(
130 color,
131 darkIcons,
132 isNavigationBarContrastEnforced,
133 transformColorForLightContent
134 )
135 }
136
137 /** Property which holds whether the status bar icons + content are 'dark' or not. */
138 var statusBarDarkContentEnabled: Boolean
139
140 /** Property which holds whether the navigation bar icons + content are 'dark' or not. */
141 var navigationBarDarkContentEnabled: Boolean
142
143 /**
144 * Property which holds whether the status & navigation bar icons + content are 'dark' or not.
145 */
146 var systemBarsDarkContentEnabled: Boolean
147 get() = statusBarDarkContentEnabled && navigationBarDarkContentEnabled
148 set(value) {
149 statusBarDarkContentEnabled = value
150 navigationBarDarkContentEnabled = value
151 }
152
153 /**
154 * Property which holds whether the system is ensuring that the navigation bar has enough
155 * contrast when a fully transparent background is requested. Only has an affect when running on
156 * Android API 29+ devices.
157 */
158 var isNavigationBarContrastEnforced: Boolean
159 }
160
161 /**
162 * Remembers a [SystemUiController] for the given [window].
163 *
164 * If no [window] is provided, an attempt to find the correct [Window] is made.
165 *
166 * First, if the [LocalView]'s parent is a [DialogWindowProvider], then that dialog's [Window] will
167 * be used.
168 *
169 * Second, we attempt to find [Window] for the [Activity] containing the [LocalView].
170 *
171 * If none of these are found (such as may happen in a preview), then the functionality of the
172 * returned [SystemUiController] will be degraded, but won't throw an exception.
173 */
174 @Composable
rememberSystemUiControllernull175 fun rememberSystemUiController(
176 window: Window? = findWindow(),
177 ): SystemUiController {
178 val view = LocalView.current
179 return remember(view, window) { AndroidSystemUiController(view, window) }
180 }
181
182 @Composable
findWindownull183 private fun findWindow(): Window? =
184 (LocalView.current.parent as? DialogWindowProvider)?.window
185 ?: LocalView.current.context.findWindow()
186
187 private tailrec fun Context.findWindow(): Window? =
188 when (this) {
189 is Activity -> window
190 is ContextWrapper -> baseContext.findWindow()
191 else -> null
192 }
193
194 /**
195 * A helper class for setting the navigation and status bar colors for a [View], gracefully
196 * degrading behavior based upon API level.
197 *
198 * Typically you would use [rememberSystemUiController] to remember an instance of this.
199 */
200 internal class AndroidSystemUiController(private val view: View, private val window: Window?) :
201 SystemUiController {
<lambda>null202 private val windowInsetsController = window?.let { WindowCompat.getInsetsController(it, view) }
203
setStatusBarColornull204 override fun setStatusBarColor(
205 color: Color,
206 darkIcons: Boolean,
207 transformColorForLightContent: (Color) -> Color
208 ) {
209 statusBarDarkContentEnabled = darkIcons
210
211 window?.statusBarColor =
212 when {
213 darkIcons && windowInsetsController?.isAppearanceLightStatusBars != true -> {
214 // If we're set to use dark icons, but our windowInsetsController call didn't
215 // succeed (usually due to API level), we instead transform the color to
216 // maintain contrast
217 transformColorForLightContent(color)
218 }
219 else -> color
220 }.toArgb()
221 }
222
setNavigationBarColornull223 override fun setNavigationBarColor(
224 color: Color,
225 darkIcons: Boolean,
226 navigationBarContrastEnforced: Boolean,
227 transformColorForLightContent: (Color) -> Color
228 ) {
229 navigationBarDarkContentEnabled = darkIcons
230 isNavigationBarContrastEnforced = navigationBarContrastEnforced
231
232 window?.navigationBarColor =
233 when {
234 darkIcons && windowInsetsController?.isAppearanceLightNavigationBars != true -> {
235 // If we're set to use dark icons, but our windowInsetsController call didn't
236 // succeed (usually due to API level), we instead transform the color to
237 // maintain contrast
238 transformColorForLightContent(color)
239 }
240 else -> color
241 }.toArgb()
242 }
243
244 override var isStatusBarVisible: Boolean
245 get() {
246 return ViewCompat.getRootWindowInsets(view)
247 ?.isVisible(WindowInsetsCompat.Type.statusBars()) == true
248 }
249 set(value) {
250 if (value) {
251 windowInsetsController?.show(WindowInsetsCompat.Type.statusBars())
252 } else {
253 windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars())
254 }
255 }
256
257 override var isNavigationBarVisible: Boolean
258 get() {
259 return ViewCompat.getRootWindowInsets(view)
260 ?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true
261 }
262 set(value) {
263 if (value) {
264 windowInsetsController?.show(WindowInsetsCompat.Type.navigationBars())
265 } else {
266 windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars())
267 }
268 }
269
270 override var statusBarDarkContentEnabled: Boolean
271 get() = windowInsetsController?.isAppearanceLightStatusBars == true
272 set(value) {
273 windowInsetsController?.isAppearanceLightStatusBars = value
274 }
275
276 override var navigationBarDarkContentEnabled: Boolean
277 get() = windowInsetsController?.isAppearanceLightNavigationBars == true
278 set(value) {
279 windowInsetsController?.isAppearanceLightNavigationBars = value
280 }
281
282 override var isNavigationBarContrastEnforced: Boolean
283 get() = Build.VERSION.SDK_INT >= 29 && window?.isNavigationBarContrastEnforced == true
284 set(value) {
285 if (Build.VERSION.SDK_INT >= 29) {
286 window?.isNavigationBarContrastEnforced = value
287 }
288 }
289 }
290
291 private val BlackScrim = Color(0f, 0f, 0f, 0.3f) // 30% opaque black
originalnull292 private val BlackScrimmed: (Color) -> Color = { original -> BlackScrim.compositeOver(original) }
293