Replace BarChart by new Kotlin implementation
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 350 B |
|
Before Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 424 B |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -19,6 +19,11 @@
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
interface Component {
|
||||
interface View {
|
||||
fun draw(canvas: Canvas)
|
||||
}
|
||||
|
||||
interface DataView : View {
|
||||
var dataOffset: Int
|
||||
val dataColumnWidth: Double
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.isoron.platform.time
|
||||
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
|
||||
@@ -32,15 +33,6 @@ enum class DayOfWeek(val index: Int) {
|
||||
SATURDAY(6),
|
||||
}
|
||||
|
||||
data class Timestamp(val millisSince1970: Long) {
|
||||
val localDate: LocalDate
|
||||
get() {
|
||||
val millisSince2000 = millisSince1970 - 946684800000
|
||||
val daysSince2000 = millisSince2000 / 86400000
|
||||
return LocalDate(daysSince2000.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
data class LocalDate(val daysSince2000: Int) {
|
||||
|
||||
var yearCache = -1
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
package org.isoron.uhabits.core.models;
|
||||
|
||||
import org.apache.commons.lang3.builder.*;
|
||||
import org.isoron.platform.time.LocalDate;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
|
||||
import kotlin.*;
|
||||
@@ -66,6 +68,13 @@ public final class Timestamp implements Comparable<Timestamp>
|
||||
return unixTime;
|
||||
}
|
||||
|
||||
public LocalDate toLocalDate()
|
||||
{
|
||||
long millisSince2000 = unixTime - 946684800000L;
|
||||
int daysSince2000 = (int) (millisSince2000 / 86400000);
|
||||
return new LocalDate(daysSince2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns -1 if this timestamp is older than the given timestamp, 1 if this
|
||||
* timestamp is newer, or zero if they are equal.
|
||||
|
||||
@@ -22,6 +22,7 @@ package org.isoron.uhabits.core.ui;
|
||||
import androidx.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.ui.views.*;
|
||||
|
||||
public abstract class ThemeSwitcher
|
||||
{
|
||||
@@ -59,6 +60,8 @@ public abstract class ThemeSwitcher
|
||||
|
||||
public abstract int getSystemTheme();
|
||||
|
||||
public abstract Theme getCurrentTheme();
|
||||
|
||||
public boolean isNightMode()
|
||||
{
|
||||
int systemTheme = getSystemTheme();
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardPresente
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardViewModel
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardViewModel
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
|
||||
data class ShowHabitViewModel(
|
||||
val title: String = "",
|
||||
@@ -60,6 +61,7 @@ class ShowHabitPresenter {
|
||||
fun present(
|
||||
habit: Habit,
|
||||
preferences: Preferences,
|
||||
theme: Theme,
|
||||
): ShowHabitViewModel {
|
||||
return ShowHabitViewModel(
|
||||
title = habit.name,
|
||||
@@ -100,6 +102,7 @@ class ShowHabitPresenter {
|
||||
firstWeekday = preferences.firstWeekday,
|
||||
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
|
||||
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
|
||||
theme = theme,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,9 +23,11 @@ import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.groupedSum
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
|
||||
data class BarCardViewModel(
|
||||
val theme: Theme,
|
||||
val boolSpinnerPosition: Int,
|
||||
val bucketSize: Int,
|
||||
val color: PaletteColor,
|
||||
@@ -43,6 +45,7 @@ class BarCardPresenter {
|
||||
firstWeekday: Int,
|
||||
numericalSpinnerPosition: Int,
|
||||
boolSpinnerPosition: Int,
|
||||
theme: Theme,
|
||||
): BarCardViewModel {
|
||||
val bucketSize = if (habit.isNumerical) {
|
||||
numericalBucketSizes[numericalSpinnerPosition]
|
||||
@@ -57,6 +60,7 @@ class BarCardPresenter {
|
||||
isNumerical = habit.isNumerical,
|
||||
)
|
||||
return BarCardViewModel(
|
||||
theme = theme,
|
||||
entries = entries,
|
||||
bucketSize = bucketSize,
|
||||
color = habit.color,
|
||||
|
||||
@@ -21,7 +21,7 @@ package org.isoron.uhabits.core.ui.views
|
||||
|
||||
import org.isoron.platform.gui.Canvas
|
||||
import org.isoron.platform.gui.Color
|
||||
import org.isoron.platform.gui.Component
|
||||
import org.isoron.platform.gui.DataView
|
||||
import org.isoron.platform.gui.TextAlign
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.platform.time.LocalDateFormatter
|
||||
@@ -32,12 +32,13 @@ import kotlin.math.round
|
||||
class BarChart(
|
||||
var theme: Theme,
|
||||
var dateFormatter: LocalDateFormatter,
|
||||
) : Component {
|
||||
) : DataView {
|
||||
|
||||
// Data
|
||||
var series = mutableListOf<List<Double>>()
|
||||
var colors = mutableListOf<Color>()
|
||||
var axis = listOf<LocalDate>()
|
||||
override var dataOffset = 0
|
||||
|
||||
// Style
|
||||
var paddingTop = 20.0
|
||||
@@ -50,6 +51,9 @@ class BarChart(
|
||||
var nGridlines = 6
|
||||
var backgroundColor = theme.cardBackgroundColor
|
||||
|
||||
override val dataColumnWidth: Double
|
||||
get() = barWidth + barMargin * 2
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = canvas.getWidth()
|
||||
val height = canvas.getHeight()
|
||||
@@ -75,8 +79,11 @@ class BarChart(
|
||||
barMargin
|
||||
|
||||
fun drawColumn(s: Int, c: Int) {
|
||||
val dataColumn = nColumns - c - 1
|
||||
val value = if (dataColumn < series[s].size) series[s][dataColumn] else 0.0
|
||||
val dataColumn = nColumns - c - 1 + dataOffset
|
||||
val value = when {
|
||||
dataColumn < 0 || dataColumn >= series[s].size -> 0.0
|
||||
else -> series[s][dataColumn]
|
||||
}
|
||||
if (value <= 0) return
|
||||
val perc = value / maxValue
|
||||
val barHeight = round(maxBarHeight * perc)
|
||||
@@ -142,12 +149,12 @@ class BarChart(
|
||||
canvas.setTextAlign(TextAlign.CENTER)
|
||||
var prevMonth = -1
|
||||
var prevYear = -1
|
||||
val isLargeInterval = (axis[0].distanceTo(axis[1]) > 300)
|
||||
val isLargeInterval = axis.size < 2 || (axis[0].distanceTo(axis[1]) > 300)
|
||||
|
||||
for (c in 0 until nColumns) {
|
||||
val x = barGroupOffset(c)
|
||||
val dataColumn = nColumns - c - 1
|
||||
if (dataColumn >= axis.size) continue
|
||||
val dataColumn = nColumns - c - 1 + dataOffset
|
||||
if (dataColumn < 0 || dataColumn >= axis.size) continue
|
||||
val date = axis[dataColumn]
|
||||
if (isLargeInterval) {
|
||||
canvas.drawText(
|
||||
|
||||
@@ -21,8 +21,8 @@ package org.isoron.uhabits.core.ui.views
|
||||
|
||||
import org.isoron.platform.gui.Canvas
|
||||
import org.isoron.platform.gui.Color
|
||||
import org.isoron.platform.gui.Component
|
||||
import org.isoron.platform.gui.TextAlign
|
||||
import org.isoron.platform.gui.View
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.platform.time.LocalDateFormatter
|
||||
import kotlin.math.floor
|
||||
@@ -33,7 +33,7 @@ class CalendarChart(
|
||||
var color: Color,
|
||||
var theme: Theme,
|
||||
var dateFormatter: LocalDateFormatter
|
||||
) : Component {
|
||||
) : View {
|
||||
|
||||
var padding = 5.0
|
||||
var backgroundColor = Color(0xFFFFFF)
|
||||
|
||||
@@ -21,15 +21,15 @@ package org.isoron.uhabits.core.ui.views
|
||||
|
||||
import org.isoron.platform.gui.Canvas
|
||||
import org.isoron.platform.gui.Color
|
||||
import org.isoron.platform.gui.Component
|
||||
import org.isoron.platform.gui.Font
|
||||
import org.isoron.platform.gui.FontAwesome
|
||||
import org.isoron.platform.gui.View
|
||||
|
||||
class CheckmarkButton(
|
||||
private val value: Int,
|
||||
private val color: Color,
|
||||
private val theme: Theme
|
||||
) : Component {
|
||||
) : View {
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.setFont(Font.FONT_AWESOME)
|
||||
canvas.setFontSize(theme.smallTextSize * 1.5)
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
package org.isoron.uhabits.core.ui.views
|
||||
|
||||
import org.isoron.platform.gui.Canvas
|
||||
import org.isoron.platform.gui.Component
|
||||
import org.isoron.platform.gui.Font
|
||||
import org.isoron.platform.gui.View
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.platform.time.LocalDateFormatter
|
||||
|
||||
@@ -30,7 +30,7 @@ class HabitListHeader(
|
||||
private val nButtons: Int,
|
||||
private val theme: Theme,
|
||||
private val fmt: LocalDateFormatter
|
||||
) : Component {
|
||||
) : View {
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = canvas.getWidth()
|
||||
|
||||
@@ -21,8 +21,8 @@ package org.isoron.uhabits.core.ui.views
|
||||
|
||||
import org.isoron.platform.gui.Canvas
|
||||
import org.isoron.platform.gui.Color
|
||||
import org.isoron.platform.gui.Component
|
||||
import org.isoron.platform.gui.Font
|
||||
import org.isoron.platform.gui.View
|
||||
import java.lang.String.format
|
||||
import kotlin.math.round
|
||||
|
||||
@@ -52,7 +52,7 @@ class NumberButton(
|
||||
val threshold: Double,
|
||||
val units: String,
|
||||
val theme: Theme
|
||||
) : Component {
|
||||
) : View {
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = canvas.getWidth()
|
||||
|
||||
@@ -21,7 +21,7 @@ package org.isoron.uhabits.core.ui.views
|
||||
|
||||
import org.isoron.platform.gui.Canvas
|
||||
import org.isoron.platform.gui.Color
|
||||
import org.isoron.platform.gui.Component
|
||||
import org.isoron.platform.gui.View
|
||||
import java.lang.String.format
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -33,7 +33,7 @@ class Ring(
|
||||
val radius: Double,
|
||||
val theme: Theme,
|
||||
val label: Boolean = false
|
||||
) : Component {
|
||||
) : View {
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = canvas.getWidth()
|
||||
|
||||
@@ -22,27 +22,23 @@ package org.isoron.uhabits.core.ui.views
|
||||
import org.isoron.platform.gui.Color
|
||||
|
||||
abstract class Theme {
|
||||
val toolbarColor = Color(0xffffff)
|
||||
open val appBackgroundColor = Color(0xf4f4f4)
|
||||
open val cardBackgroundColor = Color(0xFAFAFA)
|
||||
open val headerBackgroundColor = Color(0xeeeeee)
|
||||
open val headerBorderColor = Color(0xcccccc)
|
||||
open val headerTextColor = Color(0x9E9E9E)
|
||||
open val highContrastTextColor = Color(0x202020)
|
||||
open val itemBackgroundColor = Color(0xffffff)
|
||||
open val lowContrastTextColor = Color(0xe0e0e0)
|
||||
open val mediumContrastTextColor = Color(0x9E9E9E)
|
||||
open val statusBarBackgroundColor = Color(0x333333)
|
||||
open val toolbarBackgroundColor = Color(0xf4f4f4)
|
||||
open val toolbarColor = Color(0xffffff)
|
||||
|
||||
val lowContrastTextColor = Color(0xe0e0e0)
|
||||
val mediumContrastTextColor = Color(0x9E9E9E)
|
||||
val highContrastTextColor = Color(0x202020)
|
||||
|
||||
val cardBackgroundColor = Color(0xFFFFFF)
|
||||
val appBackgroundColor = Color(0xf4f4f4)
|
||||
val toolbarBackgroundColor = Color(0xf4f4f4)
|
||||
val statusBarBackgroundColor = Color(0x333333)
|
||||
|
||||
val headerBackgroundColor = Color(0xeeeeee)
|
||||
val headerBorderColor = Color(0xcccccc)
|
||||
val headerTextColor = mediumContrastTextColor
|
||||
|
||||
val itemBackgroundColor = Color(0xffffff)
|
||||
|
||||
fun color(paletteIndex: Int): Color {
|
||||
open fun color(paletteIndex: Int): Color {
|
||||
return when (paletteIndex) {
|
||||
0 -> Color(0xD32F2F)
|
||||
1 -> Color(0x512DA8)
|
||||
1 -> Color(0xE64A19)
|
||||
2 -> Color(0xF57C00)
|
||||
3 -> Color(0xFF8F00)
|
||||
4 -> Color(0xF9A825)
|
||||
@@ -58,6 +54,9 @@ abstract class Theme {
|
||||
14 -> Color(0x8E24AA)
|
||||
15 -> Color(0xD81B60)
|
||||
16 -> Color(0x5D4037)
|
||||
17 -> Color(0x424242)
|
||||
18 -> Color(0x757575)
|
||||
19 -> Color(0x9E9E9E)
|
||||
else -> Color(0x000000)
|
||||
}
|
||||
}
|
||||
@@ -68,3 +67,44 @@ abstract class Theme {
|
||||
}
|
||||
|
||||
class LightTheme : Theme()
|
||||
|
||||
class DarkTheme : Theme() {
|
||||
override val appBackgroundColor = Color(0x212121)
|
||||
override val cardBackgroundColor = Color(0x303030)
|
||||
override val headerBackgroundColor = Color(0x212121)
|
||||
override val headerBorderColor = Color(0xcccccc)
|
||||
override val headerTextColor = Color(0x9E9E9E)
|
||||
override val highContrastTextColor = Color(0xF5F5F5)
|
||||
override val itemBackgroundColor = Color(0xffffff)
|
||||
override val lowContrastTextColor = Color(0x424242)
|
||||
override val mediumContrastTextColor = Color(0x9E9E9E)
|
||||
override val statusBarBackgroundColor = Color(0x333333)
|
||||
override val toolbarBackgroundColor = Color(0xf4f4f4)
|
||||
override val toolbarColor = Color(0xffffff)
|
||||
|
||||
override fun color(paletteIndex: Int): Color {
|
||||
return when (paletteIndex) {
|
||||
0 -> Color(0xEF9A9A)
|
||||
1 -> Color(0xFFAB91)
|
||||
2 -> Color(0xFFCC80)
|
||||
3 -> Color(0xFFECB3)
|
||||
4 -> Color(0xFFF59D)
|
||||
5 -> Color(0xE6EE9C)
|
||||
6 -> Color(0xC5E1A5)
|
||||
7 -> Color(0x69F0AE)
|
||||
8 -> Color(0x80CBC4)
|
||||
9 -> Color(0x80DEEA)
|
||||
10 -> Color(0x81D4FA)
|
||||
11 -> Color(0x64B5F6)
|
||||
12 -> Color(0x9FA8DA)
|
||||
13 -> Color(0xB39DDB)
|
||||
14 -> Color(0xCE93D8)
|
||||
15 -> Color(0xF48FB1)
|
||||
16 -> Color(0xBCAAA4)
|
||||
17 -> Color(0xF5F5F5)
|
||||
18 -> Color(0xE0E0E0)
|
||||
19 -> Color(0x9E9E9E)
|
||||
else -> Color(0xFFFFFF)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.awt.image.BufferedImage.TYPE_INT_ARGB
|
||||
class JavaCanvasTest {
|
||||
@Test
|
||||
fun run() = runBlocking {
|
||||
assertRenders("components/CanvasTest.png", createCanvas(500, 400).apply { drawTestImage() })
|
||||
assertRenders("views/CanvasTest.png", createCanvas(500, 400).apply { drawTestImage() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,9 @@ suspend fun assertRenders(
|
||||
width: Int,
|
||||
height: Int,
|
||||
expectedPath: String,
|
||||
component: Component,
|
||||
view: View,
|
||||
) {
|
||||
val canvas = createCanvas(width, height)
|
||||
component.draw(canvas)
|
||||
view.draw(canvas)
|
||||
assertRenders(expectedPath, canvas)
|
||||
}
|
||||
|
||||
@@ -17,19 +17,17 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.components
|
||||
package org.isoron.uhabits.core.ui.views
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.isoron.platform.gui.assertRenders
|
||||
import org.isoron.platform.time.JavaLocalDateFormatter
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.uhabits.core.ui.views.BarChart
|
||||
import org.isoron.uhabits.core.ui.views.LightTheme
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
|
||||
class BarChartTest {
|
||||
val base = "components/BarChart"
|
||||
val base = "views/BarChart"
|
||||
val today = LocalDate(2015, 1, 25)
|
||||
val fmt = JavaLocalDateFormatter(Locale.US)
|
||||
val theme = LightTheme()
|
||||
@@ -37,11 +35,20 @@ class BarChartTest {
|
||||
val axis = (0..100).map { today.minus(it) }
|
||||
val series1 = listOf(200.0, 0.0, 150.0, 137.0, 0.0, 0.0, 500.0, 30.0, 100.0, 0.0, 300.0)
|
||||
|
||||
@Test
|
||||
fun testDraw() = runBlocking {
|
||||
init {
|
||||
component.axis = axis
|
||||
component.series.add(series1)
|
||||
component.colors.add(theme.color(8))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDraw() = runBlocking {
|
||||
assertRenders(300, 200, "$base/base.png", component)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDrawWithOffset() = runBlocking {
|
||||
component.dataOffset = 5
|
||||
assertRenders(300, 200, "$base/offset.png", component)
|
||||
}
|
||||
}
|
||||
|
||||