Replace BarChart by new Kotlin implementation

pull/707/head
Alinson S. Xavier 5 years ago
parent e97cdce467
commit 354c930d85

@ -1,83 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import androidx.test.filters.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class BarChartTest extends BaseViewTest
{
private static final String BASE_PATH = "common/BarChart/";
private BarChart view;
@Override
@Before
public void setUp()
{
super.setUp();
Habit habit = fixtures.createLongNumericalHabit();
view = new BarChart(targetContext);
Timestamp today = DateUtils.getToday();
EntryList entries = habit.getComputedEntries();
view.setEntries(entries.getByInterval(today.minus(20), today));
view.setColor(PaletteUtilsKt.toThemedAndroidColor(habit.getColor(), targetContext));
measureView(view, dpToPixels(300), dpToPixels(200));
}
@Test
public void testRender() throws Throwable
{
assertRenders(view, BASE_PATH + "render.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, BASE_PATH + "renderDataOffset.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(view, dpToPixels(200), dpToPixels(200));
assertRenders(view, BASE_PATH + "renderDifferentSize.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsTransparencyEnabled(true);
assertRenders(view, BASE_PATH + "renderTransparent.png");
}
}

@ -25,8 +25,6 @@ import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Typeface import android.graphics.Typeface
import android.text.TextPaint import android.text.TextPaint
import android.util.AttributeSet
import android.view.View
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
class AndroidCanvas : Canvas { class AndroidCanvas : Canvas {
@ -107,7 +105,7 @@ class AndroidCanvas : Canvas {
} }
override fun setFontSize(size: Double) { override fun setFontSize(size: Double) {
textPaint.textSize = size.toDp() * 1.07f textPaint.textSize = size.toDp()
} }
override fun setStrokeWidth(size: Double) { override fun setStrokeWidth(size: Double) {
@ -156,14 +154,3 @@ class AndroidCanvas : Canvas {
return AndroidImage(bmp) return AndroidImage(bmp)
} }
} }
class AndroidCanvasTestView(context: Context, attrs: AttributeSet) : View(context, attrs) {
val canvas = AndroidCanvas()
override fun onDraw(canvas: android.graphics.Canvas) {
this.canvas.context = context
this.canvas.innerCanvas = canvas
this.canvas.density = resources.displayMetrics.density.toDouble()
this.canvas.drawTestImage()
}
}

@ -0,0 +1,109 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.widget.Scroller
/**
* An AndroidView that implements scrolling.
*/
class AndroidDataView(
context: Context,
attrs: AttributeSet,
) : AndroidView<DataView>(context, attrs),
GestureDetector.OnGestureListener,
ValueAnimator.AnimatorUpdateListener {
private val detector = GestureDetector(context, this)
private val scroller = Scroller(context, null, true)
private val scrollAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
addUpdateListener(this@AndroidDataView)
}
override fun onTouchEvent(event: MotionEvent?) = detector.onTouchEvent(event)
override fun onDown(e: MotionEvent?) = true
override fun onShowPress(e: MotionEvent?) = Unit
override fun onSingleTapUp(e: MotionEvent?) = false
override fun onLongPress(e: MotionEvent?) = Unit
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
dx: Float,
dy: Float,
): Boolean {
if (Math.abs(dx) > Math.abs(dy)) {
val parent = parent
parent?.requestDisallowInterceptTouchEvent(true)
}
scroller.startScroll(
scroller.currX,
scroller.currY,
-dx.toInt(),
dy.toInt(),
0
)
scroller.computeScrollOffset()
updateDataOffset()
return true
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float,
): Boolean {
scroller.fling(scroller.currX, scroller.currY, velocityX.toInt() / 2, 0, 0, 10000, 0, 0)
invalidate()
scrollAnimator.duration = scroller.duration.toLong()
scrollAnimator.start()
return false
}
override fun onAnimationUpdate(animation: ValueAnimator?) {
if (!scroller.isFinished) {
scroller.computeScrollOffset()
updateDataOffset()
} else {
scrollAnimator.cancel()
}
}
fun resetDataOffset() {
scroller.finalX = 0
scroller.computeScrollOffset()
updateDataOffset()
}
private fun updateDataOffset() {
var newDataOffset: Int = scroller.currX / (view.dataColumnWidth * canvas.density).toInt()
newDataOffset = Math.max(0, newDataOffset)
if (newDataOffset != view.dataOffset) {
view.dataOffset = newDataOffset
postInvalidate()
}
}
}

