Fade color from green to red based on how long until chain times out. Only works for 1 per X days habits

pull/600/head
NegatioN 5 years ago
parent 9f2e4a8341
commit b1a7b78d76

@ -21,15 +21,17 @@ package org.isoron.androidbase.utils;
import android.graphics.*; import android.graphics.*;
import static java.lang.Math.abs;
public abstract class ColorUtils public abstract class ColorUtils
{ {
final static byte ALPHA_CHANNEL = 24;
final static byte RED_CHANNEL = 16;
final static byte GREEN_CHANNEL = 8;
final static byte BLUE_CHANNEL = 0;
public static int mixColors(int color1, int color2, float amount) public static int mixColors(int color1, int color2, float amount)
{ {
final byte ALPHA_CHANNEL = 24;
final byte RED_CHANNEL = 16;
final byte GREEN_CHANNEL = 8;
final byte BLUE_CHANNEL = 0;
final float inverseAmount = 1.0f - amount; final float inverseAmount = 1.0f - amount;
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) + int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
@ -48,6 +50,18 @@ public abstract class ColorUtils
b << BLUE_CHANNEL; b << BLUE_CHANNEL;
} }
public static int interPolateHSV(int col1, int col2, float factor){
//based on https://www.alanzucconi.com/2016/01/06/colour-interpolation/2/
float[] a = new float[3];
float[] b = new float[3];
Color.colorToHSV(col1, a);
Color.colorToHSV(col2, b);
float[] out = new float[3];
androidx.core.graphics.ColorUtils.blendHSL(a, b, factor, out);
return Color.HSVToColor(out);
}
public static int setAlpha(int color, float newAlpha) public static int setAlpha(int color, float newAlpha)
{ {
int intAlpha = (int) (newAlpha * 255); int intAlpha = (int) (newAlpha * 255);

@ -22,9 +22,9 @@ package org.isoron.uhabits.widgets
import android.content.* import android.content.*
import android.view.* import android.view.*
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.widgets.views.* import org.isoron.uhabits.widgets.views.*
class CurrentStreakWidget( class CurrentStreakWidget(
context: Context, context: Context,
widgetId: Int, widgetId: Int,
@ -41,11 +41,13 @@ class CurrentStreakWidget(
} else { } else {
"0" "0"
} }
val timeoutPercent = habit.timeoutPercentage()
(v as CurrentStreakWidgetView).apply { (v as CurrentStreakWidgetView).apply {
setBackgroundAlpha(preferedBackgroundAlpha) setBackgroundAlpha(preferedBackgroundAlpha)
setCurrentStreak(numReps) setCurrentStreak(numReps)
setPercentage(habit.scores.todayValue.toFloat()) setPercentage(habit.scores.todayValue.toFloat())
setActiveColor(PaletteUtils.getColor(context, habit.color)) setTimeOutPercentage(1 - timeoutPercent)
setName(habit.name) setName(habit.name)
setCheckmarkValue(habit.checkmarks.todayValue) setCheckmarkValue(habit.checkmarks.todayValue)
refresh() refresh()

@ -27,18 +27,16 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.isoron.androidbase.utils.ColorUtils;
import org.isoron.androidbase.utils.StyledResources; import org.isoron.androidbase.utils.StyledResources;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.RingView; import org.isoron.uhabits.activities.common.views.RingView;
import org.isoron.uhabits.core.models.Checkmark; import org.isoron.uhabits.core.models.Checkmark;
import org.isoron.uhabits.utils.PaletteUtils;
import static org.isoron.androidbase.utils.InterfaceUtils.getDimension; import static org.isoron.androidbase.utils.InterfaceUtils.getDimension;
public class CurrentStreakWidgetView extends HabitWidgetView public class CurrentStreakWidgetView extends HabitWidgetView
{ {
private int activeColor;
private float percentage; private float percentage;
@Nullable @Nullable
@ -49,7 +47,10 @@ public class CurrentStreakWidgetView extends HabitWidgetView
private TextView label; private TextView label;
private int checkmarkValue; private int checkmarkValue;
private int startColor = 0x388e3c + (255 << 24); // add full alpha
private int endColor = 0xc62828 + (255 << 24); // add full alpha
private String currentStreak; private String currentStreak;
private float timeoutPercentage;
public CurrentStreakWidgetView(Context context) public CurrentStreakWidgetView(Context context)
{ {
@ -69,34 +70,30 @@ public class CurrentStreakWidgetView extends HabitWidgetView
StyledResources res = new StyledResources(getContext()); StyledResources res = new StyledResources(getContext());
String text;
int bgColor; int bgColor;
int fgColor; int fgColor;
bgColor = ColorUtils.interPolateHSV(startColor, endColor, timeoutPercentage);
switch (checkmarkValue) switch (checkmarkValue)
{ {
case Checkmark.CHECKED_EXPLICITLY: case Checkmark.CHECKED_EXPLICITLY:
bgColor = activeColor;
fgColor = res.getColor(R.attr.highContrastReverseTextColor); fgColor = res.getColor(R.attr.highContrastReverseTextColor);
setShadowAlpha(0x4f); setShadowAlpha(0x4f);
backgroundPaint.setColor(bgColor);
frame.setBackgroundDrawable(background);
break; break;
case Checkmark.CHECKED_IMPLICITLY: case Checkmark.CHECKED_IMPLICITLY:
case Checkmark.UNCHECKED: case Checkmark.UNCHECKED:
default: default:
bgColor = res.getColor(R.attr.cardBgColor);
fgColor = res.getColor(R.attr.mediumContrastTextColor); fgColor = res.getColor(R.attr.mediumContrastTextColor);
setShadowAlpha(0x00);
break; break;
} }
text = currentStreak;
backgroundPaint.setColor(bgColor);
frame.setBackgroundDrawable(background);
ring.setPercentage(percentage); ring.setPercentage(percentage);
ring.setColor(fgColor); ring.setColor(fgColor);
ring.setBackgroundColor(bgColor); ring.setBackgroundColor(bgColor);
ring.setText(text); ring.setText(currentStreak);
label.setText(name); label.setText(name);
label.setTextColor(fgColor); label.setTextColor(fgColor);
@ -105,10 +102,6 @@ public class CurrentStreakWidgetView extends HabitWidgetView
postInvalidate(); postInvalidate();
} }
public void setActiveColor(int activeColor)
{
this.activeColor = activeColor;
}
public void setCurrentStreak(String currentStreak) public void setCurrentStreak(String currentStreak)
{ {
this.currentStreak = currentStreak; this.currentStreak = currentStreak;
@ -128,6 +121,10 @@ public class CurrentStreakWidgetView extends HabitWidgetView
{ {
this.percentage = percentage; this.percentage = percentage;
} }
public void setTimeOutPercentage(float percentage)
{
this.timeoutPercentage = percentage;
}
@Override @Override
@NonNull @NonNull
@ -181,7 +178,6 @@ public class CurrentStreakWidgetView extends HabitWidgetView
{ {
percentage = 0.75f; percentage = 0.75f;
name = "Wake up early"; name = "Wake up early";
activeColor = PaletteUtils.getAndroidTestColor(6);
checkmarkValue = Checkmark.CHECKED_EXPLICITLY; checkmarkValue = Checkmark.CHECKED_EXPLICITLY;
refresh(); refresh();
} }

@ -22,12 +22,15 @@ package org.isoron.uhabits.core.models;
import androidx.annotation.*; import androidx.annotation.*;
import org.apache.commons.lang3.builder.*; import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.DateUtils;
import java.util.*; import java.util.*;
import javax.annotation.concurrent.*; import javax.annotation.concurrent.*;
import javax.inject.*; import javax.inject.*;
import autovalue.shaded.com.google$.common.base.$Function;
import static org.isoron.uhabits.core.models.Checkmark.*; import static org.isoron.uhabits.core.models.Checkmark.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle; import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
@ -334,6 +337,76 @@ public class Habit
else return (todayCheckmark != UNCHECKED); else return (todayCheckmark != UNCHECKED);
} }
private boolean activeValue(int value) {
return value == Checkmark.CHECKED_EXPLICITLY || value == Checkmark.CHECKED_IMPLICITLY;
}
public boolean isActive() {
CheckmarkList cm = getCheckmarks();
int[] values = cm.getAllValues();
if (values.length == 0 || isArchived()){
return false;
}
return activeValue(values[0]);
}
public float timeoutPercentage(){
Timestamp now = DateUtils.getToday();
CheckmarkList cm = getCheckmarks();
int[] values = cm.getAllValues();
if (values.length == 0) {
return 0f;
} else if (!activeValue(values[0]) && !activeValue(cm.getTodayValue())) {
return 0f;
} else if (cm.getTodayValue() == CHECKED_EXPLICITLY) {
return 1f;
}
int numDaysLimit = getData().frequency.getDenominator();
//int numTimesLimit = getData().frequency.getNumerator(); // we assume this is 1, and dont handle multiple now
Timestamp oldest = getRepetitions().getOldest().getTimestamp(); //shouldnt null if we have checkmarks
Timestamp potentialNewest = DateUtils.getToday().plus(numDaysLimit);
//TODO speedup by not getting ALL checkmarks?
int[] vals = getCheckmarks().getValues(oldest, potentialNewest);
//System.out.println(Arrays.toString(vals));
int daysSinceStreakStart = 0;
boolean isActiveSegment = false;
for (int c : vals){
if (c != UNCHECKED && !isActiveSegment) isActiveSegment = true;
if (isActiveSegment){
if (c == CHECKED_IMPLICITLY || c == UNCHECKED) {
daysSinceStreakStart += 1;
} else {
break;
}
}
}
int streakDaysRemain = numDaysLimit - daysSinceStreakStart;
//TODO can this fail if we completed the streak TOO often the last few days?
//TODO we handle this differently if numerator is higher than 1?
//TODO just dont handle multicase?
//TODO Need to change CheckmarkList.getValues(from, to) to fix this?
Timestamp trueEnd = now.plus(streakDaysRemain + 1); // a streak will always be terminated on the evening of that day?
Timestamp trueStart = now.minus(daysSinceStreakStart);
long nowDiff = now.getUnixTime() - trueStart.getUnixTime();
long endDiff = trueEnd.getUnixTime() - trueStart.getUnixTime();
//System.out.println("n: " + nowDiff + " e: " + endDiff + " start: " + daysSinceStreakStart + " remain: " + (streakDaysRemain+1));
return 1f - ((float) nowDiff / endDiff);
}
public synchronized boolean isNumerical() public synchronized boolean isNumerical()
{ {
return data.type == NUMBER_HABIT; return data.type == NUMBER_HABIT;

@ -19,6 +19,7 @@
package org.isoron.uhabits.core.models; package org.isoron.uhabits.core.models;
import org.hamcrest.number.IsCloseTo;
import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.*;
import org.junit.*; import org.junit.*;
import org.junit.rules.*; import org.junit.rules.*;
@ -26,6 +27,7 @@ import org.junit.rules.*;
import nl.jqno.equalsverifier.*; import nl.jqno.equalsverifier.*;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.closeTo;
import static org.isoron.uhabits.core.utils.DateUtils.*; import static org.isoron.uhabits.core.utils.DateUtils.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -53,6 +55,96 @@ public class HabitTest extends BaseUnitTest
assertNotNull(habit.getCheckmarks()); assertNotNull(habit.getCheckmarks());
} }
@Test
public void testActiveState_archived()
{
Habit model = modelFactory.buildHabit();
model.setArchived(true);
model.getRepetitions().toggle(getToday());
assertFalse(model.isActive());
}
@Test
public void testActiveState()
{
Habit model = modelFactory.buildHabit();
assertFalse(model.isActive());
model.getRepetitions().toggle(getToday());
assertTrue(model.isActive());
}
@Test
public void testTimeoutPercentage()
{
Habit model = modelFactory.buildHabit();
assertThat(model.timeoutPercentage(), is(0f));
model.getRepetitions().toggle(getToday());
assertThat(model.timeoutPercentage(), is(1f));
}
@Test
public void testTimeoutPercentage_once_per_period()
{
Habit model = modelFactory.buildHabit();
Timestamp day = getToday().minus(2);
model.getRepetitions().toggle(day);
model.setFrequency(new Frequency(1, 3));
assertThat((double) model.timeoutPercentage(), is(closeTo(0.25f, 0.01f)));
model.setFrequency(new Frequency(1, 5));
assertThat((double) model.timeoutPercentage(), is(closeTo(0.5f, 0.01f)));
}
@Test
public void testTimeoutPercentage_several_hits()
{
Habit model = modelFactory.buildHabit();
Timestamp day = getToday().minus(2);
model.getRepetitions().toggle(day);
model.getRepetitions().toggle(day.minus(2));
model.setFrequency(new Frequency(1, 3));
assertThat((double) model.timeoutPercentage(), is(closeTo(0.25f, 0.01f)));
}
@Test
public void testTimeoutPercentage_every_day()
{
Habit model = modelFactory.buildHabit();
model.setFrequency(new Frequency(1, 1));
Timestamp day = getToday().minus(1);
model.getRepetitions().toggle(day);
assertThat((double) model.timeoutPercentage(), is(closeTo(0.5f, 0.01f)));
}
@Test
@Ignore
public void testTimeoutPercentage_multiple_per_period()
{
Habit model = modelFactory.buildHabit();
model.setFrequency(new Frequency(2, 5));
Timestamp day = getToday().minus(2);
model.getRepetitions().toggle(day);
model.getRepetitions().toggle(day.minus(2));
// if we have 2 triggered, this validates 5 days, starting at first day
// But, current code doesn't handle telling us when the last day we can still chain.
//TODO Need to change CheckmarkList.getValues(from, to) to fix this?
assertThat((double) model.timeoutPercentage(), is(closeTo(0.25f, 0.01f)));
model.setFrequency(new Frequency(1, 5));
assertThat((double) model.timeoutPercentage(), is(closeTo(0.5f, 0.01f)));
}
@Test @Test
public void test_copyAttributes() public void test_copyAttributes()
{ {

Loading…
Cancel
Save