mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 15:26:58 +00:00
Custom Notification Dot Counter Color Support (#2977)
* Add `notificationDotTextColor` to `PreferenceManager2` * Add `notificationDotTextColor` to preferences UI * Pass counter color to `DotRenderer` * Convert `LauncherDefault` to `Default` * Optimize imports * Rename "Default" to "Managed by Lawnchair" * Use the correct `ColorDot` on `ColorPreference` * Show a contrast warning for dot colors * Show a special icon on `ColorDot` when the color is 0 * Make `ColorDot(Color)` private to avoid accidental usages * Create `WarningPreference` as a reusable composable for warnings * Use Rounded icon on `WarningPreference` * Fix `ColorContrastWarning.kt`'s file name * Update platform_frameworks_libs_systemui Co-authored-by: Daria Hamrah Paytakht <info@dariarnd.ir>
This commit is contained in:
@@ -48,11 +48,12 @@
|
||||
<string name="config_default_smart_space_calendar" translatable="false">gregorian</string>
|
||||
|
||||
<!-- which accent color to use by default -->
|
||||
<string name="config_default_accent_color" translatable="false">default</string>
|
||||
<string name="config_default_accent_color" translatable="false">no_default</string>
|
||||
|
||||
<!-- which notification dot color to use by default -->
|
||||
<string name="config_default_notification_dot_color" translatable="false">app_icon</string>
|
||||
<string name="config_default_folder_color" translatable="false">launcher_default</string>
|
||||
<string name="config_default_notification_dot_color" translatable="false">default</string>
|
||||
<string name="config_default_notification_dot_text_color" translatable="false">default</string>
|
||||
<string name="config_default_folder_color" translatable="false">default</string>
|
||||
|
||||
<!-- which time format to use by default on smartspace -->
|
||||
<string name="config_default_smartspace_time_format" translatable="false">system</string>
|
||||
|
||||
@@ -75,6 +75,9 @@
|
||||
<!-- <string name="notification_dots" />-->
|
||||
<string name="show_notification_count">Show Notification Count</string>
|
||||
<string name="notification_dots_color">Notification Dot Color</string>
|
||||
<string name="notification_dots_text_color">Notification Count Color</string>
|
||||
<string name="notification_dots_color_contrast_warning_always">Warning: Notification Dot & Counter colors do not have enough contrast with each other</string>
|
||||
<string name="notification_dots_color_contrast_warning_sometimes">Warning: Notification Dot & Counter colors might not always have enough contrast with each other</string>
|
||||
|
||||
<!-- Notification popup -->
|
||||
<string name="missing_notification_access_label">Notification Access Needed</string>
|
||||
@@ -156,7 +159,7 @@
|
||||
<string name="action_paste">Paste</string>
|
||||
<string name="copied_toast">Copied to clipboard.</string>
|
||||
<string name="invalid_color">Invalid Color</string>
|
||||
<string name="launcher_default_color">Default</string>
|
||||
<string name="launcher_default_color">Managed by Lawnchair</string>
|
||||
|
||||
<!-- HomeScreenPreferences -->
|
||||
<!-- <string name="general_label" /> -->
|
||||
|
||||
@@ -100,6 +100,14 @@ class PreferenceManager2(private val context: Context) : PreferenceManager {
|
||||
defaultValue = ColorOption.fromString(context.getString(R.string.config_default_notification_dot_color)),
|
||||
)
|
||||
|
||||
val notificationDotTextColor = preference(
|
||||
key = stringPreferencesKey(name = "notification_dot_text_color"),
|
||||
parse = ColorOption::fromString,
|
||||
save = ColorOption::toString,
|
||||
onSet = { reloadHelper.reloadGrid() },
|
||||
defaultValue = ColorOption.fromString(context.getString(R.string.config_default_notification_dot_text_color)),
|
||||
)
|
||||
|
||||
val folderColor = preference(
|
||||
key = stringPreferencesKey(name = "folder_color"),
|
||||
parse = ColorOption::fromString,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.lawnchair.theme.color
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceEntry
|
||||
import app.lawnchair.ui.theme.getSystemAccent
|
||||
@@ -61,7 +60,7 @@ sealed class ColorOption {
|
||||
override fun toString() = "custom|#${String.format("%08x", color)}"
|
||||
}
|
||||
|
||||
object LauncherDefault : ColorOption() {
|
||||
object Default : ColorOption() {
|
||||
override val isSupported = false
|
||||
|
||||
override val colorPreferenceEntry = ColorPreferenceEntry<ColorOption>(
|
||||
@@ -70,7 +69,7 @@ sealed class ColorOption {
|
||||
{ 0 }
|
||||
)
|
||||
|
||||
override fun toString() = "launcher_default"
|
||||
override fun toString() = "default"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -79,7 +78,7 @@ sealed class ColorOption {
|
||||
fun fromString(stringValue: String) = when (stringValue) {
|
||||
"system_accent" -> SystemAccent
|
||||
"wallpaper_primary" -> WallpaperPrimary
|
||||
"launcher_default" -> LauncherDefault
|
||||
"default" -> Default
|
||||
else -> instantiateCustomColor(stringValue)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import app.lawnchair.preferences.getAdapter
|
||||
import app.lawnchair.preferences.preferenceManager
|
||||
import app.lawnchair.preferences2.asState
|
||||
import app.lawnchair.preferences2.preferenceManager2
|
||||
import app.lawnchair.theme.color.ColorOption
|
||||
import app.lawnchair.ui.preferences.components.DividerColumn
|
||||
import app.lawnchair.ui.preferences.components.ExpandAndShrink
|
||||
import app.lawnchair.ui.preferences.components.FontPreference
|
||||
import app.lawnchair.ui.preferences.components.IconShapePreview
|
||||
@@ -37,6 +39,8 @@ import app.lawnchair.ui.preferences.components.PreferenceLayout
|
||||
import app.lawnchair.ui.preferences.components.SliderPreference
|
||||
import app.lawnchair.ui.preferences.components.SwitchPreference
|
||||
import app.lawnchair.ui.preferences.components.ThemePreference
|
||||
import app.lawnchair.ui.preferences.components.WarningPreference
|
||||
import app.lawnchair.ui.preferences.components.colorpreference.ColorContrastWarning
|
||||
import app.lawnchair.ui.preferences.components.colorpreference.ColorPreference
|
||||
import app.lawnchair.ui.preferences.components.iconShapeEntries
|
||||
import app.lawnchair.ui.preferences.components.iconShapeGraph
|
||||
@@ -99,7 +103,7 @@ fun GeneralPreferences() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val wrapAdaptiveIcons = prefs.wrapAdaptiveIcons.getAdapter()
|
||||
PreferenceGroup(
|
||||
heading = stringResource(id = R.string.icons),
|
||||
@@ -148,15 +152,52 @@ fun GeneralPreferences() {
|
||||
val serviceEnabled = notificationServiceEnabled()
|
||||
NotificationDotsPreference(enabled = enabled, serviceEnabled = serviceEnabled)
|
||||
if (enabled && serviceEnabled) {
|
||||
SwitchPreference(
|
||||
adapter = prefs2.showNotificationCount.getAdapter(),
|
||||
label = stringResource(id = R.string.show_notification_count),
|
||||
)
|
||||
val showNotificationCountAdapter = prefs2.showNotificationCount.getAdapter()
|
||||
ColorPreference(
|
||||
preference = prefs2.notificationDotColor,
|
||||
label = stringResource(id = R.string.notification_dots_color),
|
||||
)
|
||||
SwitchPreference(
|
||||
adapter = showNotificationCountAdapter,
|
||||
label = stringResource(id = R.string.show_notification_count),
|
||||
)
|
||||
ExpandAndShrink(visible = showNotificationCountAdapter.state.value) {
|
||||
DividerColumn {
|
||||
ColorPreference(
|
||||
preference = prefs2.notificationDotTextColor,
|
||||
label = stringResource(id = R.string.notification_dots_text_color),
|
||||
)
|
||||
NotificationDotColorContrastWarnings(
|
||||
dotColor = prefs2.notificationDotColor.asState().value,
|
||||
dotTextColor = prefs2.notificationDotTextColor.asState().value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationDotColorContrastWarnings(
|
||||
dotColor: ColorOption,
|
||||
dotTextColor: ColorOption,
|
||||
) {
|
||||
|
||||
val dotColorIsDynamic = when (dotColor) {
|
||||
is ColorOption.SystemAccent,
|
||||
is ColorOption.WallpaperPrimary,
|
||||
is ColorOption.Default -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (dotColorIsDynamic && dotTextColor !is ColorOption.Default) {
|
||||
WarningPreference(text = stringResource(id = R.string.notification_dots_color_contrast_warning_sometimes))
|
||||
} else {
|
||||
ColorContrastWarning(
|
||||
foregroundColor = dotTextColor,
|
||||
backgroundColor = dotColor,
|
||||
text = stringResource(id = R.string.notification_dots_color_contrast_warning_always),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.lawnchair.ui.preferences.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun WarningPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
) {
|
||||
PreferenceTemplate(
|
||||
modifier = modifier,
|
||||
title = {},
|
||||
description = {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
startWidget = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package app.lawnchair.ui.preferences.components.colorpreference
|
||||
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import app.lawnchair.theme.color.ColorOption
|
||||
import app.lawnchair.ui.preferences.components.WarningPreference
|
||||
|
||||
private const val CONTRAST_THRESHOLD = 1.5
|
||||
|
||||
/**
|
||||
* Displays [WarningPreference] when [foregroundColor] & [backgroundColor] have less than optimal contrast with each other.
|
||||
*
|
||||
* @see CONTRAST_THRESHOLD
|
||||
*/
|
||||
@Composable
|
||||
fun ColorContrastWarning(
|
||||
modifier: Modifier = Modifier,
|
||||
foregroundColor: ColorOption,
|
||||
backgroundColor: ColorOption,
|
||||
text: String,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val foregroundColorInt = foregroundColor.colorPreferenceEntry.lightColor(context)
|
||||
val backgroundColorInt = backgroundColor.colorPreferenceEntry.lightColor(context)
|
||||
ColorContrastWarning(
|
||||
modifier = modifier,
|
||||
foregroundColor = foregroundColorInt,
|
||||
backgroundColor = backgroundColorInt,
|
||||
text = text,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays [WarningPreference] when [foregroundColor] & [backgroundColor] have less than optimal contrast with each other.
|
||||
*
|
||||
* @see CONTRAST_THRESHOLD
|
||||
*/
|
||||
@Composable
|
||||
fun ColorContrastWarning(
|
||||
modifier: Modifier = Modifier,
|
||||
@ColorInt foregroundColor: Int,
|
||||
@ColorInt backgroundColor: Int,
|
||||
text: String,
|
||||
) {
|
||||
|
||||
val enoughContrast = if (foregroundColor != 0 && backgroundColor != 0) {
|
||||
ColorUtils.calculateContrast(
|
||||
foregroundColor,
|
||||
backgroundColor,
|
||||
) >= CONTRAST_THRESHOLD
|
||||
} else true
|
||||
|
||||
if (!enoughContrast) {
|
||||
WarningPreference(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,11 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.HdrAuto
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -17,20 +21,27 @@ fun <T> ColorDot(
|
||||
entry: ColorPreferenceEntry<T>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val colorLight = entry.lightColor(context)
|
||||
val colorDark = entry.darkColor(context)
|
||||
|
||||
val color = if (MaterialTheme.colors.isLight) {
|
||||
entry.lightColor(LocalContext.current)
|
||||
colorLight
|
||||
} else {
|
||||
entry.darkColor(LocalContext.current)
|
||||
colorDark
|
||||
}
|
||||
|
||||
ColorDot(
|
||||
color = Color(color),
|
||||
modifier = modifier
|
||||
)
|
||||
if (colorLight != 0) {
|
||||
ColorDot(
|
||||
color = Color(color),
|
||||
modifier = modifier
|
||||
)
|
||||
} else DefaultColorDot(modifier = modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorDot(
|
||||
private fun ColorDot(
|
||||
color: Color,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -38,6 +49,25 @@ fun ColorDot(
|
||||
modifier = modifier
|
||||
.size(30.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = color)
|
||||
.background(color = color),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultColorDot(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(30.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = androidx.compose.material3.MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.HdrAuto,
|
||||
contentDescription = null,
|
||||
tint = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,4 @@ val dynamicColors = listOf(ColorOption.SystemAccent, ColorOption.WallpaperPrimar
|
||||
.filter(ColorOption::isSupported)
|
||||
.map(ColorOption::colorPreferenceEntry)
|
||||
|
||||
val dynamicColorsWithDefault = dynamicColors + listOf(ColorOption.LauncherDefault.colorPreferenceEntry)
|
||||
val dynamicColorsWithDefault = dynamicColors + listOf(ColorOption.Default.colorPreferenceEntry)
|
||||
|
||||
@@ -44,7 +44,9 @@ fun ColorPreference(
|
||||
val navController = LocalNavController.current
|
||||
PreferenceTemplate(
|
||||
title = { Text(text = label) },
|
||||
endWidget = { ColorDot(Color(adapter.state.value.colorPreferenceEntry.lightColor(LocalContext.current))) },
|
||||
endWidget = {
|
||||
ColorDot(adapter.state.value.colorPreferenceEntry)
|
||||
},
|
||||
description = {
|
||||
Text(text = adapter.state.value.colorPreferenceEntry.label())
|
||||
},
|
||||
|
||||
@@ -45,18 +45,21 @@ fun NavGraphBuilder.colorSelectionGraph(route: String) {
|
||||
val pref = when (prefKey) {
|
||||
preferenceManager2.accentColor.key.name -> preferenceManager2.accentColor
|
||||
preferenceManager2.notificationDotColor.key.name -> preferenceManager2.notificationDotColor
|
||||
preferenceManager2.notificationDotTextColor.key.name -> preferenceManager2.notificationDotTextColor
|
||||
preferenceManager2.folderColor.key.name -> preferenceManager2.folderColor
|
||||
else -> return@composable
|
||||
}
|
||||
val label = when (prefKey) {
|
||||
preferenceManager2.accentColor.key.name -> stringResource(id = R.string.accent_color)
|
||||
preferenceManager2.notificationDotColor.key.name -> stringResource(id = R.string.notification_dots_color)
|
||||
preferenceManager2.notificationDotTextColor.key.name -> stringResource(id = R.string.notification_dots_text_color)
|
||||
preferenceManager2.folderColor.key.name -> stringResource(id = R.string.folder_preview_bg_color_label)
|
||||
else -> return@composable
|
||||
}
|
||||
val dynamicEntries = when (prefKey) {
|
||||
preferenceManager2.folderColor.key.name,
|
||||
preferenceManager2.notificationDotColor.key.name -> dynamicColorsWithDefault
|
||||
preferenceManager2.notificationDotColor.key.name,
|
||||
preferenceManager2.notificationDotTextColor.key.name -> dynamicColorsWithDefault
|
||||
else -> dynamicColors
|
||||
}
|
||||
ColorSelection(
|
||||
|
||||
Submodule platform_frameworks_libs_systemui updated: b634f78d6c...6c2c80fa8c
@@ -504,14 +504,18 @@ public class DeviceProfile {
|
||||
}
|
||||
|
||||
// Load dot color
|
||||
ColorOption colorOption = PreferenceExtensionsKt.firstBlocking(preferenceManager2.getNotificationDotColor());
|
||||
int color = colorOption.getColorPreferenceEntry().getLightColor().invoke(context);
|
||||
ColorOption dotColorOption = PreferenceExtensionsKt.firstBlocking(preferenceManager2.getNotificationDotColor());
|
||||
int dotColor = dotColorOption.getColorPreferenceEntry().getLightColor().invoke(context);
|
||||
|
||||
// Load counter color
|
||||
ColorOption counterColorOption = PreferenceExtensionsKt.firstBlocking(preferenceManager2.getNotificationDotTextColor());
|
||||
int countColor = counterColorOption.getColorPreferenceEntry().getLightColor().invoke(context);
|
||||
|
||||
// This is done last, after iconSizePx is calculated above.
|
||||
Path dotPath = GraphicsUtils.getShapePath(DEFAULT_DOT_SIZE);
|
||||
mDotRendererWorkSpace = new DotRenderer(iconSizePx, dotPath, DEFAULT_DOT_SIZE, showNotificationCount, typeface, color);
|
||||
mDotRendererWorkSpace = new DotRenderer(iconSizePx, dotPath, DEFAULT_DOT_SIZE, showNotificationCount, typeface, dotColor, countColor);
|
||||
mDotRendererAllApps = iconSizePx == allAppsIconSizePx ? mDotRendererWorkSpace :
|
||||
new DotRenderer(allAppsIconSizePx, dotPath, DEFAULT_DOT_SIZE, showNotificationCount, typeface, color);
|
||||
new DotRenderer(allAppsIconSizePx, dotPath, DEFAULT_DOT_SIZE, showNotificationCount, typeface, dotColor, countColor);
|
||||
}
|
||||
|
||||
private int getHorizontalMarginPx(InvariantDeviceProfile idp, Resources res) {
|
||||
|
||||
Reference in New Issue
Block a user