@ -0,0 +1,34 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import android.content.Context
import android.util.AttributeSet
class AndroidTestView(context: Context, attrs: AttributeSet) : android.view.View(context, attrs) {
val canvas = AndroidCanvas()
override fun onDraw(canvas: android.graphics.Canvas) {
this.canvas.context = context
this.canvas.innerCanvas = canvas
this.canvas.density = resources.displayMetrics.density.toDouble()
this.canvas.drawTestImage()
}
}

@ -0,0 +1,39 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import android.content.Context
import android.util.AttributeSet
open class AndroidView<T : View>(
context: Context,
attrs: AttributeSet,
) : android.view.View(context, attrs) {
lateinit var view: T
val canvas = AndroidCanvas()
override fun onDraw(canvas: android.graphics.Canvas) {
this.canvas.context = context
this.canvas.innerCanvas = canvas
this.canvas.density = resources.displayMetrics.density.toDouble()
view.draw(this.canvas)
}
}

@ -28,6 +28,9 @@ import androidx.core.content.ContextCompat
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.ThemeSwitcher import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.views.DarkTheme
import org.isoron.uhabits.core.ui.views.LightTheme
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope import org.isoron.uhabits.inject.ActivityScope
@ -35,9 +38,11 @@ import org.isoron.uhabits.inject.ActivityScope
class AndroidThemeSwitcher class AndroidThemeSwitcher
constructor( constructor(
@ActivityContext val context: Context, @ActivityContext val context: Context,
preferences: Preferences preferences: Preferences,
) : ThemeSwitcher(preferences) { ) : ThemeSwitcher(preferences) {
private var currentTheme: Theme = LightTheme()
override fun getSystemTheme(): Int { override fun getSystemTheme(): Int {
if (SDK_INT < 29) return THEME_LIGHT if (SDK_INT < 29) return THEME_LIGHT
val uiMode = context.resources.configuration.uiMode val uiMode = context.resources.configuration.uiMode
@ -48,17 +53,24 @@ constructor(
} }
} }
override fun getCurrentTheme(): Theme {
return currentTheme
}
override fun applyDarkTheme() { override fun applyDarkTheme() {
currentTheme = DarkTheme()
context.setTheme(R.style.AppBaseThemeDark) context.setTheme(R.style.AppBaseThemeDark)
(context as Activity).window.navigationBarColor = (context as Activity).window.navigationBarColor =
ContextCompat.getColor(context, R.color.grey_900) ContextCompat.getColor(context, R.color.grey_900)
} }
override fun applyLightTheme() { override fun applyLightTheme() {
currentTheme = LightTheme()
context.setTheme(R.style.AppBaseTheme) context.setTheme(R.style.AppBaseTheme)
} }
override fun applyPureBlackTheme() { override fun applyPureBlackTheme() {
currentTheme = DarkTheme()
context.setTheme(R.style.AppBaseThemeDark_PureBlack) context.setTheme(R.style.AppBaseThemeDark_PureBlack)
(context as Activity).window.navigationBarColor = (context as Activity).window.navigationBarColor =
ContextCompat.getColor(context, R.color.black) ContextCompat.getColor(context, R.color.black)

@ -1,474 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.util.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class BarChart extends ScrollableChart
{
private static final PorterDuffXfermode XFERMODE_CLEAR =
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
private static final PorterDuffXfermode XFERMODE_SRC =
new PorterDuffXfermode(PorterDuff.Mode.SRC);
private Paint pGrid;
private float em;
private SimpleDateFormat dfMonth;
private SimpleDateFormat dfDay;
private SimpleDateFormat dfYear;
private Paint pText, pGraph;
private RectF rect, prevRect;
private int baseSize;
private int paddingTop;
private float columnWidth;
private int columnHeight;
private int nColumns;
private int textColor;
private int gridColor;
@Nullable
private List<Entry> entries;
private int bucketSize = 7;
private int backgroundColor;
private Bitmap drawingCache;
private Canvas cacheCanvas;
private boolean isTransparencyEnabled;
private int skipYear = 0;
private String previousYearText;
private String previousMonthText;
private double maxValue;
private int primaryColor;
public BarChart(Context context)
{
super(context);
init();
}
public BarChart(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public void populateWithRandomData()
{
Random random = new Random();
List<Entry> entries = new LinkedList<>();
Timestamp today = DateUtils.getToday();
for (int i = 1; i < 100; i++)
{
int value = random.nextInt(1000);
entries.add(new Entry(today.minus(i), value));
}
setEntries(entries);
}
public void setBucketSize(int bucketSize)
{
this.bucketSize = bucketSize;
postInvalidate();
}
public void setEntries(@NonNull List<Entry> entries)
{
this.entries = entries;
maxValue = 1.0;
for (Entry e : entries)
maxValue = Math.max(maxValue, e.getValue());
maxValue = Math.ceil(maxValue / 1000 * 1.05) * 1000;
postInvalidate();
}
public void setColor(int primaryColor)
{
StyledResources res = new StyledResources(getContext());
this.primaryColor = primaryColor;
postInvalidate();
}
public void setIsTransparencyEnabled(boolean enabled)
{
this.isTransparencyEnabled = enabled;
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Canvas activeCanvas;
if (isTransparencyEnabled)
{
if (drawingCache == null) initCache(getWidth(), getHeight());
activeCanvas = cacheCanvas;
drawingCache.eraseColor(Color.TRANSPARENT);
}
else
{
activeCanvas = canvas;
}
if (entries == null) return;
rect.set(0, 0, nColumns * columnWidth, columnHeight);
rect.offset(0, paddingTop);
drawGrid(activeCanvas, rect);
pText.setColor(textColor);
pGraph.setColor(primaryColor);
prevRect.setEmpty();
previousMonthText = "";
previousYearText = "";
skipYear = 0;
for (int k = 0; k < nColumns; k++)
{
int offset = nColumns - k - 1 + getDataOffset();
if (offset >= entries.size()) continue;
double value = entries.get(offset).getValue();
Timestamp timestamp = entries.get(offset).getTimestamp();
int height = (int) (columnHeight * value / maxValue);
rect.set(0, 0, baseSize, height);
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,
paddingTop + columnHeight - height);
drawValue(activeCanvas, rect, value);
drawBar(activeCanvas, rect, value);
prevRect.set(rect);
rect.set(0, 0, columnWidth, columnHeight);
rect.offset(k * columnWidth, paddingTop);
drawFooter(activeCanvas, rect, timestamp);
}
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int width,
int height,
int oldWidth,
int oldHeight)
{
if (height < 9) height = 200;
float maxTextSize = getDimension(getContext(), R.dimen.tinyTextSize);
float textSize = height * 0.06f;
pText.setTextSize(Math.min(textSize, maxTextSize));
em = pText.getFontSpacing();
int footerHeight = (int) (3 * em);
paddingTop = (int) (em);
baseSize = (height - footerHeight - paddingTop) / 12;
columnWidth = baseSize;
columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
nColumns = (int) (width / columnWidth);
columnWidth = (float) width / nColumns;
setScrollerBucketSize((int) columnWidth);
columnHeight = 12 * baseSize;
float minStrokeWidth = dpToPixels(getContext(), 1);
pGraph.setTextSize(baseSize * 0.5f);
pGraph.setStrokeWidth(baseSize * 0.1f);
pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
if (isTransparencyEnabled) initCache(width, height);
}
private void drawBar(Canvas canvas, RectF rect, double value)
{
float margin = baseSize * 0.225f;
float round = dpToPixels(getContext(), 2);
int color = primaryColor;
rect.inset(-margin, 0);
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
canvas.drawRoundRect(rect, round, round, pGraph);
rect.inset(margin, 0);
setModeOrColor(pGraph, XFERMODE_SRC, color);
canvas.drawRoundRect(rect, round, round, pGraph);
rect.set(rect.left,
rect.top + rect.height() / 2.0f,
rect.right,
rect.bottom);
canvas.drawRect(rect, pGraph);
if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC);
}
private void drawFooter(Canvas canvas, RectF rect, Timestamp currentDate)
{
String yearText = dfYear.format(currentDate.toJavaDate());
String monthText = dfMonth.format(currentDate.toJavaDate());
String dayText = dfDay.format(currentDate.toJavaDate());
GregorianCalendar calendar = currentDate.toCalendar();
pText.setColor(textColor);
String text;
int year = calendar.get(Calendar.YEAR);
boolean shouldPrintYear = true;
if (yearText.equals(previousYearText)) shouldPrintYear = false;
if (skipYear > 0)
{
skipYear--;
shouldPrintYear = false;
}
if (bucketSize >= 365) shouldPrintYear = true;
if (shouldPrintYear)
{
previousYearText = yearText;
previousMonthText = "";
pText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText);
skipYear = 1;
}
if (bucketSize < 365)
{
if (!monthText.equals(previousMonthText))
{
previousMonthText = monthText;
text = monthText;
}
else
{
text = dayText;
}
canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f,
pText);
}
}
private void drawGrid(Canvas canvas, RectF rGrid)
{
int nRows = 5;
float rowHeight = rGrid.height() / nRows;
pText.setColor(textColor);
pGrid.setColor(gridColor);
for (int i = 0; i < nRows; i++)
{
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
pGrid);
rGrid.offset(0, rowHeight);
}
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
}
private void drawValue(Canvas canvas, RectF rect, double value)
{
if (value == 0) return;
int activeColor = primaryColor;
String label = NumberButtonViewKt.toShortString(value / 1000);
Rect rText = new Rect();
pText.getTextBounds(label, 0, label.length(), rText);
float offset = 0.5f * em;
float x = rect.centerX();
float y = rect.top - offset;
int cap = (int) (-0.1f * em);
rText.offset((int) x, (int) y);
rText.offset(-rText.width() / 2, 0);
rText.inset(3 * cap, cap);
setModeOrColor(pText, XFERMODE_CLEAR, backgroundColor);
canvas.drawRect(rText, pText);
setModeOrColor(pText, XFERMODE_SRC, activeColor);
canvas.drawText(label, x, y, pText);
}
private float getMaxDayWidth()
{
float maxDayWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset();
for (int i = 0; i < 28; i++)
{
day.set(Calendar.DAY_OF_MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxDayWidth = Math.max(maxDayWidth, monthWidth);
}
return maxDayWidth;
}
private float getMaxMonthWidth()
{
float maxMonthWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset();
for (int i = 0; i < 12; i++)
{
day.set(Calendar.MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
}
return maxMonthWidth;
}
private void init()
{
initPaints();
initColors();
initDateFormats();
initRects();
}
private void initCache(int width, int height)
{
if (drawingCache != null) drawingCache.recycle();
drawingCache =
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(drawingCache);
}
private void initColors()
{
StyledResources res = new StyledResources(getContext());
primaryColor = Color.BLACK;
textColor = res.getColor(R.attr.mediumContrastTextColor);
gridColor = res.getColor(R.attr.lowContrastTextColor);
backgroundColor = res.getColor(R.attr.cardBgColor);
}
private void initDateFormats()
{
if (isInEditMode())
{
dfYear = new SimpleDateFormat("yyyy", Locale.US);
dfMonth = new SimpleDateFormat("MMM", Locale.US);
dfDay = new SimpleDateFormat("d", Locale.US);
return;
}
dfYear = DateExtensionsKt.toSimpleDataFormat("yyyy");
dfMonth = DateExtensionsKt.toSimpleDataFormat("MMM");
dfDay = DateExtensionsKt.toSimpleDataFormat("d");
}
private void initPaints()
{
pText = new Paint();
pText.setAntiAlias(true);
pText.setTextAlign(Paint.Align.CENTER);
pGraph = new Paint();
pGraph.setTextAlign(Paint.Align.CENTER);
pGraph.setAntiAlias(true);
pGrid = new Paint();
pGrid.setAntiAlias(true);
}
private void initRects()
{
rect = new RectF();
prevRect = new RectF();
}
private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
{
if (isTransparencyEnabled) p.setXfermode(mode);
else p.setColor(color);
}
}

@ -43,13 +43,14 @@ import org.isoron.uhabits.intents.IntentFactory
class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener { class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
private val presenter = ShowHabitPresenter() val presenter = ShowHabitPresenter()
private lateinit var commandRunner: CommandRunner private lateinit var commandRunner: CommandRunner
private lateinit var menu: ShowHabitMenu private lateinit var menu: ShowHabitMenu
private lateinit var view: ShowHabitView private lateinit var view: ShowHabitView
private lateinit var habit: Habit private lateinit var habit: Habit
private lateinit var preferences: Preferences private lateinit var preferences: Preferences
private lateinit var themeSwitcher: AndroidThemeSwitcher
private val scope = CoroutineScope(Dispatchers.Main) private val scope = CoroutineScope(Dispatchers.Main)
@ -61,7 +62,8 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
habit = habitList.getById(ContentUris.parseId(intent.data!!))!! habit = habitList.getById(ContentUris.parseId(intent.data!!))!!
preferences = appComponent.preferences preferences = appComponent.preferences
commandRunner = appComponent.commandRunner commandRunner = appComponent.commandRunner
AndroidThemeSwitcher(this, preferences).apply() themeSwitcher = AndroidThemeSwitcher(this, preferences)
themeSwitcher.apply()
view = ShowHabitView(this) view = ShowHabitView(this)
@ -134,6 +136,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
presenter.present( presenter.present(
habit = habit, habit = habit,
preferences = preferences, preferences = preferences,
theme = themeSwitcher.currentTheme,
) )
) )
} }

