New Compose color picker

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

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

@ -1,5 +1,6 @@
package org.tasks.compose.settings package org.tasks.compose.settings
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -12,7 +13,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.res.colorResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.flask.colorpicker.ColorPickerView
import com.flask.colorpicker.builder.ColorPickerDialogBuilder
import org.tasks.R import org.tasks.R
import org.tasks.compose.Constants import org.tasks.compose.Constants
import org.tasks.kmp.org.tasks.compose.settings.SettingRow import org.tasks.kmp.org.tasks.compose.settings.SettingRow
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.themes.ThemeColor
@Composable @Composable
fun SelectColorRow( fun SelectColorRow(
hasPro: Boolean,
color: Int, color: Int,
selectColor: () -> Unit, colors: List<ThemeColor>,
clearColor: () -> Unit 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( SettingRow(
modifier = Modifier.clickable(onClick = selectColor), modifier = Modifier.clickable(onClick = { showColorPicker = true }),
left = { left = {
val context = LocalContext.current val context = LocalContext.current
val adjusted = remember(color) { val adjusted = remember(color) {
ColorProvider(context).getThemeColor(color).primaryColor ColorProvider(context).getThemeColor(color).primaryColor
} }
IconButton(onClick = { selectColor() }) { Box(
modifier = Modifier.size(56.dp),
contentAlignment = Alignment.Center,
) {
if (color == 0) { if (color == 0) {
Icon( Icon(
imageVector = Icons.Outlined.NotInterested, imageVector = Icons.Outlined.NotInterested,
@ -49,18 +131,14 @@ fun SelectColorRow(
contentDescription = null contentDescription = null
) )
} else { } else {
val borderColor = colorResource(R.color.icon_tint_with_alpha) // colorResource(R.color.text_tertiary) val borderColor =
Box( colorResource(R.color.icon_tint_with_alpha) // colorResource(R.color.text_tertiary)
modifier = Modifier.size(56.dp),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.size(24.dp)) { Canvas(modifier = Modifier.size(24.dp)) {
drawCircle(color = Color(adjusted)) drawCircle(color = Color(adjusted))
drawCircle(color = borderColor, style = Stroke(width = 4.0f)) drawCircle(color = borderColor, style = Stroke(width = 4.0f))
} }
} }
} }
}
}, },
center = { center = {
Text( Text(
@ -70,7 +148,7 @@ fun SelectColorRow(
}, },
right = { right = {
if (color != 0) { if (color != 0) {
IconButton(onClick = clearColor) { IconButton(onClick = { selectColor(0) }) {
Icon( Icon(
imageVector = Icons.Outlined.Clear, imageVector = Icons.Outlined.Clear,
contentDescription = null contentDescription = null
@ -79,15 +157,18 @@ fun SelectColorRow(
} }
} }
) )
}
@Composable @Composable
@Preview(showBackground = true) @Preview(showBackground = true)
private fun ColorSelectPreview () { private fun ColorSelectPreview () {
TasksTheme { TasksTheme {
SelectColorRow( SelectColorRow(
hasPro = true,
colors = emptyList(),
purchase = {},
color = Color.Red.toArgb(), color = Color.Red.toArgb(),
selectColor = {}, selectColor = {},
clearColor = {}
) )
} }
} }

Loading…
Cancel
Save