Replace BarChart by new Kotlin implementation
@@ -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.Typeface
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||
|
||||
class AndroidCanvas : Canvas {
|
||||
@@ -107,7 +105,7 @@ class AndroidCanvas : Canvas {
|
||||
}
|
||||
|
||||
override fun setFontSize(size: Double) {
|
||||
textPaint.textSize = size.toDp() * 1.07f
|
||||
textPaint.textSize = size.toDp()
|
||||
}
|
||||
|
||||
override fun setStrokeWidth(size: Double) {
|
||||
@@ -156,14 +154,3 @@ class AndroidCanvas : Canvas {
|
||||
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.core.preferences.Preferences
|
||||
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.ActivityScope
|
||||
|
||||
@@ -35,9 +38,11 @@ import org.isoron.uhabits.inject.ActivityScope
|
||||
class AndroidThemeSwitcher
|
||||
constructor(
|
||||
@ActivityContext val context: Context,
|
||||
preferences: Preferences
|
||||
preferences: Preferences,
|
||||
) : ThemeSwitcher(preferences) {
|
||||
|
||||
private var currentTheme: Theme = LightTheme()
|
||||
|
||||
override fun getSystemTheme(): Int {
|
||||
if (SDK_INT < 29) return THEME_LIGHT
|
||||
val uiMode = context.resources.configuration.uiMode
|
||||
@@ -48,17 +53,24 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentTheme(): Theme {
|
||||
return currentTheme
|
||||
}
|
||||
|
||||
override fun applyDarkTheme() {
|
||||
currentTheme = DarkTheme()
|
||||
context.setTheme(R.style.AppBaseThemeDark)
|
||||
(context as Activity).window.navigationBarColor =
|
||||
ContextCompat.getColor(context, R.color.grey_900)
|
||||
}
|
||||
|
||||
override fun applyLightTheme() {
|
||||
currentTheme = LightTheme()
|
||||
context.setTheme(R.style.AppBaseTheme)
|
||||
}
|
||||
|
||||
override fun applyPureBlackTheme() {
|
||||
currentTheme = DarkTheme()
|
||||
context.setTheme(R.style.AppBaseThemeDark_PureBlack)
|
||||
(context as Activity).window.navigationBarColor =
|
||||
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 {
|
||||
|
||||
private val presenter = ShowHabitPresenter()
|
||||
val presenter = ShowHabitPresenter()
|
||||
|
||||
private lateinit var commandRunner: CommandRunner
|
||||
private lateinit var menu: ShowHabitMenu
|
||||
private lateinit var view: ShowHabitView
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var preferences: Preferences
|
||||
private lateinit var themeSwitcher: AndroidThemeSwitcher
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
@@ -61,7 +62,8 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
habit = habitList.getById(ContentUris.parseId(intent.data!!))!!
|
||||
preferences = appComponent.preferences
|
||||
commandRunner = appComponent.commandRunner
|
||||
AndroidThemeSwitcher(this, preferences).apply()
|
||||
themeSwitcher = AndroidThemeSwitcher(this, preferences)
|
||||
themeSwitcher.apply()
|
||||
|
||||
view = ShowHabitView(this)
|
||||
|
||||
@@ -134,6 +136,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
presenter.present(
|
||||
habit = habit,
|
||||
preferences = preferences,
|
||||
theme = themeSwitcher.currentTheme,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,12 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
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.views.BarChart
|
||||
import org.isoron.uhabits.databinding.ShowHabitBarBinding
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
import java.util.Locale
|
||||
|
||||
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 = {}
|
||||
|
||||
fun update(data: BarCardViewModel) {
|
||||
binding.barChart.setEntries(data.entries)
|
||||
binding.barChart.setBucketSize(data.bucketSize)
|
||||
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.barChart.setColor(androidColor)
|
||||
if (data.isNumerical) {
|
||||
binding.boolSpinner.visibility = GONE
|
||||
} else {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.isoron.platform.gui.AndroidCanvasTestView
|
||||
<org.isoron.platform.gui.AndroidTestView
|
||||
android:layout_width="500dp"
|
||||
android:layout_height="400dp" />
|
||||
</LinearLayout>
|
||||
@@ -52,8 +52,8 @@
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/history" />
|
||||
|
||||
<org.isoron.uhabits.activities.common.views.BarChart
|
||||
android:id="@+id/barChart"
|
||||
<org.isoron.platform.gui.AndroidDataView
|
||||
android:id="@+id/chart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="220dp"
|
||||
android:layout_below="@id/title"/>
|
||||
|
||||
|
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)
|
||||
}
|
||||
}
|
||||
|
||||