From c3ad086806c4c1e67a9d57077bb8afa55a592e90 Mon Sep 17 00:00:00 2001 From: Naveen Date: Tue, 11 Apr 2023 13:49:30 +0530 Subject: [PATCH] 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 --- .../calendar/pro/activities/EventActivity.kt | 13 ++-- .../pro/adapters/CheckableColorAdapter.kt | 55 +++++++++++++++ .../pro/dialogs/SelectEventColorDialog.kt | 67 ++++++------------- .../pro/dialogs/SelectEventTypeColorDialog.kt | 10 +-- .../calendar/pro/helpers/CalDAVHelper.kt | 34 ++++++++-- .../pro/helpers/HsvColorComparator.kt | 40 +++++++++++ .../pro/views/AutoGridLayoutManager.kt | 34 ++++++++++ app/src/main/res/layout/activity_event.xml | 2 +- .../res/layout/checkable_color_button.xml | 12 ++++ .../res/layout/dialog_select_event_color.xml | 40 ++++++----- 10 files changed, 224 insertions(+), 83 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/calendar/pro/adapters/CheckableColorAdapter.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/HsvColorComparator.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/calendar/pro/views/AutoGridLayoutManager.kt create mode 100644 app/src/main/res/layout/checkable_color_button.xml diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/activities/EventActivity.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/activities/EventActivity.kt index ddc73170f..ac1457b1c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/activities/EventActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/activities/EventActivity.kt @@ -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) } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/adapters/CheckableColorAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/adapters/CheckableColorAdapter.kt new file mode 100644 index 000000000..523696159 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/adapters/CheckableColorAdapter.kt @@ -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() { + + 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) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventColorDialog.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventColorDialog.kt index e4590ad2d..6ccf9dd41 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventColorDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventColorDialog.kt @@ -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() - } } diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventTypeColorDialog.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventTypeColorDialog.kt index 54b794f9e..0467bb229 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventTypeColorDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventTypeColorDialog.kt @@ -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()) diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/CalDAVHelper.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/CalDAVHelper.kt index 218e43ffe..5a0a3cf53 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/CalDAVHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/CalDAVHelper.kt @@ -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 { - val colors = mutableMapOf() + val colors = mutableMapOf() 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 + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/HsvColorComparator.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/HsvColorComparator.kt new file mode 100644 index 000000000..3e83c2b68 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/HsvColorComparator.kt @@ -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 { + 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 + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/views/AutoGridLayoutManager.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/views/AutoGridLayoutManager.kt new file mode 100644 index 000000000..2428a85d2 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/views/AutoGridLayoutManager.kt @@ -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) + } +} diff --git a/app/src/main/res/layout/activity_event.xml b/app/src/main/res/layout/activity_event.xml index 5c722a01b..5f5382cf9 100644 --- a/app/src/main/res/layout/activity_event.xml +++ b/app/src/main/res/layout/activity_event.xml @@ -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" /> + diff --git a/app/src/main/res/layout/dialog_select_event_color.xml b/app/src/main/res/layout/dialog_select_event_color.xml index 18698e72b..014aceeca 100644 --- a/app/src/main/res/layout/dialog_select_event_color.xml +++ b/app/src/main/res/layout/dialog_select_event_color.xml @@ -1,22 +1,26 @@ - + android:layout_height="wrap_content" + android:paddingHorizontal="@dimen/activity_margin" + android:paddingTop="@dimen/big_margin" + android:paddingBottom="@dimen/medium_margin"> - + - - - - +