From 0a41c840edfa27bb1cb66b276c4cd7fb2f2ecc79 Mon Sep 17 00:00:00 2001 From: sonulen Date: Thu, 18 Apr 2024 23:24:48 +0300 Subject: [PATCH] AND-81: add compose ktx --- compose-ktx/CHANGELOG.md | 10 + compose-ktx/README.md | 47 ++++ compose-ktx/build.gradle.kts | 32 +++ .../main/kotlin/FixedFontScaleContainer.kt | 44 ++++ .../src/main/kotlin/modifier/ApplyIf.kt | 32 +++ .../systemui/NavigationBarStyleEffect.kt | 50 ++++ .../kotlin/systemui/StatusBarStyleEffect.kt | 38 +++ .../kotlin/systemui/SystemBarsStyleEffect.kt | 217 ++++++++++++++++++ settings.gradle.kts | 3 + 9 files changed, 473 insertions(+) create mode 100644 compose-ktx/CHANGELOG.md create mode 100644 compose-ktx/README.md create mode 100644 compose-ktx/build.gradle.kts create mode 100644 compose-ktx/src/main/kotlin/FixedFontScaleContainer.kt create mode 100644 compose-ktx/src/main/kotlin/modifier/ApplyIf.kt create mode 100644 compose-ktx/src/main/kotlin/systemui/NavigationBarStyleEffect.kt create mode 100644 compose-ktx/src/main/kotlin/systemui/StatusBarStyleEffect.kt create mode 100644 compose-ktx/src/main/kotlin/systemui/SystemBarsStyleEffect.kt diff --git a/compose-ktx/CHANGELOG.md b/compose-ktx/CHANGELOG.md new file mode 100644 index 0000000..ec88929 --- /dev/null +++ b/compose-ktx/CHANGELOG.md @@ -0,0 +1,10 @@ +## Unreleased + +### Changed + +- Create compose-ktx + added: + - `Modifier.applyIf` - Applies the given block of modifications to the Modifier if the condition is true + - `Modifier.applyChoice` - Chooses between two blocks of modifications based on a condition and returns the resulting Modifier + - `SystemBarsStyleEffect` - changes the system UI style on lifecycle event ON_START, at ON_STOP returns the previous style + - `FixedFontScaleContainer` - A container that fixes the font scale, ignoring values, that are set in the phone's system settings diff --git a/compose-ktx/README.md b/compose-ktx/README.md new file mode 100644 index 0000000..9ced6ec --- /dev/null +++ b/compose-ktx/README.md @@ -0,0 +1,47 @@ +# compose-ktx +[![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/compose-ktx?style=flat-square)][mavenCentral] +[![License](https://img.shields.io/github/license/RedMadRobot/redmadrobot-android-ktx?style=flat-square)][license] + +--- + + + +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) + + + +Extended set of extensions for compose. + +## Installation + +Add the dependency: +```groovy +repositories { + mavenCentral() + google() +} + +dependencies { + implementation("com.redmadrobot.extensions:compose-ktx:") +} +``` + +## Usage + +| Extension | Description | +|:--------------------|:-----------| +| `Modifier.applyIf` | Applies the given block of modifications to the Modifier if the condition is true | +| `Modifier.applyChoice` | Chooses between two blocks of modifications based on a condition and returns the resulting Modifier | +| `SystemBarsStyleEffect` | changes the system UI style on lifecycle event ON_START, at ON_STOP returns the previous style | +| `FixedFontScaleContainer` | A container that fixes the font scale, ignoring values, that are set in the phone's system settings | + +## Contributing + +Merge requests are welcome. +For major changes, please open an issue first to discuss what you would like to change. + + +[mavenCentral]: https://search.maven.org/artifact/com.redmadrobot.extensions/compose-ktx +[license]: ../LICENSE diff --git a/compose-ktx/build.gradle.kts b/compose-ktx/build.gradle.kts new file mode 100644 index 0000000..2a614dc --- /dev/null +++ b/compose-ktx/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("com.redmadrobot.android-library") + id("com.redmadrobot.publish") + convention.library.android +} + +version = "1.6.6-0" +description = "Compose extensions" + +redmadrobot { + android.minSdk = 21 +} + +android { + namespace = "com.redmadrobot.compose-ktx" + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = androidx.versions.compose.compiler.get() + } +} + +dependencies { + api(androidx.compose.ui) + api(androidx.annotation) + api(androidx.compose.runtime) + api(androidx.core) + api(androidx.lifecycle.common) +} diff --git a/compose-ktx/src/main/kotlin/FixedFontScaleContainer.kt b/compose-ktx/src/main/kotlin/FixedFontScaleContainer.kt new file mode 100644 index 0000000..f07d571 --- /dev/null +++ b/compose-ktx/src/main/kotlin/FixedFontScaleContainer.kt @@ -0,0 +1,44 @@ +@file:Suppress("FunctionNaming") +package com.redmadrobot.extensions.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density + +/** + * A container that fixes the font scale, ignoring values, + * that are set in the phone's system settings + */ +@Composable +public fun FixedFontScaleContainer( + content: @Composable () -> Unit, +) { + val fixedFontScaleDensity = Density(LocalDensity.current.density) + CompositionLocalProvider( + LocalDensity provides fixedFontScaleDensity, + content = content, + ) +} + +/** + * A container that restricts the font scale, ignoring values, + * that are set in the phone's system settings + * + * @param limit - the upper limit of font enlargement + */ +@Composable +public fun LimitedFontScaleContainer( + limit: Float, + content: @Composable () -> Unit, +) { + val fontScale = LocalDensity.current.fontScale.coerceAtMost(limit) + val fixedFontScaleDensity = Density( + density = LocalDensity.current.density, + fontScale = fontScale, + ) + CompositionLocalProvider( + LocalDensity provides fixedFontScaleDensity, + content = content, + ) +} diff --git a/compose-ktx/src/main/kotlin/modifier/ApplyIf.kt b/compose-ktx/src/main/kotlin/modifier/ApplyIf.kt new file mode 100644 index 0000000..8206cf9 --- /dev/null +++ b/compose-ktx/src/main/kotlin/modifier/ApplyIf.kt @@ -0,0 +1,32 @@ +package com.redmadrobot.extensions.compose.modifier + +import androidx.compose.ui.Modifier + +/** + * Applies the given block of modifications to the Modifier if the condition is true. + * + * @param condition condition to determine if the block should be applied. + * @param block Lambda function that modifies the Modifier. + * @return Modified Modifier based on the condition. + */ +public inline fun Modifier.applyIf( + condition: Boolean, + block: Modifier.() -> Modifier, +): Modifier = applyChoice(condition = condition, trueBlock = block, falseBlock = { this }) + +/** + * Chooses between two blocks of modifications based on a condition and returns the resulting Modifier. + * + * @param condition Boolean condition to determine which block to apply. + * @param trueBlock Lambda function to modify the Modifier if the condition is true. + * @param falseBlock Lambda function to modify the Modifier if the condition is false. + * @return Modified Modifier based on the condition. + */ +public inline fun Modifier.applyChoice( + condition: Boolean, + trueBlock: Modifier.() -> Modifier, + falseBlock: Modifier.() -> Modifier, +): Modifier { + val modifier = if (condition) trueBlock() else falseBlock() + return this.then(modifier) +} diff --git a/compose-ktx/src/main/kotlin/systemui/NavigationBarStyleEffect.kt b/compose-ktx/src/main/kotlin/systemui/NavigationBarStyleEffect.kt new file mode 100644 index 0000000..97b3dcf --- /dev/null +++ b/compose-ktx/src/main/kotlin/systemui/NavigationBarStyleEffect.kt @@ -0,0 +1,50 @@ +@file:Suppress("FunctionNaming") +package com.redmadrobot.extensions.compose.systemui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +/** + * Changes the color of navigation icons on [Event.ON_START], and reverts to the previous color on [Event.ON_STOP]. + * @param darkIcons Whether to use dark or light icons in navigation + * @param withScrim Whether to add a scrim to the navigation for additional contrast of icons + */ +@Composable +public fun NavigationBarStyleEffect( + darkIcons: Boolean, + withScrim: Boolean = false, +) { + NavigationBarStyleEffect( + color = scrimOrTransparent(withScrim, darkIcons), + darkIcons = darkIcons, + ) +} + +/** + * Changes the color of the navigation on [Event.ON_START], and reverts to the previous color on [Event.ON_STOP]. + * The color of the navigation icons will be automatically selected based on the provided color. + * @param color The color to which the navigation will be recolored + */ +@Composable +public fun NavigationBarStyleEffect(color: Color) { + NavigationBarStyleEffect( + color = color, + darkIcons = color.isLight(), + ) +} + +/** + * Changes the style of navigation on [Event.ON_START], and reverts to the previous style on [Event.ON_STOP]. + * @param color The color to which the navigation will be recolored + * @param darkIcons Whether to use dark or light icons in navigation + */ +@Composable +public fun NavigationBarStyleEffect( + color: Color, + darkIcons: Boolean, +) { + SystemBarsStyleEffect( + statusBarStyle = SystemBarStyle.Unspecified, + navigationBarStyle = rememberSystemBarStyle(color, darkIcons), + ) +} diff --git a/compose-ktx/src/main/kotlin/systemui/StatusBarStyleEffect.kt b/compose-ktx/src/main/kotlin/systemui/StatusBarStyleEffect.kt new file mode 100644 index 0000000..ea1d4d4 --- /dev/null +++ b/compose-ktx/src/main/kotlin/systemui/StatusBarStyleEffect.kt @@ -0,0 +1,38 @@ +@file:Suppress("FunctionNaming") +package com.redmadrobot.extensions.compose.systemui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.Lifecycle.Event + +/** + * When [Event.ON_START] changes the color of status bar icons, when [Event.ON_STOP] returns the previous color + * @param darkIcons Whether to use dark or light icons in the status bar + * @param withScrim Whether to add darkening/lightening of the status bar for additional contrast of icons + */ +@Composable +public fun StatusBarStyleEffect( + darkIcons: Boolean, + withScrim: Boolean = false, +) { + StatusBarStyleEffect( + color = scrimOrTransparent(withScrim, darkIcons), + darkIcons = darkIcons, + ) +} + +/** + * When [Event.ON_START] changes the status bar style, when [Event.ON_STOP] returns the previous style + * @param color The color in which the status bar will be repainted + * @param darkIcons Whether to use dark or light icons in the status bar + */ +@Composable +public fun StatusBarStyleEffect( + color: Color, + darkIcons: Boolean, +) { + SystemBarsStyleEffect( + statusBarStyle = rememberSystemBarStyle(color, darkIcons), + navigationBarStyle = SystemBarStyle.Unspecified, + ) +} diff --git a/compose-ktx/src/main/kotlin/systemui/SystemBarsStyleEffect.kt b/compose-ktx/src/main/kotlin/systemui/SystemBarsStyleEffect.kt new file mode 100644 index 0000000..cbcf673 --- /dev/null +++ b/compose-ktx/src/main/kotlin/systemui/SystemBarsStyleEffect.kt @@ -0,0 +1,217 @@ +@file:Suppress("FunctionNaming") +package com.redmadrobot.extensions.compose.systemui + +import android.app.Activity +import android.os.Build +import android.view.Window +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.view.WindowCompat +import androidx.lifecycle.Lifecycle.Event +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner + +/** + * When [Event.ON_START] changes the system UI style, when [Event.ON_STOP] returns the previous style. + * @param darkIcons Whether to use dark or light icons + * @param withScrim Whether to add darkening/lightening for additional icon contrast + */ +@Composable +public fun SystemBarsStyleEffect( + darkIcons: Boolean, + withScrim: Boolean = false, +) { + SystemBarsStyleEffect( + color = scrimOrTransparent(withScrim, darkIcons), + darkIcons = darkIcons, + ) +} + +/** + * When [Event.ON_START] changes the system UI style, when [Event.ON_STOP] returns the previous style + * @param color The color in which the system bars will be repainted + * @param darkIcons Whether to use dark or light icons in system bars + */ +@Composable +public fun SystemBarsStyleEffect( + color: Color, + darkIcons: Boolean, +) { + val style = rememberSystemBarStyle(color = color, darkIcons = darkIcons) + SystemBarsStyleEffect( + statusBarStyle = style, + navigationBarStyle = style + ) +} + +/** + * At [Event.ON_START] changes the system UI style, at [Event.ON_STOP] returns the previous style + * @param statusBarStyle Status Bar Style + * @param navigationBarStyle Navigation style + */ +@Composable +public fun SystemBarsStyleEffect( + statusBarStyle: SystemBarStyle, + navigationBarStyle: SystemBarStyle, +) { + val window = (LocalContext.current as? Activity)?.window ?: return + val statusBarColorChanger = rememberStatusBarColorChanger(window, statusBarStyle, navigationBarStyle) + val lifecycle = LocalLifecycleOwner.current.lifecycle + + DisposableEffect(statusBarColorChanger, lifecycle) { + lifecycle.addObserver(statusBarColorChanger) + onDispose { lifecycle.removeObserver(statusBarColorChanger) } + } +} + +@Composable +private fun rememberStatusBarColorChanger( + window: Window, + statusBarStyle: SystemBarStyle, + navigationBarStyle: SystemBarStyle, +): SystemBarsStyleChanger { + return remember(window, statusBarStyle, navigationBarStyle) { + SystemBarsStyleChanger(window, statusBarStyle, navigationBarStyle) + } +} + +private class SystemBarsStyleChanger( + private val window: Window, + private val statusBarStyle: SystemBarStyle, + private val navigationBarStyle: SystemBarStyle, +) : LifecycleEventObserver { + + private val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + + private var originalStatusStyle = SystemBarStyle.Unspecified + private var originalNavigationStyle = SystemBarStyle.Unspecified + + override fun onStateChanged(source: LifecycleOwner, event: Event) { + when (event) { + Event.ON_START -> { + if (statusBarStyle.isSpecified) setStatusBarAppearance() + if (navigationBarStyle.isSpecified) setNavigationBarAppearance() + } + + Event.ON_STOP -> { + if (statusBarStyle.isSpecified) resetStatusBarAppearance() + if (navigationBarStyle.isSpecified) resetNavigationBarAppearance() + } + + else -> Unit // no-op + } + } + + // region Status Bar + private fun setStatusBarAppearance() { + originalStatusStyle = SystemBarStyle( + color = Color(window.statusBarColor), + darkIcons = windowInsetsController.isAppearanceLightStatusBars, + ) + setStatusBarColor(statusBarStyle) + } + + private fun resetStatusBarAppearance() { + setStatusBarColor(originalStatusStyle) + } + + private fun setStatusBarColor(style: SystemBarStyle) { + windowInsetsController.isAppearanceLightStatusBars = style.darkIcons + style.useColor { window.statusBarColor = it } + } + // endregion + + // region Navigation Bar + private fun setNavigationBarAppearance() { + originalNavigationStyle = SystemBarStyle( + color = Color(window.navigationBarColor), + darkIcons = windowInsetsController.isAppearanceLightNavigationBars, + ) + setNavigationBarColor(navigationBarStyle) + } + + private fun resetNavigationBarAppearance() { + setNavigationBarColor(originalNavigationStyle) + } + + @Suppress("BooleanPropertyNaming") + private fun setNavigationBarColor(style: SystemBarStyle) { + windowInsetsController.isAppearanceLightNavigationBars = style.darkIcons + // If we're set to use dark icons, but our windowInsetsController call didn't + // succeed (usually due to API level), we instead transform the color to maintain + // contrast + val failedToApplyDarkIcons = style.darkIcons && !windowInsetsController.isAppearanceLightNavigationBars + style.useColor(scrimmed = failedToApplyDarkIcons) { color -> + window.navigationBarColor = color + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + window.navigationBarDividerColor = color + } + } + } + // endregion +} + +/** + * Creates a [SystemBarStyle] style with the given parameters. + * @param color The color in which the system bar will be repainted + * @param darkIcons Whether to use dark or light icons in the system bar + * @see SystemBarsStyleEffect + */ +@Composable +public fun rememberSystemBarStyle(color: Color, darkIcons: Boolean): SystemBarStyle { + return remember(color, darkIcons) { SystemBarStyle(color, darkIcons) } +} + +/** @see rememberSystemBarStyle */ +@Suppress("BooleanPropertyNaming") +@Immutable +public data class SystemBarStyle( + val color: Color, + val darkIcons: Boolean, +) { + internal val isSpecified: Boolean + get() = this !== Unspecified + + public companion object { + public val Unspecified: SystemBarStyle = SystemBarStyle( + color = Color.Unspecified, + darkIcons = false, + ) + } +} + +private inline fun SystemBarStyle.useColor(scrimmed: Boolean = false, block: (Int) -> Unit) { + if (color.isSpecified) { + val contrastColor = if (scrimmed && color.alpha == 1f) { + DarkScrim.compositeOver(color) + } else { + color + } + block(contrastColor.toArgb()) + } +} + +@Suppress("MagicNumber") +internal fun Color.isLight(): Boolean = luminance() > 0.5f + +internal fun scrimOrTransparent(withScrim: Boolean, darkIcons: Boolean): Color = when { + withScrim && darkIcons -> LightScrim + withScrim && !darkIcons -> DarkScrim + else -> Color.Transparent +} + +// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/res/color/system_bar_background_semi_transparent.xml +// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/remote_color_resources_res/values/colors.xml;l=67 +internal val DarkScrim = Color(red = 0.1f, green = 0.1f, blue = 0.1f, alpha = 0.5f) + +// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/policy/DecorView.java;drc=6ef0f022c333385dba2c294e35b8de544455bf19;l=142 +internal val LightScrim = Color(red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 0.9f) diff --git a/settings.gradle.kts b/settings.gradle.kts index fe8f7ec..6570839 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,5 @@ +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + pluginManagement { repositories { google() @@ -34,4 +36,5 @@ include( "lifecycle-livedata-ktx", "resources-ktx", "viewbinding-ktx", + "compose-ktx", )