1 /* 2 * 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 175 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 183 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 { 202 private val windowInsetsController = window?.let { WindowCompat.getInsetsController(it, view) } 203 204 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 223 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 292 private val BlackScrimmed: (Color) -> Color = { original -> BlackScrim.compositeOver(original) } 293