Skip to content

Commit 27fe8f9

Browse files
committed
add color gradient to streak calendar
1 parent 3bc625a commit 27fe8f9

4 files changed

Lines changed: 101 additions & 25 deletions

File tree

data_local/src/main/java/com/example/util/simpletimetracker/data_local/record/RecordDao.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ interface RecordDao {
5050
suspend fun getFromRangeByType(typesIds: List<Long>, start: Long, end: Long): List<RecordWithRecordTagsDBO>
5151

5252
@Transaction
53-
@Query("SELECT * FROM records " +
54-
"WHERE time_ended <= :timeStarted AND type_id NOT IN (:ignoreTypeIds) " +
55-
"ORDER BY time_ended DESC LIMIT 1")
53+
@Query(
54+
"SELECT * FROM records " +
55+
"WHERE time_ended <= :timeStarted AND type_id NOT IN (:ignoreTypeIds) " +
56+
"ORDER BY time_ended DESC LIMIT 1",
57+
)
5658
suspend fun getPrev(timeStarted: Long, ignoreTypeIds: List<Long>): RecordWithRecordTagsDBO?
5759

5860
@Transaction

features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/customView/SeriesCalendarView.kt

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.example.util.simpletimetracker.feature_views.extension.dpToPx
2121
import kotlin.math.ceil
2222
import kotlin.math.roundToInt
2323
import kotlinx.parcelize.Parcelize
24+
import androidx.core.content.withStyledAttributes
2425

2526
class SeriesCalendarView @JvmOverloads constructor(
2627
context: Context,
@@ -48,6 +49,8 @@ class SeriesCalendarView @JvmOverloads constructor(
4849
private var panFactor: Float = 0f
4950
private var lastPanFactor: Float = 0f
5051
private var chartFullWidth: Float = 0f
52+
private var cellNotPresentColorAlpha = 0.2f
53+
private var cellPresentColorAlphaBase = 0.6f
5154
private var listener: (ViewData, Coordinates) -> Unit = { _, _ -> }
5255

5356
private val cellPresentPaint: Paint = Paint()
@@ -64,6 +67,9 @@ class SeriesCalendarView @JvmOverloads constructor(
6467
onSlideStop = ::onSwipeStop,
6568
)
6669

70+
private val colorLevelAlphaMap: Map<ViewData.ColorLevel, Int> =
71+
ViewData.ColorLevel.entries.associateWith { getPresentCellFinalAlpha(it) }
72+
6773
init {
6874
initArgs(context, attrs, defStyleAttr)
6975
initPaint()
@@ -146,16 +152,14 @@ class SeriesCalendarView @JvmOverloads constructor(
146152
defStyleAttr: Int = 0,
147153
) {
148154
context
149-
.obtainStyledAttributes(
155+
.withStyledAttributes(
150156
attrs,
151157
R.styleable.SeriesCalendarView, defStyleAttr, 0,
152-
)
153-
.run {
158+
) {
154159
legendTextSize =
155160
getDimensionPixelSize(R.styleable.SeriesCalendarView_seriesLegendTextSize, 14).toFloat()
156161
legendTextColor =
157162
getColor(R.styleable.SeriesCalendarView_seriesLegendTextColor, Color.BLACK)
158-
recycle()
159163
}
160164
}
161165

@@ -169,7 +173,7 @@ class SeriesCalendarView @JvmOverloads constructor(
169173
isAntiAlias = true
170174
color = cellColor
171175
style = Paint.Style.FILL
172-
alpha = (255 * 0.3f).toInt()
176+
alpha = (255 * cellNotPresentColorAlpha).toInt()
173177
}
174178
legendTextPaint.apply {
175179
isAntiAlias = true
@@ -181,7 +185,7 @@ class SeriesCalendarView @JvmOverloads constructor(
181185
private fun initEditMode() {
182186
if (isInEditMode) {
183187
(30 downTo 1).toList()
184-
.map { ViewData.Present(0, "") }
188+
.map { ViewData.Present(0, "", ViewData.ColorLevel.THREE) }
185189
.let { setData(it, rowsCount) }
186190
}
187191
}
@@ -244,6 +248,7 @@ class SeriesCalendarView @JvmOverloads constructor(
244248
cellRadius,
245249
cellRadius,
246250
if (point.cell is ViewData.Present) {
251+
cellPresentPaint.alpha = colorLevelAlphaMap[point.cell.colorLevel].orZero()
247252
cellPresentPaint
248253
} else {
249254
cellNotPresentPaint
@@ -252,6 +257,17 @@ class SeriesCalendarView @JvmOverloads constructor(
252257
}
253258
}
254259

260+
private fun getPresentCellFinalAlpha(colorLevel: ViewData.ColorLevel): Int {
261+
val maxLevel = ViewData.ColorLevel.entries.size
262+
val alphaCoefficient = if (maxLevel > 1) {
263+
colorLevel.value.toFloat() / (maxLevel - 1)
264+
} else {
265+
1f
266+
}
267+
val baseAlpha = cellPresentColorAlphaBase + (1 - cellPresentColorAlphaBase) * alphaCoefficient
268+
return (255 * baseAlpha).toInt()
269+
}
270+
255271
@Suppress("UNUSED_PARAMETER")
256272
private fun onSwipe(offset: Float, direction: SwipeDetector.Direction, event: MotionEvent) {
257273
if (direction.isHorizontal()) {
@@ -310,18 +326,26 @@ class SeriesCalendarView @JvmOverloads constructor(
310326
data class Present(
311327
override val rangeStart: Long,
312328
override val monthLegend: String,
329+
val colorLevel: ColorLevel,
313330
) : ViewData
314331

315332
data class NotPresent(
316333
override val rangeStart: Long,
317334
override val monthLegend: String,
318335
) : ViewData
319336

320-
object Dummy : ViewData {
337+
data object Dummy : ViewData {
321338
// Not needed for dummy view.
322339
override val rangeStart: Long = 0L
323340
override val monthLegend: String = ""
324341
}
342+
343+
enum class ColorLevel(val value: Int) {
344+
ONE(0),
345+
TWO(1),
346+
THREE(2),
347+
FOUR(3),
348+
}
325349
}
326350

327351
@Parcelize

features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailStatsInteractor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class StatisticsDetailStatsInteractor @Inject constructor(
118118
}
119119
val recordsAllIcon = StatisticsDetailCardInternalViewData.Icon(
120120
iconDrawable = R.drawable.arrow_right,
121-
iconColor = resourceRepo.getThemedAttr(R.attr.appInactiveColor, isDarkTheme)
121+
iconColor = resourceRepo.getThemedAttr(R.attr.appInactiveColor, isDarkTheme),
122122
)
123123

124124
fun formatInterval(value: Long?): String {

features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailStreaksInteractor.kt

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ class StatisticsDetailStreaksInteractor @Inject constructor(
244244
streaksGoal = streaksGoal,
245245
goal = goal,
246246
rangeLength = rangeLength,
247+
calculateCalendar = true,
247248
)
248249

249250
// If range is not all data - calculate current streak on all data.
@@ -259,6 +260,7 @@ class StatisticsDetailStreaksInteractor @Inject constructor(
259260
streaksGoal = streaksGoal,
260261
goal = goal,
261262
rangeLength = rangeLength,
263+
calculateCalendar = false,
262264
).currentStreak
263265
stats.copy(currentStreak = currentStreak)
264266
}
@@ -285,6 +287,7 @@ class StatisticsDetailStreaksInteractor @Inject constructor(
285287
streaksGoal: StreaksGoal,
286288
goal: RecordTypeGoal?,
287289
rangeLength: RangeLength,
290+
calculateCalendar: Boolean,
288291
): IntermediateData {
289292
// If doesn't have a goal - count any duration.
290293
val defaultGoalType = RecordTypeGoal.Type.Duration(1)
@@ -379,19 +382,29 @@ class StatisticsDetailStreaksInteractor @Inject constructor(
379382
streakEnd = 0
380383
}
381384
}
382-
when (streaksType) {
383-
StatisticsStreaksType.LONGEST -> data.sortByDescending { it.value }
384-
StatisticsStreaksType.LATEST -> data.sortByDescending { it.streakEnd }
385+
386+
val rangeCurrentData = if (calculateCalendar) {
387+
when (streaksType) {
388+
StatisticsStreaksType.LONGEST -> data.sortByDescending { it.value }
389+
StatisticsStreaksType.LATEST -> data.sortByDescending { it.streakEnd }
390+
}
391+
data.take(MAX_STREAKS_IN_CHART)
392+
} else {
393+
emptyList()
394+
}
395+
396+
val calendarData = if (calculateCalendar) {
397+
mapDurationsToCalendarData(
398+
data = durations,
399+
firstDayOfWeek = firstDayOfWeek,
400+
startOfDayShift = startOfDayShift,
401+
goalValue = goalValue,
402+
goalSubtype = goalSubtype,
403+
rangeLength = rangeLength,
404+
)
405+
} else {
406+
emptyList()
385407
}
386-
val rangeCurrentData = data.take(MAX_STREAKS_IN_CHART)
387-
val calendarData = mapDurationsToCalendarData(
388-
data = durations,
389-
firstDayOfWeek = firstDayOfWeek,
390-
startOfDayShift = startOfDayShift,
391-
goalValue = goalValue,
392-
goalSubtype = goalSubtype,
393-
rangeLength = rangeLength,
394-
)
395408

396409
return IntermediateData(
397410
longestStreak = longestStreak.takeIf { it > 1 }.orZero(), // Series of one day makes no sense.
@@ -517,6 +530,15 @@ class StatisticsDetailStreaksInteractor @Inject constructor(
517530
0
518531
}
519532
val dummyDays = List(daysToAdd) { SeriesCalendarView.ViewData.Dummy }
533+
var minDataValue: Long? = null
534+
var maxDataValue: Long? = null
535+
data.forEach { (_, value) ->
536+
if (value > 0 && minDataValue == null) minDataValue = value
537+
if (value > 0 && value > maxDataValue.orZero()) maxDataValue = value
538+
if (value > 0 && value < minDataValue.orZero()) minDataValue = value
539+
}
540+
val dataValueRange = maxDataValue.orZero() - minDataValue.orZero()
541+
val dataValueRangeStep = dataValueRange / SeriesCalendarView.ViewData.ColorLevel.entries.size
520542

521543
return dummyDays + data
522544
.map {
@@ -532,9 +554,21 @@ class StatisticsDetailStreaksInteractor @Inject constructor(
532554
""
533555
}
534556
if (isReached) {
535-
SeriesCalendarView.ViewData.Present(rangeStart, monthLegend)
557+
val colorLevel = mapColorLevel(
558+
dataValueRangeStep = dataValueRangeStep,
559+
value = it.second,
560+
minDataValue = minDataValue,
561+
)
562+
SeriesCalendarView.ViewData.Present(
563+
rangeStart = rangeStart,
564+
monthLegend = monthLegend,
565+
colorLevel = colorLevel,
566+
)
536567
} else {
537-
SeriesCalendarView.ViewData.NotPresent(rangeStart, monthLegend)
568+
SeriesCalendarView.ViewData.NotPresent(
569+
rangeStart = rangeStart,
570+
monthLegend = monthLegend,
571+
)
538572
}
539573
}
540574
.reversed()
@@ -692,6 +726,22 @@ class StatisticsDetailStreaksInteractor @Inject constructor(
692726
}
693727
}
694728

729+
private fun mapColorLevel(
730+
dataValueRangeStep: Long,
731+
value: Long,
732+
minDataValue: Long?,
733+
): SeriesCalendarView.ViewData.ColorLevel {
734+
return if (dataValueRangeStep == 0L) {
735+
SeriesCalendarView.ViewData.ColorLevel.entries.last()
736+
} else {
737+
((value - minDataValue.orZero()) / dataValueRangeStep).let {
738+
SeriesCalendarView.ViewData.ColorLevel.entries
739+
.firstOrNull { level -> level.value == it.toInt() }
740+
?: SeriesCalendarView.ViewData.ColorLevel.entries.last()
741+
}
742+
}
743+
}
744+
695745
private data class IntermediateData(
696746
val longestStreak: Long,
697747
val currentStreak: Streak,

0 commit comments

Comments
 (0)