Color selector improvements (#2991)

* Refactor color preference args

* Combine "Dynamic" and "Presets" page

* Fix color picker initialized as transparent

* Extract duplicate hsv conversion calls

* Align HSV picker paddings

* Add section headers for color presets

* Automatically apply color on preset click

* Change color picker arguments to Int
This commit is contained in:
Suphon T
2022-10-04 16:05:01 +07:00
committed by GitHub
parent 956e7b49a7
commit b8943840dc
11 changed files with 179 additions and 129 deletions
+11 -10
View File
@@ -46,7 +46,7 @@
<string name="app_info_drop_target_label">App Info</string>
<string name="debug_restart_launcher">Restart Lawnchair</string>
<string name="experimental_features_label">Experimental Features</string>
<!-- ExperimentalFeaturesPreferences -->
<!-- <string name="experimental_features_label" />-->
<string name="font_picker_label">Font Customization</string>
@@ -138,11 +138,12 @@
<string name="font_weight_bold">Bold</string>
<string name="font_weight_extra_bold">Extra Bold</string>
<string name="font_weight_extra_black">Black</string>
<string name="reset_font">Reset</string>
<!-- colorpreference/* -->
<string name="dynamic">Dynamic</string>
<string name="swatches">Swatches</string>
<string name="presets">Presets</string>
<string name="custom">Custom</string>
<string name="rgb">RGB</string>
@@ -193,7 +194,7 @@
<string name="reset_custom_icons">Reset Custom Icons</string>
<string name="reset_custom_icons_confirmation">All custom icons will be reset. Do you want to continue?</string>
<!-- <string name="widget_button_text" /> (unknown source)-->
<string name="force_rounded_widgets">Rounded Corners</string>
<string name="allow_widget_overlap">Allow Overlap</string>
@@ -246,7 +247,7 @@
<string name="smartspace_widget_toggle_label">Show on Home Screen</string>
<string name="smartspace_widget_toggle_description">At a Glance can be manually added by placing the "Lawnchair" widget.</string>
<!-- DockPreferences -->
<!-- <string name="dock_label" /> -->
<string name="search_bar_label">Search Bar</string>
@@ -271,11 +272,11 @@
<string name="app_label">App</string>
<string name="website_label">Website</string>
<string name="qsb_search_provider_app_required">App required.</string>
<!-- AppDrawerPrefences -->
<!-- app_drawer_label -->
<!-- general_label -->
<string name="hidden_apps_label">Hidden Apps</string>
<string name="hidden_apps_label">Hidden Apps</string>
<string name="background_opacity">Background Opacity</string>
<string name="pref_category_search">Search</string>
@@ -287,7 +288,7 @@
<!-- <string name="grid" /> -->
<string name="app_drawer_columns">App Drawer Columns</string>
<string name="row_height_label">Row Height</string>
<!--
<string name="icons" />
<string name="icon_size" />
@@ -311,7 +312,7 @@
<!-- <string name="grid" /> -->
<string name="max_folder_columns">Max. Folder Columns</string>
<string name="max_folder_rows">Max. Folder Rows</string>
<!-- GesturePreference -->
<!-- <string name="gestures_label" /> -->
<string name="gesture_double_tap">Double Tap</string>
@@ -341,7 +342,7 @@
<string name="action_share">Share</string>
<string name="action_lens">Lens</string>
<string name="recents_clear_all">Clear All</string>
<string name="window_corner_radius_label">Screen Corner Radius</string>
<string name="override_window_corner_radius_label">Custom Screen Corner Radius</string>
<string name="window_corner_radius_description">When you swipe up to open Recents, the current app follows your finger, shrinking into a card. Use this slider to adjust the corner radius of the card when its nearly full-screen such that it matches the corners of your screen.</string>
@@ -355,7 +356,7 @@
<string name="twitter" translatable="false">Twitter</string>
<string name="github" translatable="false">GitHub</string>
<string name="discord" translatable="false">Discord</string>
<string name="product">Product</string>
<string name="design_and_development">Design &amp; Development</string>
<string name="development">Development</string>
@@ -38,10 +38,7 @@ fun FolderPreferences() {
val prefs = preferenceManager()
val prefs2 = preferenceManager2()
PreferenceGroup(heading = stringResource(id = R.string.general_label)) {
ColorPreference(
preference = prefs2.folderColor,
label = stringResource(id = R.string.folder_preview_bg_color_label),
)
ColorPreference(preference = prefs2.folderColor)
SliderPreference(
label = stringResource(id = R.string.folder_preview_bg_opacity_label),
adapter = prefs2.folderPreviewBackgroundOpacity.getAdapter(),
@@ -141,10 +141,7 @@ fun GeneralPreferences() {
PreferenceGroup(heading = stringResource(id = R.string.colors)) {
ThemePreference()
ColorPreference(
preference = prefs2.accentColor,
label = stringResource(id = R.string.accent_color),
)
ColorPreference(preference = prefs2.accentColor)
}
PreferenceGroup(heading = stringResource(id = R.string.notification_dots)) {
@@ -153,20 +150,14 @@ fun GeneralPreferences() {
NotificationDotsPreference(enabled = enabled, serviceEnabled = serviceEnabled)
if (enabled && serviceEnabled) {
val showNotificationCountAdapter = prefs2.showNotificationCount.getAdapter()
ColorPreference(
preference = prefs2.notificationDotColor,
label = stringResource(id = R.string.notification_dots_color),
)
ColorPreference(preference = prefs2.notificationDotColor)
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),
)
ColorPreference(preference = prefs2.notificationDotTextColor)
NotificationDotColorContrastWarnings(
dotColor = prefs2.notificationDotColor.asState().value,
dotTextColor = prefs2.notificationDotTextColor.asState().value,
@@ -22,6 +22,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.datastore.preferences.core.Preferences
import app.lawnchair.preferences.PreferenceAdapter
import app.lawnchair.preferences.getAdapter
import app.lawnchair.theme.color.ColorOption
@@ -36,20 +38,19 @@ import com.patrykmichalik.opto.domain.Preference
* @see ColorSelection
*/
@Composable
fun ColorPreference(
preference: Preference<ColorOption, String, *>,
label: String,
) {
val adapter: PreferenceAdapter<ColorOption> = preference.getAdapter()
fun ColorPreference(preference: Preference<ColorOption, String, Preferences.Key<String>>) {
val modelList = ColorPreferenceModelList.INSTANCE.get(LocalContext.current)
val model = modelList[preference.key.name]
val adapter: PreferenceAdapter<ColorOption> = model.prefObject.getAdapter()
val navController = LocalNavController.current
PreferenceTemplate(
title = { Text(text = label) },
title = { Text(text = stringResource(id = model.labelRes)) },
endWidget = {
ColorDot(adapter.state.value.colorPreferenceEntry)
},
description = {
Text(text = adapter.state.value.colorPreferenceEntry.label())
},
modifier = Modifier.clickable { navController.navigate(route = "/colorSelection/${preference.key}/") },
modifier = Modifier.clickable { navController.navigate(route = "/colorSelection/${model.prefObject.key.name}/") },
)
}
@@ -0,0 +1,12 @@
package app.lawnchair.ui.preferences.components.colorpreference
import androidx.annotation.StringRes
import androidx.datastore.preferences.core.Preferences
import app.lawnchair.theme.color.ColorOption
import com.patrykmichalik.opto.domain.Preference
data class ColorPreferenceModel(
val prefObject: Preference<ColorOption, String, Preferences.Key<String>>,
@StringRes val labelRes: Int,
val dynamicEntries: List<ColorPreferenceEntry<ColorOption>>
)
@@ -0,0 +1,54 @@
package app.lawnchair.ui.preferences.components.colorpreference
import android.content.Context
import app.lawnchair.preferences2.PreferenceManager2
import com.android.launcher3.R
import com.android.launcher3.util.MainThreadInitializedObject
class ColorPreferenceModelList(context: Context) {
private val models = mutableMapOf<String, ColorPreferenceModel>()
init {
val prefs = PreferenceManager2.getInstance(context)
registerModel(
ColorPreferenceModel(
prefObject = prefs.accentColor,
labelRes = R.string.accent_color,
dynamicEntries = dynamicColors,
)
)
registerModel(
ColorPreferenceModel(
prefObject = prefs.notificationDotColor,
labelRes = R.string.notification_dots_color,
dynamicEntries = dynamicColorsWithDefault,
)
)
registerModel(
ColorPreferenceModel(
prefObject = prefs.notificationDotTextColor,
labelRes = R.string.notification_dots_text_color,
dynamicEntries = dynamicColorsWithDefault,
)
)
registerModel(
ColorPreferenceModel(
prefObject = prefs.folderColor,
labelRes = R.string.folder_preview_bg_color_label,
dynamicEntries = dynamicColorsWithDefault,
)
)
}
operator fun get(key: String): ColorPreferenceModel {
return models[key] ?: throw IllegalArgumentException("Unknown key: $key")
}
private fun registerModel(model: ColorPreferenceModel) {
models[model.prefObject.key.name] = model
}
companion object {
val INSTANCE = MainThreadInitializedObject(::ColorPreferenceModelList)
}
}
@@ -1,22 +1,26 @@
package app.lawnchair.ui.preferences.components.colorpreference
import android.content.Context
import android.graphics.Color
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.navArgument
import app.lawnchair.preferences.getAdapter
import app.lawnchair.preferences2.preferenceManager2
import app.lawnchair.theme.color.ColorOption
import app.lawnchair.ui.preferences.components.*
import app.lawnchair.ui.preferences.components.BottomSpacer
import app.lawnchair.ui.preferences.components.Chip
import app.lawnchair.ui.preferences.components.PreferenceLayout
import app.lawnchair.ui.preferences.components.colorpreference.pickers.CustomColorPicker
import app.lawnchair.ui.preferences.components.colorpreference.pickers.PresetsList
import app.lawnchair.ui.preferences.components.colorpreference.pickers.SwatchGrid
@@ -41,31 +45,12 @@ fun NavGraphBuilder.colorSelectionGraph(route: String) {
val args = backStackEntry.arguments!!
val prefKey = args.getString("prefKey")!!
val preferenceManager2 = preferenceManager2()
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,
preferenceManager2.notificationDotTextColor.key.name -> dynamicColorsWithDefault
else -> dynamicColors
}
val modelList = ColorPreferenceModelList.INSTANCE.get(LocalContext.current)
val model = modelList[prefKey]
ColorSelection(
label = label,
preference = pref,
dynamicEntries = dynamicEntries,
label = stringResource(id = model.labelRes),
preference = model.prefObject,
dynamicEntries = model.dynamicEntries,
)
}
}
@@ -79,26 +64,39 @@ fun ColorSelection(
dynamicEntries: List<ColorPreferenceEntry<ColorOption>> = dynamicColors,
staticEntries: List<ColorPreferenceEntry<ColorOption>> = staticColors,
) {
val adapter = preference.getAdapter()
val appliedColor by adapter
val selectedColor = remember { mutableStateOf(appliedColor) }
val appliedColor = adapter.state.value
val context = LocalContext.current
val selectedColor = remember { mutableStateOf(appliedColor.forCustomPicker(context)) }
val selectedColorApplied = derivedStateOf {
appliedColor is ColorOption.CustomColor && appliedColor.color == selectedColor.value
}
val defaultTabIndex = when {
dynamicEntries.any { it.value == appliedColor } -> 0
staticEntries.any { it.value == appliedColor } -> 1
else -> 2
}
val onPresetClick = { option: ColorOption ->
selectedColor.value = option.forCustomPicker(context)
adapter.onChange(newValue = option)
}
val pagerState = rememberPagerState(defaultTabIndex)
PreferenceLayout(
label = label,
bottomBar = {
if (pagerState.currentPage == 0) {
BottomSpacer()
return@PreferenceLayout
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Button(
enabled = selectedColor.value != appliedColor,
onClick = { adapter.onChange(newValue = selectedColor.value) },
enabled = !selectedColorApplied.value,
onClick = { adapter.onChange(newValue = ColorOption.CustomColor(selectedColor.value)) },
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
@@ -109,74 +107,72 @@ fun ColorSelection(
}
},
) {
val pagerState = rememberPagerState(defaultTabIndex)
val scope = rememberCoroutineScope()
val scrollToPage =
{ page: Int -> scope.launch { pagerState.animateScrollToPage(page) } }
Column {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
modifier = Modifier.padding(horizontal = 16.dp),
) {
Chip(
label = stringResource(id = R.string.dynamic),
label = stringResource(id = R.string.presets),
onClick = { scrollToPage(0) },
currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
page = 0,
)
Chip(
label = stringResource(id = R.string.presets),
label = stringResource(id = R.string.custom),
onClick = { scrollToPage(1) },
currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
page = 1,
)
Chip(
label = stringResource(id = R.string.custom),
onClick = { scrollToPage(2) },
currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
page = 2,
)
}
HorizontalPager(
count = 3,
count = 2,
state = pagerState,
verticalAlignment = Alignment.Top,
modifier = Modifier.animateContentSize(),
) { page ->
when (page) {
0 -> {
PresetsList(
dynamicEntries = dynamicEntries,
onPresetClick = { selectedColor.value = it },
isPresetSelected = { it == selectedColor.value },
)
Column {
PresetsList(
dynamicEntries = dynamicEntries,
onPresetClick = onPresetClick,
isPresetSelected = { it == appliedColor },
)
SwatchGrid(
modifier = Modifier.padding(top = 12.dp),
contentModifier = Modifier.padding(
start = 16.dp,
top = 20.dp,
end = 16.dp,
bottom = 16.dp,
),
entries = staticEntries,
onSwatchClick = onPresetClick,
isSwatchSelected = { it == appliedColor },
)
}
}
1 -> {
SwatchGrid(
modifier = Modifier.padding(top = 12.dp),
contentModifier = Modifier.padding(
start = 16.dp,
top = 20.dp,
end = 16.dp,
bottom = 16.dp,
),
entries = staticEntries,
onSwatchClick = { selectedColor.value = it },
isSwatchSelected = { it == selectedColor.value },
)
}
2 -> {
CustomColorPicker(
selectedColorOption = selectedColor.value,
selectedColor = selectedColor.value,
onSelect = { selectedColor.value = it },
)
}
}
}
}
}
}
private fun ColorOption.forCustomPicker(context: Context): Int {
val color = colorPreferenceEntry.lightColor(context)
if (color == 0) {
return Color.BLACK
}
return color
}
@@ -117,13 +117,12 @@ fun HsbColorSlider(
}
PreferenceTemplate(
modifier = Modifier.padding(horizontal = 8.dp),
title = {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 6.dp)
.padding(horizontal = 16.dp)
.fillMaxWidth()
.padding(top = 16.dp),
) {
@@ -161,7 +160,7 @@ fun HsbColorSlider(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.padding(horizontal = 6.dp)
.padding(horizontal = 16.dp)
.requiredHeight(24.dp)
.clip(RoundedCornerShape(6.dp))
.background(brush = Brush.horizontalGradient(brushColors)),
@@ -176,6 +175,7 @@ fun HsbColorSlider(
colors = SliderDefaults.colors(),
modifier = Modifier
.height(24.dp)
.padding(horizontal = 8.dp)
.fillMaxWidth(),
)
}
@@ -2,7 +2,7 @@ package app.lawnchair.ui.preferences.components.colorpreference.pickers
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Color as AndroidColor
import android.graphics.Color.argb
import android.widget.Toast
import androidx.compose.animation.Crossfade
@@ -58,13 +58,11 @@ import kotlinx.coroutines.launch
@Composable
fun CustomColorPicker(
modifier: Modifier = Modifier,
selectedColorOption: ColorOption,
onSelect: (ColorOption) -> Unit,
selectedColor: Int,
onSelect: (Int) -> Unit,
) {
val focusManager = LocalFocusManager.current
val selectedColor = selectedColorOption.colorPreferenceEntry.lightColor(LocalContext.current)
val selectedColorCompose = Color(selectedColor)
val textFieldValue = remember {
@@ -76,7 +74,6 @@ fun CustomColorPicker(
}
Column(modifier = modifier) {
PreferenceGroup(
heading = stringResource(id = R.string.hex),
modifier = Modifier.padding(top = 8.dp),
@@ -104,7 +101,7 @@ fun CustomColorPicker(
textFieldValue.value = newValue.copy(text = newText)
val newColor = colorStringToIntColor(colorString = newText)
if (newColor != null) {
onSelect(ColorOption.CustomColor(newColor))
onSelect(newColor)
}
},
)
@@ -128,7 +125,7 @@ fun CustomColorPicker(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 12.dp),
.padding(top = 16.dp),
) {
Chip(
label = stringResource(id = R.string.hsb),
@@ -276,12 +273,12 @@ private fun HexColorPicker(
private fun HsvColorPicker(
selectedColor: Int,
onSelectedColorChange: () -> Unit,
onSliderValuesChange: (ColorOption.CustomColor) -> Unit,
onSliderValuesChange: (Int) -> Unit,
) {
var hue by remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[0]) }
var saturation by remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[1]) }
var brightness by remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[2]) }
val hsv = remember { intColorToHsvColorArray(selectedColor) }
var hue by remember { mutableStateOf(hsv[0]) }
var saturation by remember { mutableStateOf(hsv[1]) }
var brightness by remember { mutableStateOf(hsv[2]) }
val coroutineScope = rememberCoroutineScope()
fun updateColor(
@@ -296,12 +293,10 @@ private fun HsvColorPicker(
if (newBrightness != null) brightness = newBrightness
onSliderValuesChange(
ColorOption.CustomColor(
hsvValuesToIntColor(
hue = newHue ?: hue,
saturation = newSaturation ?: saturation,
brightness = newBrightness ?: brightness,
),
hsvValuesToIntColor(
hue = newHue ?: hue,
saturation = newSaturation ?: saturation,
brightness = newBrightness ?: brightness,
),
)
}
@@ -347,7 +342,7 @@ private fun RgbColorPicker(
selectedColor: Int,
selectedColorCompose: Color = Color(selectedColor),
onSelectedColorChange: () -> Unit,
onSliderValuesChange: (ColorOption.CustomColor) -> Unit,
onSliderValuesChange: (Int) -> Unit,
) {
var red by remember { mutableStateOf(selectedColor.red) }
@@ -367,13 +362,11 @@ private fun RgbColorPicker(
if (newBlue != null) blue = newBlue
onSliderValuesChange(
ColorOption.CustomColor(
argb(
255,
newRed ?: red,
newGreen ?: green,
newBlue ?: blue,
),
argb(
255,
newRed ?: red,
newGreen ?: green,
newBlue ?: blue,
),
)
}
@@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.lawnchair.theme.color.ColorOption
import app.lawnchair.ui.preferences.components.PreferenceDivider
@@ -19,8 +20,8 @@ import app.lawnchair.ui.preferences.components.PreferenceGroup
import app.lawnchair.ui.preferences.components.PreferenceTemplate
import app.lawnchair.ui.preferences.components.colorpreference.ColorDot
import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceEntry
import com.android.launcher3.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PresetsList(
dynamicEntries: List<ColorPreferenceEntry<ColorOption>>,
@@ -29,6 +30,7 @@ fun PresetsList(
) {
PreferenceGroup(
heading = stringResource(id = R.string.dynamic),
modifier = Modifier.padding(top = 12.dp),
showDividers = false,
) {
@@ -15,9 +15,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.lawnchair.ui.preferences.components.PreferenceGroup
import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceEntry
import com.android.launcher3.R
object SwatchGridDefaults {
val GutterSize = 12.dp
@@ -38,6 +40,7 @@ fun <T> SwatchGrid(
val gutter = SwatchGridDefaults.GutterSize
PreferenceGroup(
heading = stringResource(id = R.string.swatches),
modifier = modifier,
showDividers = false,
) {