diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index 241d3db35a..d7fd16709d 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -111,6 +111,7 @@ 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 it’s nearly full-screen such that it matches the corners of your screen. System Icons Accent Color + Invalid Color Done Loading… System @@ -180,6 +181,16 @@ Dynamic Search for More Apps Show Search Bar + RGB + Red + Green + Blue + HSB + Hue + Saturation + Brightness + Hex + Sliders Lawnchair Bug Report %1$s Crashed diff --git a/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt index 67d17fb213..4588055a65 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt @@ -27,7 +27,6 @@ import app.lawnchair.preferences.getAdapter import app.lawnchair.preferences.preferenceManager import app.lawnchair.preferences2.asState import app.lawnchair.preferences2.preferenceManager2 -import app.lawnchair.ui.preferences.components.AccentColorPreference import app.lawnchair.ui.preferences.components.ExpandAndShrink import app.lawnchair.ui.preferences.components.FontPreference import app.lawnchair.ui.preferences.components.IconShapePreference @@ -38,6 +37,7 @@ 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.colorpreference.ColorPreference import app.lawnchair.ui.preferences.components.notificationDotsEnabled import app.lawnchair.ui.preferences.components.notificationServiceEnabled import com.android.launcher3.R @@ -110,7 +110,10 @@ fun GeneralPreferences() { } PreferenceGroup(heading = stringResource(id = R.string.colors)) { ThemePreference() - AccentColorPreference() + ColorPreference( + preference = prefs2.accentColor, + label = stringResource(id = R.string.accent_color), + ) } val wrapAdaptiveIcons = prefs.wrapAdaptiveIcons.getAdapter() PreferenceGroup( diff --git a/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt index a4da801ba6..afcb142b78 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt @@ -30,6 +30,7 @@ import app.lawnchair.backup.ui.createBackupGraph import app.lawnchair.backup.ui.restoreBackupGraph import app.lawnchair.ui.preferences.about.aboutGraph import app.lawnchair.ui.preferences.components.SystemUi +import app.lawnchair.ui.preferences.components.colorpreference.colorSelectionGraph import app.lawnchair.ui.util.ProvideBottomSheetHandler import app.lawnchair.util.ProvideLifecycleState import com.google.accompanist.navigation.animation.AnimatedNavHost @@ -45,6 +46,7 @@ object Routes { const val FOLDERS = "folders" const val QUICKSTEP = "quickstep" const val FONT_SELECTION = "fontSelection" + const val COLOR_SELECTION = "colorSelection" const val DEBUG_MENU = "debugMenu" const val SELECT_ICON = "selectIcon" const val ICON_PICKER = "iconPicker" @@ -97,6 +99,7 @@ fun Preferences(interactor: PreferenceInteractor = viewModel Unit = {}, + bottomBar: @Composable () -> Unit = { BottomSpacer() }, backArrowVisible: Boolean = true, content: @Composable ColumnScope.() -> Unit ) { @@ -47,6 +48,7 @@ fun PreferenceLayout( floating = rememberFloatingState(scrollState), label = label, actions = actions, + bottomBar = bottomBar, ) { PreferenceColumn( verticalArrangement = verticalArrangement, diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceScaffold.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceScaffold.kt index d58d60a5d4..031d9f0997 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceScaffold.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceScaffold.kt @@ -15,6 +15,7 @@ fun PreferenceScaffold( floating: State = remember { mutableStateOf(false) }, label: String, actions: @Composable RowScope.() -> Unit = {}, + bottomBar: @Composable () -> Unit = { BottomSpacer() }, content: @Composable (PaddingValues) -> Unit ) { Scaffold( @@ -26,7 +27,7 @@ fun PreferenceScaffold( actions = actions, ) }, - bottomBar = { BottomSpacer() }, + bottomBar = bottomBar, contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() ) { content(it) diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorConverter.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorConverter.kt new file mode 100644 index 0000000000..c23b49a1d1 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorConverter.kt @@ -0,0 +1,21 @@ +package app.lawnchair.ui.preferences.components.colorpreference + +import android.graphics.Color.colorToHSV +import android.graphics.Color.HSVToColor +import android.graphics.Color.parseColor + +fun intColorToHsvColorArray(color: Int) = + FloatArray(size = 3).apply { colorToHSV(color, this) } + +fun hsvValuesToIntColor(hue: Float, saturation: Float, brightness: Float): Int = + HSVToColor(floatArrayOf(hue, saturation, brightness)) + +fun intColorToColorString(color: Int) = + String.format("#%06X", 0xFFFFFF and color).removePrefix("#") + +fun colorStringToIntColor(colorString: String): Int? = + try { + parseColor("#${colorString.removePrefix("#")}") + } catch (e: IllegalArgumentException) { + null + } diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/AccentColorPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorOptions.kt similarity index 54% rename from lawnchair/src/app/lawnchair/ui/preferences/components/AccentColorPreference.kt rename to lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorOptions.kt index ae2e8d4ac9..affe805bc3 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/AccentColorPreference.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorOptions.kt @@ -1,12 +1,6 @@ -package app.lawnchair.ui.preferences.components +package app.lawnchair.ui.preferences.components.colorpreference -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import app.lawnchair.preferences.getAdapter -import app.lawnchair.preferences2.preferenceManager2 import app.lawnchair.theme.color.ColorOption -import app.lawnchair.ui.preferences.components.colorpreference.ColorPreference -import com.android.launcher3.R val staticColors = listOf( ColorOption.CustomColor(0xFFF32020), @@ -23,17 +17,7 @@ val staticColors = listOf( ColorOption.CustomColor(0xFF67818E) ).map(ColorOption::colorPreferenceEntry) + val dynamicColors = listOf(ColorOption.SystemAccent, ColorOption.WallpaperPrimary) .filter(ColorOption::isSupported) .map(ColorOption::colorPreferenceEntry) - -@Composable -fun AccentColorPreference() { - val adapter = preferenceManager2().accentColor.getAdapter() - ColorPreference( - adapter = adapter, - label = stringResource(id = R.string.accent_color), - dynamicEntries = dynamicColors, - staticEntries = staticColors, - ) -} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt index f9f68113d2..58a08153bc 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt @@ -17,165 +17,36 @@ package app.lawnchair.ui.preferences.components.colorpreference import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.RadioButton -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.graphics.Color import app.lawnchair.preferences.PreferenceAdapter +import app.lawnchair.preferences.getAdapter import app.lawnchair.theme.color.ColorOption -import app.lawnchair.ui.AlertBottomSheetContent -import app.lawnchair.ui.preferences.components.Chip -import app.lawnchair.ui.preferences.components.PreferenceDivider -import app.lawnchair.ui.preferences.components.PreferenceTemplate -import app.lawnchair.ui.theme.lightenColor -import app.lawnchair.ui.util.bottomSheetHandler -import com.android.launcher3.R -import com.google.accompanist.pager.ExperimentalPagerApi -import com.google.accompanist.pager.HorizontalPager -import com.google.accompanist.pager.rememberPagerState -import kotlinx.coroutines.launch +import app.lawnchair.ui.preferences.LocalNavController +import app.lawnchair.ui.preferences.components.* +import com.patrykmichalik.opto.domain.Preference -@OptIn(ExperimentalPagerApi::class) +/** + * A a custom implementation of [PreferenceTemplate] for [ColorOption] preferences. + * + * @see colorSelectionGraph + * @see ColorSelection + */ @Composable fun ColorPreference( - adapter: PreferenceAdapter, + preference: Preference, label: String, - dynamicEntries: List>, - staticEntries: List>, ) { - var selectedColor by adapter - val selectedEntry = dynamicEntries.firstOrNull { it.value == selectedColor } ?: staticEntries.firstOrNull { it.value == selectedColor } - val defaultTabIndex = if (dynamicEntries.any { it.value == selectedColor }) 0 else 1 - val description = selectedEntry?.label?.invoke() - val bottomSheetHandler = bottomSheetHandler - var bottomSheetShown by remember { mutableStateOf(false) } - + val adapter: PreferenceAdapter = preference.getAdapter() + val navController = LocalNavController.current PreferenceTemplate( title = { Text(text = label) }, - endWidget = { ColorDot(color = MaterialTheme.colorScheme.primary) }, - modifier = Modifier.clickable { bottomSheetShown = true }, + endWidget = { ColorDot(Color(adapter.state.value.colorPreferenceEntry.lightColor())) }, description = { - if (description != null) { - Text(text = description) - } + Text(text = adapter.state.value.colorPreferenceEntry.label()) }, + modifier = Modifier.clickable { navController.navigate(route = "/colorSelection/${preference.key}/") }, ) - - if (bottomSheetShown) { - bottomSheetHandler.onDismiss { bottomSheetShown = false } - bottomSheetHandler.show { - val pagerState = rememberPagerState(defaultTabIndex) - val scope = rememberCoroutineScope() - val scrollToPage = { page: Int -> scope.launch { pagerState.animateScrollToPage(page) } } - AlertBottomSheetContent( - title = { Text(text = label) }, - buttons = { - Button(onClick = { bottomSheetHandler.hide() }) { - Text(text = stringResource(id = R.string.done)) - } - } - ) { - Column { - Row( - horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - modifier = Modifier.padding(horizontal = 16.dp), - ) { - Chip( - label = stringResource(id = R.string.dynamic), - onClick = { scrollToPage(0) }, - currentOffset = pagerState.currentPage + pagerState.currentPageOffset, - page = 0, - ) - Chip( - label = stringResource(id = R.string.presets), - onClick = { scrollToPage(1) }, - currentOffset = pagerState.currentPage + pagerState.currentPageOffset, - page = 1, - ) - } - HorizontalPager( - count = 2, - state = pagerState, - verticalAlignment = Alignment.Top, - modifier = Modifier.pagerHeight( - dynamicCount = dynamicEntries.size, - staticCount = staticEntries.size, - ), - ) { page -> - when (page) { - 0 -> { - PresetsList( - dynamicEntries = dynamicEntries, - adapter = adapter, - ) - } - 1 -> { - SwatchGrid( - entries = staticEntries, - modifier = Modifier.padding( - start = 16.dp, - top = 20.dp, - end = 16.dp, - bottom = 16.dp, - ), - onSwatchClick = { selectedColor = it }, - isSwatchSelected = { it == selectedColor }, - ) - } - } - } - } - } - } - } } - -@Composable -private fun PresetsList( - dynamicEntries: List>, - adapter: PreferenceAdapter, -) { - Box( - modifier = Modifier - .fillMaxHeight(), - contentAlignment = Alignment.TopStart - ) { - Column(modifier = Modifier.padding(top = 16.dp)) { - dynamicEntries.mapIndexed { index, entry -> - key(entry) { - if (index > 0) { - PreferenceDivider(startIndent = 40.dp) - } - PreferenceTemplate( - title = { Text(text = entry.label()) }, - verticalPadding = 12.dp, - modifier = Modifier.clickable { adapter.onChange(entry.value) }, - startWidget = { - RadioButton( - selected = entry.value == adapter.state.value, - onClick = null - ) - ColorDot( - entry = entry, - modifier = Modifier.padding(start = 16.dp) - ) - } - ) - } - } - } - } -} - -open class ColorPreferenceEntry( - val value: T, - val label: @Composable () -> String, - val lightColor: @Composable () -> Int, - val darkColor: @Composable () -> Int = { lightenColor(lightColor()) }, -) \ No newline at end of file diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreferenceEntry.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreferenceEntry.kt new file mode 100644 index 0000000000..22b8fe44d3 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreferenceEntry.kt @@ -0,0 +1,11 @@ +package app.lawnchair.ui.preferences.components.colorpreference + +import androidx.compose.runtime.Composable +import app.lawnchair.ui.theme.lightenColor + +open class ColorPreferenceEntry( + val value: T, + val label: @Composable () -> String, + val lightColor: @Composable () -> Int, + val darkColor: @Composable () -> Int = { lightenColor(lightColor()) }, +) diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSelectionPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSelectionPreference.kt new file mode 100644 index 0000000000..e10907f6d5 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSelectionPreference.kt @@ -0,0 +1,168 @@ +package app.lawnchair.ui.preferences.components.colorpreference + +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.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.colorpreference.pickers.CustomColorPicker +import app.lawnchair.ui.preferences.components.colorpreference.pickers.PresetsList +import app.lawnchair.ui.preferences.components.colorpreference.pickers.SwatchGrid +import app.lawnchair.ui.preferences.preferenceGraph +import com.android.launcher3.R +import com.google.accompanist.navigation.animation.composable +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState +import com.patrykmichalik.opto.domain.Preference +import kotlinx.coroutines.launch + +@OptIn(ExperimentalAnimationApi::class) +fun NavGraphBuilder.colorSelectionGraph(route: String) { + preferenceGraph(route, {}) { subRoute -> + composable( + route = subRoute("{prefKey}"), + arguments = listOf( + navArgument("prefKey") { type = NavType.StringType }, + ), + ) { backStackEntry -> + + val args = backStackEntry.arguments!! + val prefKey = args.getString("prefKey")!! + val preferenceManager2 = preferenceManager2() + val pref = when (prefKey) { + preferenceManager2.accentColor.key.name -> preferenceManager2.accentColor + else -> return@composable + } + val label = when (prefKey) { + preferenceManager2.accentColor.key.name -> stringResource(id = R.string.accent_color) + else -> return@composable + } + ColorSelection(label = label, preference = pref) + + } + } +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun ColorSelection( + label: String, + preference: Preference, + dynamicEntries: List> = dynamicColors, + staticEntries: List> = staticColors, +) { + + val adapter = preference.getAdapter() + val appliedColor by adapter + val appliedEntry = dynamicEntries.firstOrNull { it.value == appliedColor } + ?: staticEntries.firstOrNull { it.value == appliedColor } + val selectedColor = remember { mutableStateOf(appliedColor) } + val defaultTabIndex = when { + dynamicEntries.any { it.value == appliedColor } -> 0 + appliedEntry?.value is ColorOption.CustomColor -> 2 + else -> 1 + } + + PreferenceLayout( + label = label, + bottomBar = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.End + ) { + Button( + modifier = Modifier.padding(horizontal = 16.dp), + onClick = { + adapter.onChange(newValue = selectedColor.value) + }, + ) { + Text(text = stringResource(id = R.string.apply_grid)) + } + BottomSpacer() + } + }, + ) { + + 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), + onClick = { scrollToPage(0) }, + currentOffset = pagerState.currentPage + pagerState.currentPageOffset, + page = 0, + ) + Chip( + label = stringResource(id = R.string.presets), + 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, + state = pagerState, + verticalAlignment = Alignment.Top, + modifier = Modifier.animateContentSize(), + ) { page -> + when (page) { + 0 -> { + PresetsList( + dynamicEntries = dynamicEntries, + onPresetClick = { selectedColor.value = it }, + isPresetSelected = { it == selectedColor.value }, + ) + } + 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, + onSelect = { selectedColor.value = it }, + ) + } + } + } + + } + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSlider.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSlider.kt new file mode 100644 index 0000000000..a61011d989 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSlider.kt @@ -0,0 +1,189 @@ +package app.lawnchair.ui.preferences.components.colorpreference + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.util.toRange +import app.lawnchair.ui.preferences.components.PreferenceTemplate +import app.lawnchair.ui.preferences.components.getSteps +import app.lawnchair.ui.preferences.components.snapSliderValue +import com.android.launcher3.R +import kotlin.math.roundToInt + +@Composable +fun RgbColorSlider( + label: String, + colorStart: Color, + colorEnd: Color, + value: Int, + onValueChange: (Float) -> Unit, +) { + + val step = 0f + val rgbRange = 0f..255f + + PreferenceTemplate( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp), + title = { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + ) { + Text(text = label) + CompositionLocalProvider( + LocalContentAlpha provides ContentAlpha.medium, + LocalContentColor provides androidx.compose.material.MaterialTheme.colors.onBackground, + ) { + val valueText = snapSliderValue(rgbRange.start, value.toFloat(), step) + .roundToInt().toString() + Text(text = valueText) + } + } + }, + description = { + Row( + modifier = Modifier.padding(top = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .requiredSize(6.dp) + .clip(CircleShape) + .background(colorStart), + ) + Slider( + value = value.toFloat(), + onValueChange = onValueChange, + valueRange = rgbRange, + steps = getSteps(rgbRange, step), + modifier = Modifier + .height(24.dp) + .weight(1f) + .padding(horizontal = 3.dp), + ) + Box( + modifier = Modifier + .requiredSize(6.dp) + .clip(CircleShape) + .background(colorEnd), + ) + } + }, + applyPaddings = false, + ) + +} + +@Composable +fun HsbColorSlider( + type: HsbSliderType, + value: Float, + onValueChange: (Float) -> Unit, +) { + + val step = 0f + + val range = when (type) { + HsbSliderType.HUE -> 0f..359f + else -> 0f..1f + } + + val showAsPercentage = when (type) { + HsbSliderType.HUE -> false + else -> true + } + + val label = when (type) { + HsbSliderType.HUE -> stringResource(id = R.string.hsb_hue) + HsbSliderType.SATURATION -> stringResource(id = R.string.hsb_saturation) + HsbSliderType.BRIGHTNESS -> stringResource(id = R.string.hsb_brightness) + } + + PreferenceTemplate( + modifier = Modifier.padding(horizontal = 8.dp), + title = { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 6.dp) + .fillMaxWidth() + .padding(top = 16.dp), + ) { + Text(text = label) + CompositionLocalProvider( + LocalContentAlpha provides ContentAlpha.medium, + LocalContentColor provides androidx.compose.material.MaterialTheme.colors.onBackground, + ) { + val valueText = snapSliderValue(range.start, value, step) + Text( + text = if (showAsPercentage) stringResource( + id = R.string.n_percent, + (valueText * 100).roundToInt(), + ) else "${valueText.roundToInt()}°", + ) + } + } + }, + description = { + Column( + modifier = Modifier.padding(top = 4.dp, bottom = 12.dp), + ) { + if (type == HsbSliderType.HUE) { + val brushColors = arrayListOf() + val stepSize = 6 + repeat((range.endInclusive - range.start).toInt() / stepSize) { + val newColor = Color.hsv( + hue = it * stepSize.toFloat(), + saturation = 1f, + value = 1f, + ) + brushColors.add(newColor) + } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + .padding(horizontal = 6.dp) + .requiredHeight(24.dp) + .clip(RoundedCornerShape(6.dp)) + .background(brush = Brush.horizontalGradient(brushColors)), + ) + } + Slider( + value = value, + onValueChange = onValueChange, + onValueChangeFinished = { }, + valueRange = range, + steps = getSteps(range, step), + colors = SliderDefaults.colors(), + modifier = Modifier + .height(24.dp) + .fillMaxWidth(), + ) + } + }, + applyPaddings = false, + ) +} + +enum class HsbSliderType { + HUE, SATURATION, BRIGHTNESS +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/PagerHeightModifier.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/PagerHeightModifier.kt deleted file mode 100644 index 0a7ef7b11f..0000000000 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/PagerHeightModifier.kt +++ /dev/null @@ -1,71 +0,0 @@ -package app.lawnchair.ui.preferences.components.colorpreference - -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.LayoutModifier -import androidx.compose.ui.layout.Measurable -import androidx.compose.ui.layout.MeasureResult -import androidx.compose.ui.layout.MeasureScope -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.constrainHeight -import androidx.compose.ui.unit.dp -import java.util.* -import kotlin.math.max -import kotlin.math.min - -fun Modifier.pagerHeight( - dynamicCount: Int, - staticCount: Int -) = this.then( - PagerHeightModifier( - dynamicCount = dynamicCount, - staticCount = staticCount - ) -) - -class PagerHeightModifier( - private val dynamicCount: Int, - private val staticCount: Int -) : LayoutModifier { - - override fun MeasureScope.measure( - measurable: Measurable, - constraints: Constraints - ): MeasureResult { - val dynamicItemHeight = 54.dp.roundToPx() - val dynamicListHeight = 16.dp.roundToPx() + - dynamicItemHeight * dynamicCount + - 1.dp.roundToPx() * (dynamicCount - 1) - - val columnCount = SwatchGridDefaults.ColumnCount - val rowCount = (staticCount - 1) / columnCount + 1 - - val width = constraints.maxWidth - val horizontalPadding = 16.dp.roundToPx() * 2 - val gutterSizePx = SwatchGridDefaults.GutterSize.roundToPx() - val totalGutterWidth = gutterSizePx * (columnCount - 1) - val availableWidth = width - horizontalPadding - totalGutterWidth - val swatchMaxWidth = SwatchGridDefaults.SwatchMaxWidth.roundToPx() - val swatchWidth = min(availableWidth / columnCount, swatchMaxWidth) - - val swatchGridVerticalPadding = 20.dp.roundToPx() + 16.dp.roundToPx() - val swatchGridInnerHeight = swatchWidth * rowCount + gutterSizePx * (rowCount - 1) - val swatchGridHeight = swatchGridInnerHeight + swatchGridVerticalPadding - - val height = constraints.constrainHeight(max(dynamicListHeight, swatchGridHeight)) - - val placeable = measurable.measure(constraints.copy(maxHeight = height)) - return layout(width, constraints.constrainHeight(swatchGridHeight)) { - placeable.place(0, 0) - } - } - - override fun hashCode(): Int { - return Objects.hash(dynamicCount, staticCount) - } - - override fun equals(other: Any?): Boolean { - return other is PagerHeightModifier - && dynamicCount == other.dynamicCount - && staticCount == other.staticCount - } -} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/CustomColorPicker.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/CustomColorPicker.kt new file mode 100644 index 0000000000..9d27abf8b5 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/CustomColorPicker.kt @@ -0,0 +1,350 @@ +package app.lawnchair.ui.preferences.components.colorpreference.pickers + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red +import app.lawnchair.theme.color.ColorOption +import app.lawnchair.ui.preferences.components.Chip +import app.lawnchair.ui.preferences.components.DividerColumn +import app.lawnchair.ui.preferences.components.PreferenceGroup +import app.lawnchair.ui.preferences.components.colorpreference.* +import com.android.launcher3.R +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState +import kotlinx.coroutines.launch + +/** + * Unlike [PresetsList] & [SwatchGrid], This Composable allows the user to select a fully custom [ColorOption] using HEX, HSB & RGB values. + * + * @see HexColorPicker + * @see HsvColorPicker + * @see RgbColorPicker + */ +@OptIn(ExperimentalPagerApi::class) +@Composable +fun CustomColorPicker( + modifier: Modifier = Modifier, + selectedColorOption: ColorOption, + onSelect: (ColorOption) -> Unit, +) { + + val focusManager = LocalFocusManager.current + + val selectedColor = selectedColorOption.colorPreferenceEntry.lightColor() + val selectedColorCompose = Color(selectedColor) + + val textFieldValue = remember { + mutableStateOf( + TextFieldValue( + text = intColorToColorString(color = selectedColor), + ) + ) + } + + Column(modifier = modifier) { + + PreferenceGroup(heading = stringResource(id = R.string.hex)) { + + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + + Box( + modifier = Modifier + .requiredSize(48.dp) + .clip(CircleShape) + .background(selectedColorCompose), + ) + + Spacer(modifier = Modifier.requiredWidth(16.dp)) + + HexColorPicker( + textFieldValue = textFieldValue.value, + onTextFieldValueChange = { newValue -> + val newText = newValue.text.removePrefix("#").take(6).uppercase() + textFieldValue.value = newValue.copy(text = newText) + val newColor = colorStringToIntColor(colorString = newText) + if (newColor != null) { + onSelect(ColorOption.CustomColor(newColor)) + } + }, + ) + + } + } + + val pagerState = rememberPagerState(0) + val scope = rememberCoroutineScope() + val scrollToPage = + { page: Int -> scope.launch { pagerState.animateScrollToPage(page) } } + + PreferenceGroup( + heading = stringResource(id = R.string.color_sliders), + ) { + + Column { + + Row( + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 12.dp), + ) { + Chip( + label = stringResource(id = R.string.hsb), + onClick = { scrollToPage(0) }, + currentOffset = pagerState.currentPage + pagerState.currentPageOffset, + page = 0, + ) + Chip( + label = stringResource(id = R.string.rgb), + onClick = { scrollToPage(1) }, + currentOffset = pagerState.currentPage + pagerState.currentPageOffset, + page = 1, + ) + } + + HorizontalPager( + modifier = Modifier.animateContentSize(), + count = 2, + state = pagerState, + verticalAlignment = Alignment.Top, + ) { page -> + when (page) { + 0 -> { + HsvColorPicker( + selectedColor = selectedColor, + onSelectedColorChange = { + textFieldValue.value = + textFieldValue.value.copy( + text = intColorToColorString( + selectedColor, + ), + ) + }, + onSliderValuesChange = { newColor -> + focusManager.clearFocus() + onSelect(newColor) + } + ) + } + 1 -> { + RgbColorPicker( + selectedColor = selectedColor, + onSelectedColorChange = { + textFieldValue.value = + textFieldValue.value.copy( + text = intColorToColorString(selectedColor), + ) + }, + onSliderValuesChange = { newColor -> + focusManager.clearFocus() + onSelect(newColor) + }, + ) + } + } + } + } + } + } +} + +@Composable +private fun HexColorPicker( + textFieldValue: TextFieldValue, + onTextFieldValueChange: (TextFieldValue) -> Unit, +) { + + val focusManager = LocalFocusManager.current + + val invalidString = colorStringToIntColor(textFieldValue.text) == null + + OutlinedTextField( + textStyle = LocalTextStyle.current.copy( + fontSize = 18.sp, + textAlign = TextAlign.Start, + ), + isError = invalidString, + value = textFieldValue, + onValueChange = onTextFieldValueChange, + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Characters, + autoCorrect = false, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + }, + ), + trailingIcon = { + Crossfade(targetState = invalidString) { + if (it) { + Icon( + painter = painterResource(id = R.drawable.ic_warning), + contentDescription = stringResource(id = R.string.invalid_color), + ) + } + } + }, + ) +} + +@Composable +private fun HsvColorPicker( + selectedColor: Int, + onSelectedColorChange: () -> Unit, + onSliderValuesChange: (ColorOption.CustomColor) -> Unit, +) { + + val hue = remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[0]) } + val saturation = remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[1]) } + val brightness = remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[2]) } + + DividerColumn { + + HsbColorSlider( + type = HsbSliderType.HUE, + value = hue.value, + onValueChange = { newValue -> + hue.value = newValue + }, + ) + HsbColorSlider( + type = HsbSliderType.SATURATION, + value = saturation.value, + onValueChange = { newValue -> + saturation.value = newValue + }, + ) + HsbColorSlider( + type = HsbSliderType.BRIGHTNESS, + value = brightness.value, + onValueChange = { newValue -> + brightness.value = newValue + }, + ) + + LaunchedEffect(key1 = selectedColor) { + val hsv = intColorToHsvColorArray(selectedColor) + hue.value = hsv[0] + saturation.value = hsv[1] + brightness.value = hsv[2] + onSelectedColorChange() + } + + LaunchedEffect( + key1 = hue.value, + key2 = saturation.value, + key3 = brightness.value, + ) { + onSliderValuesChange( + ColorOption.CustomColor( + hsvValuesToIntColor( + hue = hue.value, + saturation = saturation.value, + brightness = brightness.value, + ), + ), + ) + } + + } + +} + +@Composable +private fun RgbColorPicker( + selectedColor: Int, + selectedColorCompose: Color = Color(selectedColor), + onSelectedColorChange: () -> Unit, + onSliderValuesChange: (ColorOption.CustomColor) -> Unit, +) { + + val red = remember { mutableStateOf(selectedColor.red) } + val green = remember { mutableStateOf(selectedColor.green) } + val blue = remember { mutableStateOf(selectedColor.blue) } + + DividerColumn { + + RgbColorSlider( + label = stringResource(id = R.string.rgb_red), + value = red.value, + colorStart = selectedColorCompose.copy(red = 0f), + colorEnd = selectedColorCompose.copy(red = 1f), + onValueChange = { newValue -> + red.value = newValue.toInt() + }, + ) + RgbColorSlider( + label = stringResource(id = R.string.rgb_green), + value = green.value, + colorStart = selectedColorCompose.copy(green = 0f), + colorEnd = selectedColorCompose.copy(green = 1f), + onValueChange = { newValue -> + green.value = newValue.toInt() + }, + ) + RgbColorSlider( + label = stringResource(id = R.string.rgb_blue), + value = blue.value, + colorStart = selectedColorCompose.copy(blue = 0f), + colorEnd = selectedColorCompose.copy(blue = 1f), + onValueChange = { newValue -> + blue.value = newValue.toInt() + }, + ) + + LaunchedEffect(key1 = selectedColor) { + red.value = selectedColor.red + green.value = selectedColor.green + blue.value = selectedColor.blue + onSelectedColorChange() + } + + LaunchedEffect( + key1 = red.value, + key2 = green.value, + key3 = blue.value, + ) { + onSliderValuesChange( + ColorOption.CustomColor( + android.graphics.Color.argb( + 255, + red.value, + green.value, + blue.value, + ) + ) + ) + } + } + +} \ No newline at end of file diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/PresetsList.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/PresetsList.kt new file mode 100644 index 0000000000..847505c270 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/PresetsList.kt @@ -0,0 +1,59 @@ +package app.lawnchair.ui.preferences.components.colorpreference.pickers + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.lawnchair.theme.color.ColorOption +import app.lawnchair.ui.preferences.components.PreferenceDivider +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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PresetsList( + dynamicEntries: List>, + onPresetClick: (ColorOption) -> Unit, + isPresetSelected: (ColorOption) -> Boolean, +) { + + PreferenceGroup( + modifier = Modifier.padding(top = 12.dp), + showDividers = false, + ) { + + dynamicEntries.mapIndexed { index, entry -> + key(entry) { + if (index > 0) { + PreferenceDivider(startIndent = 40.dp) + } + PreferenceTemplate( + title = { Text(text = entry.label()) }, + verticalPadding = 12.dp, + modifier = Modifier.clickable { onPresetClick(entry.value) }, + startWidget = { + RadioButton( + selected = isPresetSelected(entry.value), + onClick = null, + ) + ColorDot( + entry = entry, + modifier = Modifier.padding(start = 16.dp), + ) + }, + ) + } + } + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/SwatchGrid.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/SwatchGrid.kt similarity index 56% rename from lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/SwatchGrid.kt rename to lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/SwatchGrid.kt index b33a511439..7b8e6a2646 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/SwatchGrid.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/SwatchGrid.kt @@ -1,4 +1,4 @@ -package app.lawnchair.ui.preferences.components.colorpreference +package app.lawnchair.ui.preferences.components.colorpreference.pickers import androidx.compose.animation.Crossfade import androidx.compose.foundation.background @@ -15,6 +15,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import app.lawnchair.ui.preferences.components.PreferenceGroup +import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceEntry object SwatchGridDefaults { val GutterSize = 12.dp @@ -24,41 +26,47 @@ object SwatchGridDefaults { @Composable fun SwatchGrid( + modifier: Modifier = Modifier, + contentModifier: Modifier = Modifier, entries: List>, onSwatchClick: (T) -> Unit, - modifier: Modifier = Modifier, isSwatchSelected: (T) -> Boolean ) { val columnCount = SwatchGridDefaults.ColumnCount val rowCount = (entries.size - 1) / columnCount + 1 val gutter = SwatchGridDefaults.GutterSize - Column(modifier = modifier) { - for (rowNo in 1..rowCount) { - val firstIndex = (rowNo - 1) * columnCount - val lastIndex = firstIndex + columnCount - 1 - val indices = firstIndex..lastIndex + PreferenceGroup( + modifier = modifier, + showDividers = false, + ) { + Column(modifier = contentModifier) { + for (rowNo in 1..rowCount) { + val firstIndex = (rowNo - 1) * columnCount + val lastIndex = firstIndex + columnCount - 1 + val indices = firstIndex..lastIndex - Row { - entries.slice(indices).forEachIndexed { index, colorOption -> - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.Center - ) { - ColorSwatch( - entry = colorOption, - onClick = { onSwatchClick(colorOption.value) }, - modifier = Modifier.widthIn(0.dp, SwatchGridDefaults.SwatchMaxWidth), - selected = isSwatchSelected(colorOption.value) - ) - } - if (index != columnCount - 1) { - Spacer(modifier = Modifier.width(gutter)) + Row { + entries.slice(indices).forEachIndexed { index, colorOption -> + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + ColorSwatch( + entry = colorOption, + onClick = { onSwatchClick(colorOption.value) }, + modifier = Modifier.widthIn(0.dp, SwatchGridDefaults.SwatchMaxWidth), + selected = isSwatchSelected(colorOption.value), + ) + } + if (index != columnCount - 1) { + Spacer(modifier = Modifier.width(gutter)) + } } } - } - if (rowNo != rowCount) { - Spacer(modifier = Modifier.height(gutter)) + if (rowNo != rowCount) { + Spacer(modifier = Modifier.height(gutter)) + } } } }