mirror of
https://github.com/SimpleMobileTools/Simple-Calendar.git
synced 2025-06-05 21:59:17 +02:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
@@ -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())
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
12
app/src/main/res/layout/checkable_color_button.xml
Normal file
12
app/src/main/res/layout/checkable_color_button.xml
Normal 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" />
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user