Redesign event color picker

- Use circular color buttons instead of radio buttons
- Use recyclerview for efficiency and speed
- Sort color list using a color comparator
- Darken colors to ensure white text is visible on top of them
This commit is contained in:
Naveen
2023-04-11 13:49:30 +05:30
parent 8394572099
commit c3ad086806
10 changed files with 224 additions and 83 deletions

View File

@@ -10,6 +10,7 @@ import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.CalendarContract.Attendees
import android.provider.CalendarContract.Colors
import android.provider.ContactsContract.CommonDataKinds
import android.provider.ContactsContract.CommonDataKinds.StructuredName
import android.provider.ContactsContract.Data
@@ -834,16 +835,18 @@ class EventActivity : SimpleActivity() {
hideKeyboard()
ensureBackgroundThread {
val eventType = eventsHelper.getEventTypeWithCalDAVCalendarId(calendarId = mEventCalendarId)!!
val eventColorsMap = calDAVHelper.getAvailableCalDAVCalendarColors(eventType, Colors.TYPE_EVENT)
val eventColors = eventColorsMap.keys.toIntArray()
runOnUiThread {
val selectedColor = if (mEventColor == 0) {
val currentColor = if (mEventColor == 0) {
eventType.color
} else {
mEventColor
}
SelectEventColorDialog(activity = this, eventType = eventType, selectedColor = selectedColor) { color ->
if (color != eventType.color) {
mEventColor = color
updateEventColorInfo(eventType.color)
SelectEventColorDialog(activity = this, colors = eventColors, currentColor = currentColor) { newColor ->
if (newColor != currentColor) {
mEventColor = newColor
updateEventColorInfo(defaultColor = eventType.color)
}
}
}

View File

@@ -0,0 +1,55 @@
package com.simplemobiletools.calendar.pro.adapters
import android.annotation.SuppressLint
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.calendar.pro.R
import com.simplemobiletools.commons.extensions.applyColorFilter
import kotlinx.android.synthetic.main.checkable_color_button.view.*
class CheckableColorAdapter(private val activity: Activity, private val colors: IntArray, var currentColor: Int) :
RecyclerView.Adapter<CheckableColorAdapter.CheckableColorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheckableColorViewHolder {
val itemView = LayoutInflater.from(activity)
.inflate(R.layout.checkable_color_button, parent, false)
return CheckableColorViewHolder(itemView)
}
override fun onBindViewHolder(holder: CheckableColorViewHolder, position: Int) {
val color = colors[position]
holder.bindView(color = color, checked = color == currentColor)
}
override fun getItemCount() = colors.size
@SuppressLint("NotifyDataSetChanged")
private fun updateSelection(color: Int) {
currentColor = color
notifyDataSetChanged()
}
inner class CheckableColorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindView(color: Int, checked: Boolean) {
itemView.checkable_color_button.apply {
backgroundTintList = ColorStateList.valueOf(color)
setOnClickListener {
updateSelection(color)
}
if (checked) {
setImageResource(R.drawable.ic_check_vector)
applyColorFilter(Color.WHITE)
} else {
setImageDrawable(null)
}
}
}
}
}

View File

@@ -1,66 +1,39 @@
package com.simplemobiletools.calendar.pro.dialogs
import android.app.Activity
import android.provider.CalendarContract.Colors
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.calendar.pro.R
import com.simplemobiletools.calendar.pro.extensions.calDAVHelper
import com.simplemobiletools.calendar.pro.models.EventType
import com.simplemobiletools.calendar.pro.adapters.CheckableColorAdapter
import com.simplemobiletools.calendar.pro.views.AutoGridLayoutManager
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.getProperBackgroundColor
import com.simplemobiletools.commons.extensions.setFillWithStroke
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_select_event_type_color.view.*
import kotlinx.android.synthetic.main.radio_button_with_color.view.*
import kotlinx.android.synthetic.main.dialog_select_event_color.view.*
class SelectEventColorDialog(val activity: Activity, val eventType: EventType, val selectedColor: Int, val callback: (color: Int) -> Unit) {
private var dialog: AlertDialog? = null
private val radioGroup: RadioGroup
private var wasInit = false
private val colors = activity.calDAVHelper.getAvailableCalDAVCalendarColors(eventType, Colors.TYPE_EVENT).keys
class SelectEventColorDialog(val activity: Activity, val colors: IntArray, var currentColor: Int, val callback: (color: Int) -> Unit) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_select_event_color, null) as ViewGroup
radioGroup = view.dialog_select_event_type_color_radio
addRadioButton(index = colors.size.inc(), color = eventType.color)
colors.forEachIndexed { index, color ->
addRadioButton(index, color)
val colorAdapter = CheckableColorAdapter(activity, colors, currentColor)
view.color_grid.apply {
layoutManager = AutoGridLayoutManager(
context = activity,
itemWidth = activity.resources.getDimensionPixelSize(R.dimen.smaller_icon_size)
)
adapter = colorAdapter
}
wasInit = true
activity.getAlertDialogBuilder()
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
setPositiveButton(R.string.ok) { dialog, _ ->
callback(colorAdapter.currentColor)
dialog?.dismiss()
}
setNeutralButton(R.string.default_calendar_color) { dialog, _ ->
callback(0)
dialog?.dismiss()
}
activity.setupDialogStuff(view, this, R.string.event_color)
}
}
private fun addRadioButton(index: Int, color: Int) {
val view = activity.layoutInflater.inflate(R.layout.radio_button_with_color, null)
(view.dialog_radio_button as RadioButton).apply {
text = if (color == eventType.color) activity.getString(R.string.default_color) else String.format("#%06X", 0xFFFFFF and color)
isChecked = color == selectedColor
id = index
}
view.dialog_radio_color.setFillWithStroke(color, activity.getProperBackgroundColor())
view.setOnClickListener {
viewClicked(color)
}
radioGroup.addView(view, RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
}
private fun viewClicked(color: Int) {
if (!wasInit) {
return
}
callback(color)
dialog?.dismiss()
}
}

View File

@@ -21,7 +21,7 @@ class SelectEventTypeColorDialog(val activity: Activity, val eventType: EventTyp
private var dialog: AlertDialog? = null
private val radioGroup: RadioGroup
private var wasInit = false
private val colors = activity.calDAVHelper.getAvailableCalDAVCalendarColors(eventType, Colors.TYPE_CALENDAR)
private val colors = activity.calDAVHelper.getAvailableCalDAVCalendarColors(eventType, Colors.TYPE_CALENDAR).keys
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_select_event_type_color, null) as ViewGroup
@@ -30,8 +30,8 @@ class SelectEventTypeColorDialog(val activity: Activity, val eventType: EventTyp
showCustomColorPicker()
}
colors.forEach { (color, key) ->
addRadioButton(key.toInt(), color)
colors.forEachIndexed { index, color ->
addRadioButton(index, color)
}
wasInit = true
@@ -47,12 +47,12 @@ class SelectEventTypeColorDialog(val activity: Activity, val eventType: EventTyp
}
}
private fun addRadioButton(colorKey: Int, color: Int) {
private fun addRadioButton(index: Int, color: Int) {
val view = activity.layoutInflater.inflate(R.layout.radio_button_with_color, null)
(view.dialog_radio_button as RadioButton).apply {
text = if (color == 0) activity.getString(R.string.transparent) else String.format("#%06X", 0xFFFFFF and color)
isChecked = color == eventType.color
id = colorKey
id = index
}
view.dialog_radio_color.setFillWithStroke(color, activity.getProperBackgroundColor())

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.graphics.Color
import android.provider.CalendarContract.*
import android.widget.Toast
import com.google.gson.Gson
@@ -19,6 +20,7 @@ import org.joda.time.DateTimeZone
import org.joda.time.format.DateTimeFormat
import java.util.*
import kotlin.math.max
import kotlin.math.min
@SuppressLint("MissingPermission")
class CalDAVHelper(val context: Context) {
@@ -118,9 +120,18 @@ class CalDAVHelper(val context: Context) {
return colors[eventType.color]
}
// darkens the given color to ensure that white text is clearly visible on top of it
private fun getDisplayColorFromColor(color: Int): Int {
val hsv = FloatArray(3)
Color.colorToHSV(color, hsv)
hsv[1] = min(hsv[1] * SATURATION_ADJUST, 1.0f)
hsv[2] = hsv[2] * INTENSITY_ADJUST
return Color.HSVToColor(hsv)
}
@SuppressLint("MissingPermission")
fun getAvailableCalDAVCalendarColors(eventType: EventType, colorType: Int = Colors.TYPE_CALENDAR): Map<Int, String> {
val colors = mutableMapOf<String, Int>()
val colors = mutableMapOf<Int, String>()
val uri = Colors.CONTENT_URI
val projection = arrayOf(Colors.COLOR, Colors.COLOR_KEY)
val selection = "${Colors.COLOR_TYPE} = ? AND ${Colors.ACCOUNT_NAME} = ?"
@@ -129,11 +140,10 @@ class CalDAVHelper(val context: Context) {
context.queryCursor(uri, projection, selection, selectionArgs) { cursor ->
val colorKey = cursor.getStringValue(Colors.COLOR_KEY)
val color = cursor.getIntValue(Colors.COLOR)
colors[colorKey] = color
val displayColor = getDisplayColorFromColor(color)
colors[displayColor] = colorKey
}
return colors.toSortedMap().entries
.associate { (key, color) -> color to key }
return colors.toSortedMap(HsvColorComparator())
}
@SuppressLint("MissingPermission")
@@ -200,7 +210,12 @@ class CalDAVHelper(val context: Context) {
val reminders = getCalDAVEventReminders(id)
val attendees = Gson().toJson(getCalDAVEventAttendees(id))
val availability = cursor.getIntValue(Events.AVAILABILITY)
val color = cursor.getIntValue(Events.EVENT_COLOR)
val color = cursor.getIntValueOrNull(Events.EVENT_COLOR)
val displayColor = if (color != null) {
getDisplayColorFromColor(color)
} else {
0
}
if (endTS == 0L) {
val duration = cursor.getStringValue(Events.DURATION) ?: ""
@@ -222,7 +237,7 @@ class CalDAVHelper(val context: Context) {
reminder1?.type ?: REMINDER_NOTIFICATION, reminder2?.type ?: REMINDER_NOTIFICATION,
reminder3?.type ?: REMINDER_NOTIFICATION, repeatRule.repeatInterval, repeatRule.repeatRule,
repeatRule.repeatLimit, ArrayList(), attendees, importId, eventTimeZone, allDay, eventTypeId,
source = source, availability = availability, color = color
source = source, availability = availability, color = displayColor
)
if (event.getIsAllDay()) {
@@ -534,4 +549,9 @@ class CalDAVHelper(val context: Context) {
private fun getCalDAVEventImportId(calendarId: Int, eventId: Long) = "$CALDAV-$calendarId-$eventId"
private fun refreshCalDAVCalendar(event: Event) = context.refreshCalDAVCalendars(event.getCalDAVCalendarId().toString(), false)
companion object {
private const val INTENSITY_ADJUST = 0.8f
private const val SATURATION_ADJUST = 1.3f
}
}

View File

@@ -0,0 +1,40 @@
package com.simplemobiletools.calendar.pro.helpers
import android.graphics.Color
/**
* A color comparator which compares based on hue, saturation, and value.
* Source: AOSP Color picker, https://cs.android.com/android/platform/superproject/+/master:frameworks/opt/colorpicker/src/com/android/colorpicker/HsvColorComparator.java
*/
class HsvColorComparator : Comparator<Int?> {
override fun compare(lhs: Int?, rhs: Int?): Int {
val hsv = FloatArray(3)
Color.colorToHSV(lhs!!, hsv)
val hue1 = hsv[0]
val sat1 = hsv[1]
val val1 = hsv[2]
val hsv2 = FloatArray(3)
Color.colorToHSV(rhs!!, hsv2)
val hue2 = hsv2[0]
val sat2 = hsv2[1]
val val2 = hsv2[2]
if (hue1 < hue2) {
return 1
} else if (hue1 > hue2) {
return -1
} else {
if (sat1 < sat2) {
return 1
} else if (sat1 > sat2) {
return -1
} else {
if (val1 < val2) {
return 1
} else if (val1 > val2) {
return -1
}
}
}
return 0
}
}

View File

@@ -0,0 +1,34 @@
package com.simplemobiletools.calendar.pro.views
import android.content.Context
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.max
/**
* RecyclerView GridLayoutManager but with automatic spanCount calculation
* @param itemWidth: Grid item width in pixels. Will be used to calculate span count.
*/
class AutoGridLayoutManager(
context: Context,
private var itemWidth: Int
) : GridLayoutManager(context, 1) {
init {
require(itemWidth >= 0)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
val width = width
val height = height
if (itemWidth > 0 && width > 0 && height > 0) {
val totalSpace = if (orientation == VERTICAL) {
width - paddingRight - paddingLeft
} else {
height - paddingTop - paddingBottom
}
spanCount = max(1, totalSpace / itemWidth)
}
super.onLayoutChildren(recycler, state)
}
}

View File

@@ -536,7 +536,7 @@
android:layout_toStartOf="@+id/event_caldav_color"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="@string/change_color"
android:text="@string/event_color"
android:textSize="@dimen/day_text_size" />
<ImageView

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/checkable_color_button"
android:layout_width="@dimen/smaller_icon_size"
android:layout_height="@dimen/smaller_icon_size"
android:layout_margin="@dimen/small_margin"
android:background="@drawable/button_background_rounded"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/medium_margin"
tools:src="@drawable/ic_check_vector" />

View File

@@ -1,22 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog_select_event_type_color_scrollview"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/color_grid_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/activity_margin"
android:paddingTop="@dimen/big_margin"
android:paddingBottom="@dimen/medium_margin">
<RelativeLayout
android:id="@+id/dialog_select_event_type_other_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/color_grid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:overScrollMode="ifContentScrolls"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="16"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/checkable_color_button"
tools:spanCount="6" />
<RadioGroup
android:id="@+id/dialog_select_event_type_color_radio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingEnd="@dimen/activity_margin"
android:paddingBottom="@dimen/normal_margin" />
</RelativeLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>