New Compose color picker

pull/3645/head
Alex Baker 5 months ago
parent 36b20f47fd
commit a299363fe8

@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.content.pm.ShortcutInfoCompat
@ -29,21 +30,20 @@ import com.todoroo.andlib.utility.AndroidUtilities.atLeastS
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity
import org.tasks.compose.DeleteButton
import org.tasks.compose.IconPickerActivity.Companion.launchIconPicker
import org.tasks.compose.IconPickerActivity.Companion.registerForIconPickerResult
import org.tasks.compose.settings.ListSettingsContent
import org.tasks.compose.settings.ListSettingsScaffold
import org.tasks.data.UUIDHelper
import org.tasks.dialogs.ColorPalettePicker
import org.tasks.dialogs.ColorPalettePicker.Companion.newColorPalette
import org.tasks.dialogs.ColorPickerAdapter.Palette
import org.tasks.dialogs.ColorWheelPicker
import org.tasks.extensions.addBackPressedCallback
import org.tasks.filters.Filter
import org.tasks.icons.OutlinedGoogleMaterial
import org.tasks.intents.TaskIntents
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.themes.ColorProvider
import org.tasks.themes.Theme
import org.tasks.themes.contentColorFor
import org.tasks.widget.RequestPinWidgetReceiver
@ -53,10 +53,12 @@ import org.tasks.widget.TasksWidget
import javax.inject.Inject
abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicker.ColorPickedCallback, ColorWheelPicker.ColorPickedCallback {
abstract class BaseListSettingsActivity : AppCompatActivity() {
@Inject lateinit var tasksTheme: Theme
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var firebase: Firebase
@Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider
protected val baseViewModel: BaseListSettingsViewModel by viewModels()
@ -88,11 +90,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
}
}
private fun showThemePicker() {
newColorPalette(null, 0, baseViewModel.color, Palette.COLORS)
.show(supportFragmentManager, FRAG_TAG_COLOR_PICKER)
}
private val launcher = registerForIconPickerResult { selected ->
baseViewModel.setIcon(selected)
}
@ -101,10 +98,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
launcher.launchIconPicker(this, baseViewModel.icon)
}
override fun onColorPicked(color: Int) {
baseViewModel.setColor(color)
}
protected open fun promptDelete() { baseViewModel.promptDelete(true) }
/** Standard @Compose view content for descendants. Caller must wrap it to TasksTheme{} */
@ -132,7 +125,9 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
fab = fab,
) {
ListSettingsContent(
hasPro = remember { inventory.purchasedThemes() },
color = viewState.color,
colors = remember { colorProvider.getThemeColors() },
icon = viewState.icon ?: defaultIcon,
text = viewState.title,
error = viewState.error,
@ -142,12 +137,16 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
baseViewModel.setTitle(it)
baseViewModel.setError("")
},
pickColor = { showThemePicker() },
clearColor = { onColorPicked(0) },
setColor = { baseViewModel.setColor(it) },
pickIcon = { showIconPicker() },
addShortcutToHome = { createShortcut(color) },
addWidgetToHome = { createWidget() },
extensionContent = extensionContent,
purchase = {
startActivity(
Intent(this@BaseListSettingsActivity, PurchaseActivity::class.java)
)
},
)
}
}
@ -219,8 +218,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
}
companion object {
private const val FRAG_TAG_COLOR_PICKER = "frag_tag_color_picker"
fun createShortcutIcon(context: Context, backgroundColor: Color): IconCompat {
val size = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)

@ -0,0 +1,130 @@
package org.tasks.compose.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import org.tasks.themes.ThemeColor
@Composable
fun ColorPicker(
hasPro: Boolean,
colors: List<ThemeColor>,
onSelected: (ThemeColor) -> Unit,
onColorWheelSelected: () -> Unit = {},
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 48.dp),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth()
) {
item {
ColorWheelCircle(
onClick = onColorWheelSelected,
hasPro = hasPro,
)
}
items(colors) { color ->
ColorCircle(
color = color,
locked = !(hasPro || color.isFree),
onClick = { onSelected(color) }
)
}
}
}
@Composable
private fun ColorCircle(
color: ThemeColor,
locked: Boolean,
onClick: () -> Unit
) {
Box(
modifier = Modifier
.aspectRatio(1f)
.clickable(onClick = onClick)
.size(48.dp)
.clip(CircleShape)
.background(Color(color.primaryColor))
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline,
shape = CircleShape
),
contentAlignment = Alignment.Center,
) {
if (locked) {
LockIcon(tint = Color(color.colorOnPrimary))
}
}
}
@Composable
private fun ColorWheelCircle(
onClick: () -> Unit,
hasPro: Boolean,
) {
Box(
modifier = Modifier
.aspectRatio(1f)
.clickable(onClick = onClick)
.size(48.dp)
.clip(CircleShape)
.background(
brush = Brush.sweepGradient(
colors = listOf(
Color.Red,
Color.Magenta,
Color.Blue,
Color.Cyan,
Color.Green,
Color.Yellow,
Color.Red
)
)
)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
if (!hasPro) {
LockIcon(tint = Color.Black)
}
}
}
@Composable
private fun LockIcon(tint: Color) {
Icon(
imageVector = Icons.Outlined.Lock,
contentDescription = null,
tint = tint,
modifier = Modifier.size(24.dp)
)
}

