Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor weekday row logic and add some APIs #23

Merged
merged 7 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,21 @@ observer.setLifecycle(lifecycle) // If we want to unregister the observer on des
observer.register() // The observer should be registered manually.
```

## Weekdays

By default, the calendar will use localized weekdays in 'short' format. The format of localized weekdays can be changed via `weekdayType`. Currently there's only two options:

- `WeekdayType.SHORT`. If user locale is English, weekdays will look like: Mon, Tue, Wed, Thu, Fri, Sat, Sun
- `WeekdayType.NARROW`. If user locale is English, weekdays will look like: M, T, W, T, F, S, S. **This option only works when API level >= 24. If API level is lower, weekdays in 'short' format will be used.**

If you want to change weekdays, it can be done via `weekdays`:

```kotlin
rangeCalendarView.weekdays = arrayOf("0", "1", "2", "3", "4", "5", "6")
```

If you want back to using localized weekdays, pass null to `weekdays`.

## Other

- `getVibrateOnSelectingCustomRange()/setVibrateOnSelectingCustomRange()` to get or set whether the device should
Expand Down Expand Up @@ -276,11 +291,17 @@ observer.register() // The observer should be registered manually.
<td>rangeCalendar_weekdayType</td>
<td>
Type of weekday. <br/>
Format of weekdays is very dependent on locale. In example, English locale is used.
Format of weekdays depends on locale. In example, English locale is used.
<ul>
<li>shortName (WeekdayType.SHORT) - weekdays will look like Mob, Tue, Wed. </li>
<li>narrowName (WeekdayType.NARROW) - weekdays will look like M, T, W,. </li>
</ul>
</ul>
</td>
</tr>
<tr>
<td>rangeCalendar_weekdays</td>
<td>
Custom weekdays. The value should be a string array, whose length is 7.
</td>
</tr>
<tr>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.github.pelmenstar1.rangecalendar

import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Locale
import kotlin.test.assertContentEquals
import kotlin.test.assertSame

@RunWith(AndroidJUnit4::class)
class WeekdayDataTests {
@Suppress("UNCHECKED_CAST")
private fun assertContentEquals(expected: Array<out String>?, actual: Array<out String>?) {
// Pass null message to make Kotlin use the right method.
assertContentEquals(expected as Array<String>?, actual as Array<String>?, message = null)
}

@Test
fun getTest() {
val data = WeekdayData.get(Locale.ENGLISH)

val expectedShortWeekdays = arrayOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
val expectedNarrowWeekdays = if (Build.VERSION.SDK_INT >= 24) {
arrayOf( "M", "T", "W", "T", "F", "S", "S")
} else {
null
}

assertContentEquals(expectedShortWeekdays, data.shortWeekdays)
assertContentEquals(expectedNarrowWeekdays, data.narrowWeekdays)
}

@Test
fun getWeekdaysTest() {
val data = WeekdayData.get(Locale.ENGLISH)

val actualShortWeekdays = data.getWeekdays(WeekdayType.SHORT)
assertSame(data.shortWeekdays, actualShortWeekdays)

if (Build.VERSION.SDK_INT >= 24) {
val actualNarrowWeekdays = data.getWeekdays(WeekdayType.NARROW)

assertSame(data.narrowWeekdays, actualNarrowWeekdays)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package com.github.pelmenstar1.rangecalendar
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.icu.text.DateFormatSymbols
import android.os.Build
import com.github.pelmenstar1.rangecalendar.utils.darkerColor
import com.github.pelmenstar1.rangecalendar.utils.getColorFromAttribute
import com.github.pelmenstar1.rangecalendar.utils.getColorStateListFromAttribute
Expand All @@ -23,21 +21,13 @@ internal class CalendarResources(context: Context) {
val disabledTextColor: Int

val weekdayTextSize: Float
val weekdays: Array<String>
val defaultWeekdayData: WeekdayData

val dayNumberTextSize: Float

// Can only be used when text size is default one (dayNumberTextSize)
// It's a precomputed text sizes for day numbers using default system font and dayNumberTextSize
val defaultDayNumberSizes: PackedSizeArray

/*
* These are precomputed values for default weekdayTextSize and cannot be used for another text size.
*/
val defaultWeekdayWidths: FloatArray
val defaultShortWeekdayRowHeight: Float
val defaultNarrowWeekdayRowHeight: Float
/* ----- */

val weekdayRowMarginBottom: Float
val colorControlNormal: ColorStateList

Expand All @@ -47,7 +37,8 @@ internal class CalendarResources(context: Context) {
colorPrimary = context.getColorFromAttribute(androidx.appcompat.R.attr.colorPrimary)
colorPrimaryDark = colorPrimary.darkerColor(0.4f)
textColor = getTextColor(context)
colorControlNormal = context.getColorStateListFromAttribute(androidx.appcompat.R.attr.colorControlNormal)
colorControlNormal =
context.getColorStateListFromAttribute(androidx.appcompat.R.attr.colorControlNormal)
outMonthTextColor = colorControlNormal.getColorForState(ENABLED_STATE, 0)
disabledTextColor = colorControlNormal.getColorForState(EMPTY_STATE, 0)
hoverColor = getHoverColor(context)
Expand All @@ -61,53 +52,7 @@ internal class CalendarResources(context: Context) {
// Compute text size of numbers in [0; 31]
defaultDayNumberSizes = getTextBoundsArray(DAYS, dayNumberTextSize)

// First element in getShortWeekDays() is empty and actual items start from 1
// It's better to copy them to another array where elements start from 0
val locale = context.getLocaleCompat()
val shortWeekdays: Array<String>
var narrowWeekdays: Array<String>? = null

if (Build.VERSION.SDK_INT >= 24) {
val symbols = DateFormatSymbols.getInstance(locale)
shortWeekdays = symbols.shortWeekdays

narrowWeekdays = symbols.getWeekdays(
DateFormatSymbols.FORMAT,
DateFormatSymbols.NARROW
)
} else {
shortWeekdays = java.text.DateFormatSymbols.getInstance(locale).shortWeekdays
}

val weekdaysLength = if (Build.VERSION.SDK_INT >= 24) 14 else 7

@Suppress("UNCHECKED_CAST")
weekdays = arrayOfNulls<String?>(weekdaysLength) as Array<String>
System.arraycopy(shortWeekdays, 1, weekdays, 0, 7)

if (narrowWeekdays != null) {
System.arraycopy(narrowWeekdays, 1, weekdays, 7, 7)
}

defaultWeekdayWidths = FloatArray(weekdaysLength)
defaultShortWeekdayRowHeight = computeWeekdayWidthAndMaxHeight(SHORT_WEEKDAYS_OFFSET)
defaultNarrowWeekdayRowHeight = if (Build.VERSION.SDK_INT >= 24) {
computeWeekdayWidthAndMaxHeight(NARROW_WEEKDAYS_OFFSET)
} else Float.NaN
}

private fun computeWeekdayWidthAndMaxHeight(offset: Int): Float {
var maxHeight = -1

getTextBoundsArray(weekdays, offset, offset + 7, weekdayTextSize, typeface = null) { i, width, height ->
if (height > maxHeight) {
maxHeight = height
}

defaultWeekdayWidths[i + offset] = width.toFloat()
}

return maxHeight.toFloat()
defaultWeekdayData = WeekdayData.get(context.getLocaleCompat())
}

companion object {
Expand All @@ -119,13 +64,11 @@ internal class CalendarResources(context: Context) {
)

private val SINGLE_INT_ARRAY = IntArray(1)
private val HOVER_STATE = intArrayOf(android.R.attr.state_hovered, android.R.attr.state_enabled)
private val HOVER_STATE =
intArrayOf(android.R.attr.state_hovered, android.R.attr.state_enabled)
private val ENABLED_STATE = intArrayOf(android.R.attr.state_enabled)
private val EMPTY_STATE = IntArray(0)

const val SHORT_WEEKDAYS_OFFSET = 0
const val NARROW_WEEKDAYS_OFFSET = 7

fun getDayText(day: Int) = DAYS[day - 1]

@SuppressLint("PrivateResource")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ import kotlin.math.min

// It will never be XML layout, so there's no need to match conventions
@SuppressLint("ViewConstructor")
internal class RangeCalendarGridView(
context: Context,
val cr: CalendarResources
) : View(context) {
internal class RangeCalendarGridView(context: Context, val cr: CalendarResources) : View(context) {
interface OnSelectionListener {
fun onSelectionCleared()
fun onSelection(range: CellRange)
Expand Down Expand Up @@ -244,15 +241,9 @@ internal class RangeCalendarGridView(
private var onAnimationEnd: (() -> Unit)? = null
private var animationHandler: (() -> Unit)? = null

var isFirstDaySunday = false
private val touchHelper: TouchHelper

private var weekdayType = WeekdayType.SHORT

private var isWeekdayMeasurementsDirty = false

private var weekdayWidths: FloatArray = cr.defaultWeekdayWidths
private var maxWeekdayHeight: Float = cr.defaultShortWeekdayRowHeight
private val weekdayRow: WeekdayRow

private var isDayNumberMeasurementsDirty = false
private var dayNumberSizes = cr.defaultDayNumberSizes
Expand Down Expand Up @@ -332,6 +323,8 @@ internal class RangeCalendarGridView(
cellHoverPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}

weekdayRow = WeekdayRow(cr.defaultWeekdayData, weekdayPaint)
}

private inline fun updateUIState(block: () -> Unit) =
Expand Down Expand Up @@ -435,14 +428,24 @@ internal class RangeCalendarGridView(

fun setWeekdayTextSize(size: Float) = updateUIState(weekdayPaint.textSize, size) {
weekdayPaint.textSize = size
weekdayRow.onMeasurementsChanged()

if (size != cr.weekdayTextSize) {
isWeekdayMeasurementsDirty = true
}
onGridTopChanged()
}

fun setWeekdayTypeface(typeface: Typeface?) = updateUIState(weekdayPaint.typeface, typeface) {
weekdayPaint.typeface = typeface
weekdayRow.onMeasurementsChanged()

onGridTopChanged()
}

fun setCustomWeekdays(weekdays: Array<out String>?) = updateUIState(weekdayRow.weekdays, weekdays) {
weekdayRow.weekdays = weekdays

onGridChanged()
}

fun setInMonthTextColor(color: Int) = updateUIState(inMonthTextColor, color) {
inMonthTextColor = color
}
Expand Down Expand Up @@ -539,20 +542,8 @@ internal class RangeCalendarGridView(
// There is no narrow weekdays before API < 24, so we need to resolve it
val type = _type.resolved()

if (weekdayType != type) {
weekdayType = type

// If weekdays measurements are from calendar resources then we can use precomputed values and don't make them "dirty"
if (weekdayWidths === cr.defaultWeekdayWidths) {
maxWeekdayHeight = if (type == WeekdayType.SHORT) {
cr.defaultShortWeekdayRowHeight
} else {
cr.defaultNarrowWeekdayRowHeight
}
} else {
// Widths and max height needs to be precomputed if they are not from calendar resources.
isWeekdayMeasurementsDirty = true
}
if (weekdayRow.type != type) {
weekdayRow.type = type

onGridTopChanged()
invalidate()
Expand Down Expand Up @@ -1259,66 +1250,7 @@ internal class RangeCalendarGridView(
}

private fun drawWeekdayRow(c: Canvas) {
if (isWeekdayMeasurementsDirty) {
isWeekdayMeasurementsDirty = false

measureWeekdays()
}

var x = cr.hPadding + columnWidth * 0.5f

val offset = if (weekdayType == WeekdayType.SHORT)
CalendarResources.SHORT_WEEKDAYS_OFFSET
else
CalendarResources.NARROW_WEEKDAYS_OFFSET

val startIndex = if (isFirstDaySunday) 0 else 1

// If widths are from calendar resources, then we do not need any widths-offset to get the size,
// because it contains both short and narrow (if API level >= 24) weekday widths.
// But if the widths are recomputed, not from calendar resources,
// then it contains either short or narrow (if API level >= 24) weekday widths and
// we need to shift the index.
val widthsOffset = if (weekdayWidths === cr.defaultWeekdayWidths) 0 else offset

for (i in offset + startIndex until offset + 7) {
drawWeekday(c, i, widthsOffset, x)

x += columnWidth
}

if (!isFirstDaySunday) {
drawWeekday(c, offset, widthsOffset, x)
}
}

private fun drawWeekday(c: Canvas, index: Int, widthsOffset: Int, midX: Float) {
val textX = midX - weekdayWidths[index - widthsOffset] * 0.5f
val textY = maxWeekdayHeight

c.drawText(cr.weekdays[index], textX, textY, weekdayPaint)
}

private fun measureWeekdays() {
val offset = if (weekdayType == WeekdayType.SHORT) 0 else 7

var maxHeight = -1

// If weekdays are from calendar resources,
// then create new array to not overwrite the resources' one.
if (weekdayWidths === cr.defaultWeekdayWidths) {
weekdayWidths = FloatArray(7)
}

weekdayPaint.getTextBoundsArray(cr.weekdays, offset, offset + 7) { i, width, height ->
if (height > maxHeight) {
maxHeight = height
}

weekdayWidths[i] = width.toFloat()
}

maxWeekdayHeight = maxHeight.toFloat()
weekdayRow.draw(c, cr.hPadding, columnWidth)
}

private fun drawHover(c: Canvas) {
Expand Down Expand Up @@ -1483,12 +1415,15 @@ internal class RangeCalendarGridView(
updateGradientBoundsIfNeeded()
updateSelectionStateConfiguration()

// Height of the view depends on gridTop()
requestLayout()

// y-axis of entries depends on type of weekday, so we need to refresh accessibility info
touchHelper.invalidateRoot()
}

private fun gridTop(): Float {
return maxWeekdayHeight + cr.weekdayRowMarginBottom
return weekdayRow.height + cr.weekdayRowMarginBottom
}

// It'd be better if cellRoundRadius() returns round radius that isn't greater than half of cell size.
Expand Down
Loading