Merge pull request #1101 from KristianTashkov/kris/implement_at_most

Implement numerical habits with AT_MOST target type
This commit is contained in:
2021-09-29 04:20:13 -05:00
committed by GitHub
29 changed files with 407 additions and 56 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -24,6 +24,7 @@ import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.utils.PaletteUtils
import org.junit.Before
import org.junit.Test
@@ -42,6 +43,7 @@ class NumberButtonViewTest : BaseViewTest() {
super.setUp()
view = component.getNumberButtonViewFactory().create().apply {
units = "steps"
targetType = NumericalHabitType.AT_LEAST
threshold = 100.0
color = PaletteUtils.getAndroidTestColor(8)
onEdit = { edited = true }
@@ -74,10 +76,10 @@ class NumberButtonViewTest : BaseViewTest() {
}
@Test
fun testRender_emptyUnits() {
fun testRender_atMostAboveThreshold() {
view.value = 500.0
view.units = ""
assertRenders(view, "$PATH/render_unitless.png")
view.targetType = NumericalHabitType.AT_MOST
assertRenders(view, "$PATH/render_at_most_above.png")
}
@Test
@@ -86,12 +88,33 @@ class NumberButtonViewTest : BaseViewTest() {
assertRenders(view, "$PATH/render_below.png")
}
@Test
fun testRender_atMostBetweenThresholds() {
view.value = 110.0
view.targetType = NumericalHabitType.AT_MOST
assertRenders(view, "$PATH/render_at_most_between.png")
}
@Test
fun testRender_zero() {
view.value = 0.0
assertRenders(view, "$PATH/render_zero.png")
}
@Test
fun testRender_atMostBelowThreshold() {
view.value = 0.0
view.targetType = NumericalHabitType.AT_MOST
assertRenders(view, "$PATH/render_at_most_below.png")
}
@Test
fun testRender_emptyUnits() {
view.value = 500.0
view.units = ""
assertRenders(view, "$PATH/render_unitless.png")
}
@Test
fun testClick_shortToggleDisabled() {
prefs.isShortToggleEnabled = false

View File

@@ -24,6 +24,7 @@ import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.utils.PaletteUtils
import org.junit.After
@@ -55,6 +56,7 @@ class NumberPanelViewTest : BaseViewTest() {
buttonCount = 4
color = PaletteUtils.getAndroidTestColor(7)
units = "steps"
targetType = NumericalHabitType.AT_LEAST
threshold = 5000.0
}
view.onAttachedToWindow()

View File

@@ -53,8 +53,6 @@ class SubtitleCardViewTest : BaseViewTest() {
isNumerical = false,
question = "Did you meditate this morning?",
reminder = Reminder(8, 30, EVERY_DAY),
unit = "",
targetValue = 0.0,
theme = LightTheme(),
)
)

View File

@@ -62,6 +62,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
firstWeekday = preferences.firstWeekday,
paletteColor = habit.color,
series = emptyList(),
defaultSquare = HistoryChart.Square.OFF,
theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
@@ -101,6 +102,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
theme = LightTheme()
)
chart?.series = model.series
chart?.defaultSquare = model.defaultSquare
dataView.postInvalidate()
}

View File

@@ -88,6 +88,7 @@ class EditHabitActivity : AppCompatActivity() {
var reminderHour = -1
var reminderMin = -1
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
var targetType = NumericalHabitType.AT_LEAST
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -107,6 +108,7 @@ class EditHabitActivity : AppCompatActivity() {
color = habit.color
freqNum = habit.frequency.numerator
freqDen = habit.frequency.denominator
targetType = habit.targetType
habit.reminder?.let {
reminderHour = it.hour
reminderMin = it.minute
@@ -138,6 +140,7 @@ class EditHabitActivity : AppCompatActivity() {
HabitType.YES_NO -> {
binding.unitOuterBox.visibility = View.GONE
binding.targetOuterBox.visibility = View.GONE
binding.targetTypeOuterBox.visibility = View.GONE
}
HabitType.NUMERICAL -> {
binding.nameInput.hint = getString(R.string.measurable_short_example)
@@ -172,6 +175,23 @@ class EditHabitActivity : AppCompatActivity() {
dialog.show(supportFragmentManager, "frequencyPicker")
}
populateTargetType()
binding.targetTypePicker.setOnClickListener {
val builder = AlertDialog.Builder(this)
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
arrayAdapter.add(getString(R.string.target_type_at_least))
arrayAdapter.add(getString(R.string.target_type_at_most))
builder.setAdapter(arrayAdapter) { dialog, which ->
targetType = when (which) {
0 -> NumericalHabitType.AT_LEAST
else -> NumericalHabitType.AT_MOST
}
populateTargetType()
dialog.dismiss()
}
builder.show()
}
binding.numericalFrequencyPicker.setOnClickListener {
val builder = AlertDialog.Builder(this)
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
@@ -262,7 +282,7 @@ class EditHabitActivity : AppCompatActivity() {
habit.frequency = Frequency(freqNum, freqDen)
if (habitType == HabitType.NUMERICAL) {
habit.targetValue = targetInput.text.toString().toDouble()
habit.targetType = NumericalHabitType.AT_LEAST
habit.targetType = targetType
habit.unit = unitInput.text.trim().toString()
}
habit.type = habitType
@@ -324,6 +344,13 @@ class EditHabitActivity : AppCompatActivity() {
}
}
private fun populateTargetType() {
binding.targetTypePicker.text = when (targetType) {
NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most)
else -> getString(R.string.target_type_at_least)
}
}
private fun updateColors() {
androidColor = themeSwitcher.currentTheme.color(color).toInt()
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)

View File

@@ -236,6 +236,7 @@ class HabitCardView(
numberPanel.apply {
color = c
units = h.unit
targetType = h.targetType
threshold = h.targetValue
visibility = when (h.isNumerical) {
true -> View.VISIBLE

View File

@@ -29,13 +29,15 @@ import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres
import java.lang.Double.max
import java.text.DecimalFormat
import javax.inject.Inject
@@ -88,6 +90,12 @@ class NumberButtonView(
invalidate()
}
var targetType = NumericalHabitType.AT_LEAST
set(value) {
field = value
invalidate()
}
var units = ""
set(value) {
field = value
@@ -127,7 +135,6 @@ class NumberButtonView(
private val em: Float
private val rect: RectF = RectF()
private val sr = StyledResources(context)
private val lowContrast: Int
private val mediumContrast: Int
@@ -148,15 +155,23 @@ class NumberButtonView(
init {
em = pNumber.measureText("m")
lowContrast = sr.getColor(R.attr.contrast40)
mediumContrast = sr.getColor(R.attr.contrast60)
lowContrast = sres.getColor(R.attr.contrast40)
mediumContrast = sres.getColor(R.attr.contrast60)
}
fun draw(canvas: Canvas) {
val activeColor = when {
value <= 0.0 -> lowContrast
value < threshold -> mediumContrast
else -> color
var activeColor = if (targetType == NumericalHabitType.AT_LEAST) {
when {
value < 0.0 && preferences.areQuestionMarksEnabled -> lowContrast
max(0.0, value) >= threshold -> color
else -> mediumContrast
}
} else {
when {
value < 0.0 && preferences.areQuestionMarksEnabled -> lowContrast
value <= threshold -> color
else -> mediumContrast
}
}
val label: String

View File

@@ -20,6 +20,7 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.Context
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils
@@ -47,6 +48,12 @@ class NumberPanelView(
setupButtons()
}
var targetType = NumericalHabitType.AT_LEAST
set(value) {
field = value
setupButtons()
}
var threshold = 0.0
set(value) {
field = value
@@ -84,6 +91,7 @@ class NumberPanelView(
else -> 0.0
}
button.color = color
button.targetType = targetType
button.threshold = threshold
button.units = units
button.onEdit = { onEdit(timestamp) }

View File

@@ -43,6 +43,7 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
theme = state.theme,
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
series = state.series,
defaultSquare = state.defaultSquare,
firstWeekday = state.firstWeekday,
)
binding.chart.postInvalidate()

View File

@@ -28,6 +28,7 @@ import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.edit.formatFrequency
import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
import org.isoron.uhabits.utils.InterfaceUtils
@@ -65,7 +66,12 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
binding.questionLabel.visibility = View.VISIBLE
binding.targetIcon.visibility = View.VISIBLE
binding.targetText.visibility = View.VISIBLE
if (!state.isNumerical) {
if (state.isNumerical) {
binding.targetIcon.text = when (state.targetType) {
NumericalHabitType.AT_LEAST -> resources.getString(R.string.fa_arrow_circle_up)
else -> resources.getString(R.string.fa_arrow_circle_down)
}
} else {
binding.targetIcon.visibility = View.GONE
binding.targetText.visibility = View.GONE
}

View File

@@ -56,7 +56,9 @@ class HistoryWidget(
theme = WidgetTheme(),
)
(widgetView.dataView as AndroidDataView).apply {
(this.view as HistoryChart).series = model.series
val historyChart = (this.view as HistoryChart)
historyChart.series = model.series
historyChart.defaultSquare = model.defaultSquare
}
}
@@ -71,6 +73,7 @@ class HistoryWidget(
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
firstWeekday = prefs.firstWeekday,
series = listOf(),
defaultSquare = HistoryChart.Square.OFF,
)
}
).apply {

View File

@@ -167,6 +167,7 @@
android:hint="@string/measurable_units_example"/>
</LinearLayout>
</FrameLayout>
<LinearLayout
android:id="@+id/targetOuterBox"
android:layout_width="match_parent"
@@ -207,6 +208,22 @@
</FrameLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/targetTypeOuterBox"
style="@style/FormOuterBox">
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/target_type" />
<TextView
style="@style/FormDropdown"
android:id="@+id/targetTypePicker"
android:textColor="?attr/contrast100"
/>
</LinearLayout>
</FrameLayout>
<!-- Reminder -->
<FrameLayout style="@style/FormOuterBox">
<LinearLayout style="@style/FormInnerBox">

View File

@@ -47,7 +47,6 @@
android:id="@+id/targetIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fa_arrow_circle_up"
android:textColor="?attr/contrast60"
android:textSize="16sp" />

View File

@@ -21,6 +21,7 @@
<resources>
<string translatable="false" name="fa_star_half_o">&#xf5c0;</string>
<string translatable="false" name="fa_arrow_circle_up">&#xf0aa;</string>
<string translatable="false" name="fa_arrow_circle_down">&#xf0ab;</string>
<string translatable="false" name="fa_check">&#xf00c;</string>
<string translatable="false" name="fa_times">&#xf00d;</string>
<string translatable="false" name="fa_skipped">&#xf068;</string>
@@ -181,7 +182,6 @@
<!--<string translatable="false" name="fa_hand_o_down">&#xf0a7;</string>-->
<!--<string translatable="false" name="fa_arrow_circle_left">&#xf0a8;</string>-->
<!--<string translatable="false" name="fa_arrow_circle_right">&#xf0a9;</string>-->
<!--<string translatable="false" name="fa_arrow_circle_down">&#xf0ab;</string>-->
<!--<string translatable="false" name="fa_globe">&#xf0ac;</string>-->
<!--<string translatable="false" name="fa_wrench">&#xf0ad;</string>-->
<!--<string translatable="false" name="fa_tasks">&#xf0ae;</string>-->

View File

@@ -184,6 +184,9 @@
<string name="change_value">Change value</string>
<string name="calendar">Calendar</string>
<string name="unit">Unit</string>
<string name="target_type">Target Type</string>
<string name="target_type_at_least">At least</string>
<string name="target_type_at_most">At most</string>
<string name="example_question_boolean">e.g. Did you exercise today?</string>
<string name="question">Question</string>
<string name="target">Target</string>