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