@ -0,0 +1,52 @@
package org.tasks.compose.settings
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import org.tasks.themes.ThemeColor
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColorPickerDialog(
hasPro: Boolean,
colors: List<ThemeColor>,
onDismiss: () -> Unit,
onColorSelected: (ThemeColor) -> Unit,
onColorWheelSelected: () -> Unit = {},
) {
BasicAlertDialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surface,
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(16.dp)
) {
Box(modifier = Modifier.padding(16.dp)) {
ColorPicker(
colors = colors,
onSelected = { color ->
onColorSelected(color)
onDismiss()
},
onColorWheelSelected = {
onColorWheelSelected()
onDismiss()
},
hasPro = hasPro,
)
}
}
}
}

@ -5,22 +5,25 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.tasks.compose.Constants
import org.tasks.themes.ThemeColor
@Composable
fun ColumnScope.ListSettingsContent(
hasPro: Boolean,
color: Int,
colors: List<ThemeColor>,
icon: String,
text: String,
error: String,
requestKeyboard: Boolean,
isNew: Boolean,
setText: (String) -> Unit,
pickColor: () -> Unit,
clearColor: () -> Unit,
setColor: (Int) -> Unit,
pickIcon: () -> Unit,
addShortcutToHome: () -> Unit,
addWidgetToHome: () -> Unit,
extensionContent: @Composable ColumnScope.() -> Unit,
purchase: () -> Unit,
) {
TitleInput(
text = text,
@ -30,9 +33,11 @@ fun ColumnScope.ListSettingsContent(
setText = { setText(it) },
)
SelectColorRow(
hasPro = hasPro,
color = color,
selectColor = { pickColor() },
clearColor = { clearColor() },
colors = colors,
selectColor = { setColor(it) },
purchase = { purchase() },
)
SelectIconRow(
icon = icon,

@ -1,5 +1,6 @@
package org.tasks.compose.settings
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -12,7 +13,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -22,26 +29,101 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.flask.colorpicker.ColorPickerView
import com.flask.colorpicker.builder.ColorPickerDialogBuilder
import org.tasks.R
import org.tasks.compose.Constants
import org.tasks.kmp.org.tasks.compose.settings.SettingRow
import org.tasks.themes.ColorProvider
import org.tasks.themes.TasksTheme
import org.tasks.themes.ThemeColor
@Composable
fun SelectColorRow(
hasPro: Boolean,
color: Int,
selectColor: () -> Unit,
clearColor: () -> Unit
) =
colors: List<ThemeColor>,
purchase: () -> Unit,
selectColor: (Int) -> Unit,
) {
var showColorPicker by rememberSaveable { mutableStateOf(false) }
var showColorWheel by rememberSaveable { mutableStateOf(false) }
if (showColorPicker) {
BackHandler {
showColorPicker = false
}
ColorPickerDialog(
hasPro = hasPro,
colors = colors,
onDismiss = { showColorPicker = false },
onColorSelected = {
if (hasPro || it.isFree) {
selectColor(it.primaryColor)
} else {
purchase()
}
},
onColorWheelSelected = {
if (hasPro) {
showColorWheel = true
} else {
purchase()
}
showColorPicker = false
},
)
}
if (showColorWheel) {
BackHandler {
showColorWheel = false
showColorPicker = true
}
val context = LocalContext.current
var selected by remember { mutableIntStateOf(0) }
LaunchedEffect(showColorWheel) {
if (!showColorWheel) return@LaunchedEffect
ColorPickerDialogBuilder
.with(context)
.wheelType(ColorPickerView.WHEEL_TYPE.CIRCLE)
.density(7)
.setOnColorChangedListener { which ->
selected = which
}
.setOnColorSelectedListener { which ->
selected = which
}
.lightnessSliderOnly()
.setPositiveButton(R.string.ok) { _, _, _ ->
selectColor(selected)
}
.setNegativeButton(R.string.cancel) { _, _ ->
showColorPicker = true
}
.apply {
if (color != 0) {
initialColor(color)
}
}
.build()
.apply {
setOnDismissListener {
showColorWheel = false
}
}
.show()
}
}
SettingRow(
modifier = Modifier.clickable(onClick = selectColor),
modifier = Modifier.clickable(onClick = { showColorPicker = true }),
left = {
val context = LocalContext.current
val adjusted = remember(color) {
ColorProvider(context).getThemeColor(color).primaryColor
}
IconButton(onClick = { selectColor() }) {
Box(
modifier = Modifier.size(56.dp),
contentAlignment = Alignment.Center,
) {
if (color == 0) {
Icon(
imageVector = Icons.Outlined.NotInterested,
@ -49,15 +131,11 @@ fun SelectColorRow(
contentDescription = null
)
} else {
val borderColor = colorResource(R.color.icon_tint_with_alpha) // colorResource(R.color.text_tertiary)
Box(
modifier = Modifier.size(56.dp),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.size(24.dp)) {
drawCircle(color = Color(adjusted))
drawCircle(color = borderColor, style = Stroke(width = 4.0f))
}
val borderColor =
colorResource(R.color.icon_tint_with_alpha) // colorResource(R.color.text_tertiary)
Canvas(modifier = Modifier.size(24.dp)) {
drawCircle(color = Color(adjusted))
drawCircle(color = borderColor, style = Stroke(width = 4.0f))
}
}
}
@ -70,7 +148,7 @@ fun SelectColorRow(
},
right = {
if (color != 0) {
IconButton(onClick = clearColor) {
IconButton(onClick = { selectColor(0) }) {
Icon(
imageVector = Icons.Outlined.Clear,
contentDescription = null
@ -79,15 +157,18 @@ fun SelectColorRow(
}
}
)
}
@Composable
@Preview(showBackground = true)
private fun ColorSelectPreview () {
TasksTheme {
SelectColorRow(
hasPro = true,
colors = emptyList(),
purchase = {},
color = Color.Red.toArgb(),
selectColor = {},
clearColor = {}
)
}
}

Loading…
Cancel
Save