@ -24,9 +24,12 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.LinearLayout import android.widget.LinearLayout
import org.isoron.platform.time.JavaLocalDateFormatter
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardViewModel import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardViewModel
import org.isoron.uhabits.core.ui.views.BarChart
import org.isoron.uhabits.databinding.ShowHabitBarBinding import org.isoron.uhabits.databinding.ShowHabitBarBinding
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.Locale
class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -35,11 +38,16 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
var onBoolSpinnerPosition: (position: Int) -> Unit = {} var onBoolSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: BarCardViewModel) { fun update(data: BarCardViewModel) {
binding.barChart.setEntries(data.entries)
binding.barChart.setBucketSize(data.bucketSize)
val androidColor = data.color.toThemedAndroidColor(context) val androidColor = data.color.toThemedAndroidColor(context)
binding.chart.view = BarChart(data.theme, JavaLocalDateFormatter(Locale.US)).apply {
series = mutableListOf(data.entries.map { it.value / 1000.0 })
colors = mutableListOf(theme.color(data.color.paletteIndex))
axis = data.entries.map { it.timestamp.toLocalDate() }
}
binding.chart.resetDataOffset()
binding.chart.postInvalidate()
binding.title.setTextColor(androidColor) binding.title.setTextColor(androidColor)
binding.barChart.setColor(androidColor)
if (data.isNumerical) { if (data.isNumerical) {
binding.boolSpinner.visibility = GONE binding.boolSpinner.visibility = GONE
} else { } else {

@ -22,7 +22,7 @@
android:orientation="vertical" android:layout_width="match_parent" android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.isoron.platform.gui.AndroidCanvasTestView <org.isoron.platform.gui.AndroidTestView
android:layout_width="500dp" android:layout_width="500dp"
android:layout_height="400dp" /> android:layout_height="400dp" />
</LinearLayout> </LinearLayout>

@ -52,8 +52,8 @@
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:text="@string/history" /> android:text="@string/history" />
<org.isoron.uhabits.activities.common.views.BarChart <org.isoron.platform.gui.AndroidDataView
android:id="@+id/barChart" android:id="@+id/chart"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="220dp" android:layout_height="220dp"
android:layout_below="@id/title"/> android:layout_below="@id/title"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

@ -19,6 +19,11 @@
package org.isoron.platform.gui package org.isoron.platform.gui
interface Component { interface View {
fun draw(canvas: Canvas) fun draw(canvas: Canvas)
} }
interface DataView : View {
var dataOffset: Int
val dataColumnWidth: Double
}

@ -19,6 +19,7 @@
package org.isoron.platform.time package org.isoron.platform.time
import org.isoron.uhabits.core.models.Timestamp
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
@ -32,15 +33,6 @@ enum class DayOfWeek(val index: Int) {
SATURDAY(6), 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) { data class LocalDate(val daysSince2000: Int) {
var yearCache = -1 var yearCache = -1

@ -20,9 +20,11 @@
package org.isoron.uhabits.core.models; package org.isoron.uhabits.core.models;
import org.apache.commons.lang3.builder.*; import org.apache.commons.lang3.builder.*;
import org.isoron.platform.time.LocalDate;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
import java.time.*;
import java.util.*; import java.util.*;
import kotlin.*; import kotlin.*;
@ -66,6 +68,13 @@ public final class Timestamp implements Comparable<Timestamp>
return unixTime; 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 * Returns -1 if this timestamp is older than the given timestamp, 1 if this
* timestamp is newer, or zero if they are equal. * timestamp is newer, or zero if they are equal.

@ -22,6 +22,7 @@ package org.isoron.uhabits.core.ui;
import androidx.annotation.*; import androidx.annotation.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.views.*;
public abstract class ThemeSwitcher public abstract class ThemeSwitcher
{ {
@ -59,6 +60,8 @@ public abstract class ThemeSwitcher
public abstract int getSystemTheme(); public abstract int getSystemTheme();
public abstract Theme getCurrentTheme();
public boolean isNightMode() public boolean isNightMode()
{ {
int systemTheme = getSystemTheme(); 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.SubtitleCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter 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.screens.habits.show.views.TargetCardViewModel
import org.isoron.uhabits.core.ui.views.Theme
data class ShowHabitViewModel( data class ShowHabitViewModel(
val title: String = "", val title: String = "",
@ -60,6 +61,7 @@ class ShowHabitPresenter {
fun present( fun present(
habit: Habit, habit: Habit,
preferences: Preferences, preferences: Preferences,
theme: Theme,
): ShowHabitViewModel { ): ShowHabitViewModel {
return ShowHabitViewModel( return ShowHabitViewModel(
title = habit.name, title = habit.name,
@ -100,6 +102,7 @@ class ShowHabitPresenter {
firstWeekday = preferences.firstWeekday, firstWeekday = preferences.firstWeekday,
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition, boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition, 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.Habit
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.groupedSum import org.isoron.uhabits.core.models.groupedSum
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
data class BarCardViewModel( data class BarCardViewModel(
val theme: Theme,
val boolSpinnerPosition: Int, val boolSpinnerPosition: Int,
val bucketSize: Int, val bucketSize: Int,
val color: PaletteColor, val color: PaletteColor,
@ -43,6 +45,7 @@ class BarCardPresenter {
firstWeekday: Int, firstWeekday: Int,
numericalSpinnerPosition: Int, numericalSpinnerPosition: Int,
boolSpinnerPosition: Int, boolSpinnerPosition: Int,
theme: Theme,
): BarCardViewModel { ): BarCardViewModel {
val bucketSize = if (habit.isNumerical) { val bucketSize = if (habit.isNumerical) {
numericalBucketSizes[numericalSpinnerPosition] numericalBucketSizes[numericalSpinnerPosition]
@ -57,6 +60,7 @@ class BarCardPresenter {
isNumerical = habit.isNumerical, isNumerical = habit.isNumerical,
) )
return BarCardViewModel( return BarCardViewModel(
theme = theme,
entries = entries, entries = entries,
bucketSize = bucketSize, bucketSize = bucketSize,
color = habit.color, 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.Canvas
import org.isoron.platform.gui.Color 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.gui.TextAlign
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
import org.isoron.platform.time.LocalDateFormatter import org.isoron.platform.time.LocalDateFormatter
@ -32,12 +32,13 @@ import kotlin.math.round
class BarChart( class BarChart(
var theme: Theme, var theme: Theme,
var dateFormatter: LocalDateFormatter, var dateFormatter: LocalDateFormatter,
) : Component { ) : DataView {
// Data // Data
var series = mutableListOf<List<Double>>() var series = mutableListOf<List<Double>>()
var colors = mutableListOf<Color>() var colors = mutableListOf<Color>()
var axis = listOf<LocalDate>() var axis = listOf<LocalDate>()
override var dataOffset = 0
// Style // Style
var paddingTop = 20.0 var paddingTop = 20.0
@ -50,6 +51,9 @@ class BarChart(
var nGridlines = 6 var nGridlines = 6
var backgroundColor = theme.cardBackgroundColor var backgroundColor = theme.cardBackgroundColor
override val dataColumnWidth: Double
get() = barWidth + barMargin * 2
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
val width = canvas.getWidth() val width = canvas.getWidth()
val height = canvas.getHeight() val height = canvas.getHeight()
@ -75,8 +79,11 @@ class BarChart(
barMargin barMargin
fun drawColumn(s: Int, c: Int) { fun drawColumn(s: Int, c: Int) {
val dataColumn = nColumns - c - 1 val dataColumn = nColumns - c - 1 + dataOffset
val value = if (dataColumn < series[s].size) series[s][dataColumn] else 0.0 val value = when {
dataColumn < 0 || dataColumn >= series[s].size -> 0.0
else -> series[s][dataColumn]
}
if (value <= 0) return if (value <= 0) return
val perc = value / maxValue val perc = value / maxValue
val barHeight = round(maxBarHeight * perc) val barHeight = round(maxBarHeight * perc)
@ -142,12 +149,12 @@ class BarChart(
canvas.setTextAlign(TextAlign.CENTER) canvas.setTextAlign(TextAlign.CENTER)
var prevMonth = -1 var prevMonth = -1
var prevYear = -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) { for (c in 0 until nColumns) {
val x = barGroupOffset(c) val x = barGroupOffset(c)
val dataColumn = nColumns - c - 1 val dataColumn = nColumns - c - 1 + dataOffset
if (dataColumn >= axis.size) continue if (dataColumn < 0 || dataColumn >= axis.size) continue
val date = axis[dataColumn] val date = axis[dataColumn]
if (isLargeInterval) { if (isLargeInterval) {
canvas.drawText( canvas.drawText(

@ -21,8 +21,8 @@ package org.isoron.uhabits.core.ui.views
import org.isoron.platform.gui.Canvas import org.isoron.platform.gui.Canvas
import org.isoron.platform.gui.Color import org.isoron.platform.gui.Color
import org.isoron.platform.gui.Component
import org.isoron.platform.gui.TextAlign import org.isoron.platform.gui.TextAlign
import org.isoron.platform.gui.View
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
import org.isoron.platform.time.LocalDateFormatter import org.isoron.platform.time.LocalDateFormatter
import kotlin.math.floor import kotlin.math.floor
@ -33,7 +33,7 @@ class CalendarChart(
var color: Color, var color: Color,
var theme: Theme, var theme: Theme,
var dateFormatter: LocalDateFormatter var dateFormatter: LocalDateFormatter
) : Component { ) : View {
var padding = 5.0 var padding = 5.0
var backgroundColor = Color(0xFFFFFF) 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.Canvas
import org.isoron.platform.gui.Color import org.isoron.platform.gui.Color
import org.isoron.platform.gui.Component
import org.isoron.platform.gui.Font import org.isoron.platform.gui.Font
import org.isoron.platform.gui.FontAwesome import org.isoron.platform.gui.FontAwesome
import org.isoron.platform.gui.View
class CheckmarkButton( class CheckmarkButton(
private val value: Int, private val value: Int,
private val color: Color, private val color: Color,
private val theme: Theme private val theme: Theme
) : Component { ) : View {
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
canvas.setFont(Font.FONT_AWESOME) canvas.setFont(Font.FONT_AWESOME)
canvas.setFontSize(theme.smallTextSize * 1.5) canvas.setFontSize(theme.smallTextSize * 1.5)

@ -20,8 +20,8 @@
package org.isoron.uhabits.core.ui.views package org.isoron.uhabits.core.ui.views
import org.isoron.platform.gui.Canvas import org.isoron.platform.gui.Canvas
import org.isoron.platform.gui.Component
import org.isoron.platform.gui.Font import org.isoron.platform.gui.Font
import org.isoron.platform.gui.View
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
import org.isoron.platform.time.LocalDateFormatter import org.isoron.platform.time.LocalDateFormatter
@ -30,7 +30,7 @@ class HabitListHeader(
private val nButtons: Int, private val nButtons: Int,
private val theme: Theme, private val theme: Theme,
private val fmt: LocalDateFormatter private val fmt: LocalDateFormatter
) : Component { ) : View {
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
val width = canvas.getWidth() 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.Canvas
import org.isoron.platform.gui.Color import org.isoron.platform.gui.Color
import org.isoron.platform.gui.Component
import org.isoron.platform.gui.Font import org.isoron.platform.gui.Font
import org.isoron.platform.gui.View
import java.lang.String.format import java.lang.String.format
import kotlin.math.round import kotlin.math.round
@ -52,7 +52,7 @@ class NumberButton(
val threshold: Double, val threshold: Double,
val units: String, val units: String,
val theme: Theme val theme: Theme
) : Component { ) : View {
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
val width = canvas.getWidth() 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.Canvas
import org.isoron.platform.gui.Color import org.isoron.platform.gui.Color
import org.isoron.platform.gui.Component import org.isoron.platform.gui.View
import java.lang.String.format import java.lang.String.format
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -33,7 +33,7 @@ class Ring(
val radius: Double, val radius: Double,
val theme: Theme, val theme: Theme,
val label: Boolean = false val label: Boolean = false
) : Component { ) : View {
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
val width = canvas.getWidth() val width = canvas.getWidth()

@ -22,27 +22,23 @@ package org.isoron.uhabits.core.ui.views
import org.isoron.platform.gui.Color import org.isoron.platform.gui.Color
abstract class Theme { 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) open fun color(paletteIndex: Int): Color {
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 {
return when (paletteIndex) { return when (paletteIndex) {
0 -> Color(0xD32F2F) 0 -> Color(0xD32F2F)
1 -> Color(0x512DA8) 1 -> Color(0xE64A19)
2 -> Color(0xF57C00) 2 -> Color(0xF57C00)
3 -> Color(0xFF8F00) 3 -> Color(0xFF8F00)
4 -> Color(0xF9A825) 4 -> Color(0xF9A825)
@ -58,6 +54,9 @@ abstract class Theme {
14 -> Color(0x8E24AA) 14 -> Color(0x8E24AA)
15 -> Color(0xD81B60) 15 -> Color(0xD81B60)
16 -> Color(0x5D4037) 16 -> Color(0x5D4037)
17 -> Color(0x424242)
18 -> Color(0x757575)
19 -> Color(0x9E9E9E)
else -> Color(0x000000) else -> Color(0x000000)
} }
} }
@ -68,3 +67,44 @@ abstract class Theme {
} }
class LightTheme : 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 { class JavaCanvasTest {
@Test @Test
fun run() = runBlocking { 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, width: Int,
height: Int, height: Int,
expectedPath: String, expectedPath: String,
component: Component, view: View,
) { ) {
val canvas = createCanvas(width, height) val canvas = createCanvas(width, height)
component.draw(canvas) view.draw(canvas)
assertRenders(expectedPath, canvas) assertRenders(expectedPath, canvas)
} }

@ -17,19 +17,17 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * 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 kotlinx.coroutines.runBlocking
import org.isoron.platform.gui.assertRenders import org.isoron.platform.gui.assertRenders
import org.isoron.platform.time.JavaLocalDateFormatter import org.isoron.platform.time.JavaLocalDateFormatter
import org.isoron.platform.time.LocalDate 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 org.junit.Test
import java.util.Locale import java.util.Locale
class BarChartTest { class BarChartTest {
val base = "components/BarChart" val base = "views/BarChart"
val today = LocalDate(2015, 1, 25) val today = LocalDate(2015, 1, 25)
val fmt = JavaLocalDateFormatter(Locale.US) val fmt = JavaLocalDateFormatter(Locale.US)
val theme = LightTheme() val theme = LightTheme()
@ -37,11 +35,20 @@ class BarChartTest {
val axis = (0..100).map { today.minus(it) } 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) 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 init {
fun testDraw() = runBlocking {
component.axis = axis component.axis = axis
component.series.add(series1) component.series.add(series1)
component.colors.add(theme.color(8)) component.colors.add(theme.color(8))
}
@Test
fun testDraw() = runBlocking {
assertRenders(300, 200, "$base/base.png", component) assertRenders(300, 200, "$base/base.png", component)
} }
@Test
fun testDrawWithOffset() = runBlocking {
component.dataOffset = 5
assertRenders(300, 200, "$base/offset.png", component)
}
} }

Loading…
Cancel
Save