Merge pull request #2702

This commit is contained in:
Patryk Michalik
2022-07-10 18:59:46 +02:00
committed by GitHub
15 changed files with 874 additions and 264 deletions

View File

@@ -111,6 +111,7 @@
<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>
<string name="system_icons">System Icons</string>
<string name="accent_color">Accent Color</string>
<string name="invalid_color">Invalid Color</string>
<string name="done">Done</string>
<string name="loading">Loading…</string>
<string name="system">System</string>
@@ -180,6 +181,16 @@
<string name="dynamic">Dynamic</string>
<string name="all_apps_search_market_message">Search for More Apps</string>
<string name="show_app_search_bar">Show Search Bar</string>
<string name="rgb">RGB</string>
<string name="rgb_red">Red</string>
<string name="rgb_green">Green</string>
<string name="rgb_blue">Blue</string>
<string name="hsb">HSB</string>
<string name="hsb_hue">Hue</string>
<string name="hsb_saturation">Saturation</string>
<string name="hsb_brightness">Brightness</string>
<string name="hex">Hex</string>
<string name="color_sliders">Sliders</string>
<string name="lawnchair_bug_report">Lawnchair Bug Report</string>
<string name="crash_report_notif_title">%1$s Crashed</string>

View File

@@ -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(

View File

@@ -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<PreferenceViewModel
quickstepGraph(route = subRoute(Routes.QUICKSTEP))
aboutGraph(route = subRoute(Routes.ABOUT))
fontSelectionGraph(route = subRoute(Routes.FONT_SELECTION))
colorSelectionGraph(route = subRoute(Routes.COLOR_SELECTION))
debugMenuGraph(route = subRoute(Routes.DEBUG_MENU))
selectIconGraph(route = subRoute(Routes.SELECT_ICON))
iconPickerGraph(route = subRoute(Routes.ICON_PICKER))

View File

@@ -39,6 +39,7 @@ fun PreferenceLayout(
scrollState: ScrollState? = rememberScrollState(),
label: String,
actions: @Composable RowScope.() -> 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,

View File

@@ -15,6 +15,7 @@ fun PreferenceScaffold(
floating: State<Boolean> = 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)

View File

@@ -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
}

View File

@@ -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,
)
}

View File

@@ -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<ColorOption>,
preference: Preference<ColorOption, String, *>,
label: String,
dynamicEntries: List<ColorPreferenceEntry<ColorOption>>,
staticEntries: List<ColorPreferenceEntry<ColorOption>>,
) {
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<ColorOption> = 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<ColorPreferenceEntry<ColorOption>>,
adapter: PreferenceAdapter<ColorOption>,
) {
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<T>(
val value: T,
val label: @Composable () -> String,
val lightColor: @Composable () -> Int,
val darkColor: @Composable () -> Int = { lightenColor(lightColor()) },
)

View File

@@ -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<T>(
val value: T,
val label: @Composable () -> String,
val lightColor: @Composable () -> Int,
val darkColor: @Composable () -> Int = { lightenColor(lightColor()) },
)

View File

@@ -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<ColorOption, String, *>,
dynamicEntries: List<ColorPreferenceEntry<ColorOption>> = dynamicColors,
staticEntries: List<ColorPreferenceEntry<ColorOption>> = 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 },
)
}
}
}
}
}
}

View File

@@ -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<Color>()
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
}

View File

@@ -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
}
}

View File

@@ -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,
)
)
)
}
}
}

View File

@@ -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<ColorPreferenceEntry<ColorOption>>,
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),
)
},
)
}
}
}
}

View File

@@ -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 <T> SwatchGrid(
modifier: Modifier = Modifier,
contentModifier: Modifier = Modifier,
entries: List<ColorPreferenceEntry<T>>,
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))
}
}
}
}