1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-22 10:52:13 +00:00

feat: Change inner work to allow more depictions of datas

This commit is contained in:
Florian Bouillon 2023-01-09 17:32:28 +01:00
parent be055fd1be
commit e3313a65a8
14 changed files with 362 additions and 86 deletions

View File

@ -29,7 +29,7 @@ Permissions requests are for specifics usage and are only requests the first tim
| Permission | Why is it requested | | Permission | Why is it requested |
|:--------------------:|:-----------------------------------------------------------| |:--------------------:|:-----------------------------------------------------------|
| ACTIVITY_RECOGNITION | Device Steps Usage | | ACTIVITY_RECOGNITION | Device Steps Usage |
| INTERNET | Food fetching from OpenFoodFact | | INTERNET | Food fetching from OpenFoodFact |
| POST_NOTIFICATIONS | send notifications for water intake and device steps usage | | POST_NOTIFICATIONS | send notifications for water intake and device steps usage |
No other permissions are used. No other permissions are used.

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewFilledStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="16dp">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:paddingVertical="8dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_outline_monitor_weight_24"
android:background="@drawable/shape_circle"
app:tint="?colorOnPrimary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="4dp"
android:text="History" />
</LinearLayout>
</LinearLayout>
<com.dzeio.charts.ChartView
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="200dp"
android:minHeight="200dp" />
<Button
android:id="@+id/goal_button"
android:text="@string/add_goal"
style="?materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="16dp"
android:layout_marginVertical="16dp"
/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/debug_random_values"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Generate random values" />
<androidx.recyclerview.widget.RecyclerView
android:clipToPadding="false"
android:id="@+id/list"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/layout_item_list"
tools:context=".ui.weight.ListWeightFragment" />
</LinearLayout>

View File

@ -88,9 +88,7 @@ class StepsHomeFragment :
} }
xAxis.apply { xAxis.apply {
increment = 86400000.0 dataWidth = 604800000.0
// displayCount = 168
// displayCount = 10
textPaint.color = MaterialColors.getColor( textPaint.color = MaterialColors.getColor(
requireView(), requireView(),
com.google.android.material.R.attr.colorOnPrimaryContainer com.google.android.material.R.attr.colorOnPrimaryContainer

View File

@ -10,15 +10,18 @@ import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.charts.Entry
import com.dzeio.charts.series.LineSerie
import com.dzeio.openhealth.R import com.dzeio.openhealth.R
import com.dzeio.openhealth.adapters.WeightAdapter import com.dzeio.openhealth.adapters.WeightAdapter
import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.data.weight.Weight
import com.dzeio.openhealth.databinding.FragmentListWeightBinding import com.dzeio.openhealth.databinding.FragmentListWeightBinding
import com.dzeio.openhealth.graphs.WeightChart
import com.dzeio.openhealth.utils.GraphUtils
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.text.DateFormat
import java.util.Date
import java.util.Locale
@AndroidEntryPoint @AndroidEntryPoint
class ListWeightFragment : class ListWeightFragment :
@ -57,7 +60,7 @@ class ListWeightFragment :
} }
} }
val recycler = binding.list.apply { binding.list.apply {
val manager = LinearLayoutManager(requireContext()) val manager = LinearLayoutManager(requireContext())
layoutManager = manager layoutManager = manager
this.adapter = adapter this.adapter = adapter
@ -65,7 +68,6 @@ class ListWeightFragment :
viewModel.massUnit.observe(viewLifecycleOwner) { viewModel.massUnit.observe(viewLifecycleOwner) {
adapter.unit = it adapter.unit = it
// adapter.notifyDataSetChanged()
} }
viewModel.weights.observe(viewLifecycleOwner) { viewModel.weights.observe(viewLifecycleOwner) {
@ -77,28 +79,88 @@ class ListWeightFragment :
} }
} }
GraphUtils.lineChartSetup( val chart = binding.chart
binding.chart,
MaterialColors.getColor( val serie = LineSerie(chart).apply {
linePaint.color = MaterialColors.getColor(
requireView(), requireView(),
com.google.android.material.R.attr.colorPrimary com.google.android.material.R.attr.colorPrimary
),
MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorOnBackground
) )
) textPaint.color = MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorOnPrimary
)
}
chart.apply {
series = arrayListOf(serie)
yAxis.apply {
textLabel.color = MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorOnPrimaryContainer
)
linePaint.color = MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorOnPrimaryContainer
)
onValueFormat = { value -> "${value.toInt()}" }
}
xAxis.apply {
// 7 day history
// increment = (7 * 24 * 60 * 60 * 1000).toDouble()
textPaint.color = MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorOnPrimaryContainer
)
textPaint.textSize = 32f
onValueFormat = onValueFormat@{
val formatter = DateFormat.getDateTimeInstance(
DateFormat.SHORT,
DateFormat.SHORT,
Locale.getDefault()
)
return@onValueFormat formatter.format(Date(it.toLong()))
}
}
}
// Debug button
if (binding.debugRandomValues != null) {
binding.debugRandomValues.setOnClickListener {
viewModel.generateRandomValues()
}
binding.debugRandomValues.setOnLongClickListener {
viewModel.delete(viewModel.weights.value!!)
return@setOnLongClickListener true
}
}
} }
private fun updateGraph(list: List<Weight>) { private fun updateGraph(list: List<Weight>) {
WeightChart.setup( val chart = binding.chart
binding.chart, val serie = chart.series[0] as LineSerie
requireView(),
list, val entries: ArrayList<Entry> = arrayListOf()
viewModel.massUnit.value!!,
viewModel.goalWeight.value, list.forEach {
false entries.add(Entry(
) it.timestamp.toDouble(),
it.weight
))
}
serie.entries = entries
if (list.isEmpty()) {
chart.xAxis.x = 0.0
} else {
chart.xAxis.x = list[0].timestamp.toDouble()
}
chart.refresh()
} }
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")

View File

@ -13,6 +13,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random
@HiltViewModel @HiltViewModel
class ListWeightViewModel @Inject internal constructor( class ListWeightViewModel @Inject internal constructor(
@ -48,4 +49,20 @@ class ListWeightViewModel @Inject internal constructor(
} }
} }
} }
fun generateRandomValues(): Unit {
viewModelScope.launch {
weightRepository.addWeight(
Weight(
weight = Random.nextInt(0, 100).toFloat()
)
)
}
}
fun delete(list: List<Weight>) {
viewModelScope.launch {
for (item in list) weightRepository.deleteWeight(item)
}
}
} }

View File

@ -91,7 +91,7 @@ object Units {
); );
fun formatToString(value: Int): String { fun formatToString(value: Int): String {
return String.format("%d", value * modifier) return String.format("%.0f", (value * modifier))
} }
companion object { companion object {

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="health_permissions">
<item>androidx.health.permission.HeartRate.READ</item>
<item>androidx.health.permission.HeartRate.WRITE</item>
<item>androidx.health.permission.Steps.READ</item>
<item>androidx.health.permission.Steps.WRITE</item>
</array>
</resources>

View File

@ -34,9 +34,7 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
private val scroller = ChartScroll(this).apply { private val scroller = ChartScroll(this).apply {
var lastMovement = 0.0 var lastMovement = 0.0
setOnChartMoved { movementX, _ -> setOnChartMoved { movementX, _ ->
xAxis.x += (movementX - lastMovement) * xAxis.getDataWidth() / width
// Log.d(TAG, "scrolled: ${(movementX - lastMovement) * (xAxis.increment / 10)}")
xAxis.x = xAxis.x + (movementX - lastMovement) * (xAxis.increment * xAxis.displayCount / width)
lastMovement = movementX.toDouble() lastMovement = movementX.toDouble()
refresh() refresh()
} }
@ -140,4 +138,13 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
performClick() performClick()
return scroller.onTouchEvent(event) return scroller.onTouchEvent(event)
} }
override fun getDataset(): ArrayList<Entry> {
val data: ArrayList<Entry> = arrayListOf()
for (serie in series) {
data.addAll(serie.entries)
}
data.sortBy { it.x }
return data.filterIndexed { index, entry -> data.indexOf(entry) == index } as ArrayList<Entry>
}
} }

View File

@ -39,4 +39,9 @@ interface ChartViewInterface {
* this function should be run if you change parameters in the view * this function should be run if you change parameters in the view
*/ */
fun refresh() fun refresh()
/**
* @return the whole dataset (sorted and cleaned up of dupps)
*/
fun getDataset(): ArrayList<Entry>
} }

View File

@ -13,7 +13,7 @@ class XAxis(
) : XAxisInterface { ) : XAxisInterface {
override var x: Double = 0.0 override var x: Double = 0.0
set(value) { set(value) {
val max = getXMax() - increment * displayCount val max = getXMax() - getDataWidth()
val min = getXMin() val min = getXMin()
if (value > max && min <= max) { if (value > max && min <= max) {
field = max field = max
@ -29,10 +29,13 @@ class XAxis(
} }
override var enabled = true override var enabled = true
override var increment: Double = 1.0
override var displayCount: Int = 10 override var dataWidth: Double? = null
override var labelCount: Int = 2 override var labelCount: Int = 2
var spacing = 16.0
override val textPaint = Paint().apply { override val textPaint = Paint().apply {
isAntiAlias = true isAntiAlias = true
color = Color.parseColor("#FC496D") color = Color.parseColor("#FC496D")
@ -42,27 +45,28 @@ class XAxis(
private val rect = Rect() private val rect = Rect()
override fun getPositionOnRect(entry: Entry, rect: RectF): Double { override fun getPositionOnRect(entry: Entry, drawableSpace: RectF): Double {
return translatePositionToRect(entry.x, rect) return translatePositionToRect(entry.x, drawableSpace)
} }
fun translatePositionToRect(value: Double, rect: RectF): Double { fun translatePositionToRect(value: Double, drawableSpace: RectF): Double {
val rectItem = rect.width() / displayCount // item size in graph return drawableSpace.width() * (value - x) / getDataWidth()
return rectItem * value / increment
}
override fun getXOffset(rect: RectF): Double {
return translatePositionToRect(x, rect)
} }
override fun getXMax(): Double { override fun getXMax(): Double {
return view.series.maxOf { serie -> return view.series.maxOf { serie ->
if (serie.entries.isEmpty()) {
return 0.0
}
serie.entries.maxOf { entry -> entry.x } serie.entries.maxOf { entry -> entry.x }
} }
} }
override fun getXMin(): Double { override fun getXMin(): Double {
return view.series.minOf { serie -> return view.series.minOf { serie ->
if (serie.entries.isEmpty()) {
return 0.0
}
serie.entries.minOf { entry -> entry.x } serie.entries.minOf { entry -> entry.x }
} }
} }
@ -77,7 +81,7 @@ class XAxis(
var maxHeight = 0f var maxHeight = 0f
val graphIncrement = space.width() / (labelCount - 1) val graphIncrement = space.width() / (labelCount - 1)
val valueIncrement = (displayCount * increment / (labelCount - 1)).toDouble() val valueIncrement = (getDataWidth() / (labelCount - 1)).toDouble()
for (index in 0 until labelCount) { for (index in 0 until labelCount) {
val text = onValueFormat(x + valueIncrement * index) val text = onValueFormat(x + valueIncrement * index)
textPaint.getTextBounds(text, 0, text.length, rect) textPaint.getTextBounds(text, 0, text.length, rect)
@ -89,7 +93,6 @@ class XAxis(
xPos = space.right - rect.width() xPos = space.right - rect.width()
} }
canvas.drawText( canvas.drawText(
text, text,
xPos, xPos,
@ -103,4 +106,20 @@ class XAxis(
override fun refresh() { override fun refresh() {
// TODO("Not yet implemented") // TODO("Not yet implemented")
} }
override fun getEntryWidth(drawableSpace: RectF): Double {
var smallest = -1.0
val dataset = view.getDataset()
for (idx in 0 until dataset.size - 1) {
val distance = dataset[idx + 1].x - dataset[idx].x
if (smallest == -1.0 || smallest > distance) {
smallest = distance
}
}
return drawableSpace.width() * smallest / getDataWidth() - spacing
}
override fun getDataWidth(): Double {
return dataWidth ?: (getXMax() - getXMin())
}
} }

View File

@ -18,20 +18,19 @@ sealed interface XAxisInterface {
var x: Double var x: Double
/** /**
* X increment * the "width" of the graph
*
* if not set it will be `XMax - XMin`
*
* ex: to display a 7 days graph history with x values being timestamp in secs, use 7*24*60*60
*/ */
var increment: Double var dataWidth: Double?
/** /**
* text Paint * text Paint
*/ */
val textPaint: Paint val textPaint: Paint
/**
* indicate the max number of entries are displayed
*/
var displayCount: Int
/** /**
* indicate the number of labels displayed * indicate the number of labels displayed
*/ */
@ -49,12 +48,7 @@ sealed interface XAxisInterface {
* *
* @return the left side of the position of the entry * @return the left side of the position of the entry
*/ */
fun getPositionOnRect(entry: Entry, rect: RectF): Double fun getPositionOnRect(entry: Entry, drawableSpace: RectF): Double
/**
* get the graph offset for X (kinda like [getPositionOnRect])
*/
fun getXOffset(rect: RectF): Double
/** /**
* get the maximum the X can get to * get the maximum the X can get to
@ -66,6 +60,18 @@ sealed interface XAxisInterface {
*/ */
fun getXMin(): Double fun getXMin(): Double
/**
* get the size of an entry in the graph
*
* @return the size in [drawableSpace] px
*/
fun getEntryWidth(drawableSpace: RectF): Double
/**
* return the currently used dataWidth
*/
fun getDataWidth(): Double
/** /**
* onDraw event that will draw the XAxis * onDraw event that will draw the XAxis
* *

View File

@ -5,6 +5,7 @@ import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.util.Log
import com.dzeio.charts.ChartView import com.dzeio.charts.ChartView
import com.dzeio.charts.utils.drawRoundRect import com.dzeio.charts.utils.drawRoundRect
@ -35,9 +36,8 @@ class BarSerie(
private val rect = Rect() private val rect = Rect()
override fun onDraw(canvas: Canvas, drawableSpace: RectF) { override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
val spacing = drawableSpace.width() / view.xAxis.displayCount / 10
val barWidth = drawableSpace.width() / view.xAxis.displayCount - spacing
val displayedEntries = getDisplayedEntries() val displayedEntries = getDisplayedEntries()
val barWidth = view.xAxis.getEntryWidth(drawableSpace).toFloat()
val max = view.yAxis.getYMax() val max = view.yAxis.getYMax()
val min = view.yAxis.getYMin() val min = view.yAxis.getYMin()
@ -46,21 +46,10 @@ class BarSerie(
for (entry in displayedEntries) { for (entry in displayedEntries) {
// calculated height in percent from 0 to 100 // calculated height in percent from 0 to 100
val top = (1 - entry.y / max) * drawableSpace.height() + drawableSpace.top val top = (1 - entry.y / max) * drawableSpace.height() + drawableSpace.top
var posX = drawableSpace.left + (view.xAxis.getPositionOnRect( var posX = drawableSpace.left + view.xAxis.getPositionOnRect(
entry, entry,
drawableSpace drawableSpace
) - view.xAxis.getXOffset(drawableSpace)).toFloat() ).toFloat()
// Log.d(TAG, "gpor = ${view.xAxis.getPositionOnRect(entry, space)}, gxo = ${view.xAxis.getXOffset(space)}")
// Log.d(TAG, "max = $max, y = ${entry.y}, height = $height")
// Log.d(TAG, "posX: ${posX / 60 / 60 / 1000}, offsetX = ${view.xAxis.x / (1000 * 60 * 60)}, x = ${entry.x / (1000 * 60 * 60)}, pouet: ${(view.xAxis.x + view.xAxis.displayCount * view.xAxis.increment) / (1000 * 60 * 60)}")
// Log.d(
// TAG, """
// ${posX},
// $top,
// ${(posX + barWidth)},
// ${drawableSpace.bottom}""".trimIndent()
// )
val right = (posX + barWidth).coerceAtMost(drawableSpace.right) val right = (posX + barWidth).coerceAtMost(drawableSpace.right)

View File

@ -19,14 +19,28 @@ sealed class BaseSerie(
override var entries: ArrayList<Entry> = arrayListOf() override var entries: ArrayList<Entry> = arrayListOf()
override fun getDisplayedEntries(): ArrayList<Entry> { override fun getDisplayedEntries(): ArrayList<Entry> {
// -+ view.xAxis.increment = one out of display val minX = view.xAxis.x
val minX = view.xAxis.x - view.xAxis.increment val maxX = minX + view.xAxis.getDataWidth()
val maxX =
view.xAxis.x + view.xAxis.displayCount * view.xAxis.increment + view.xAxis.increment
return entries.filter { val result: ArrayList<Entry> = arrayListOf()
return@filter it.x in minX..maxX
} as ArrayList<Entry> var lastIndex = -1
for (i in 0 until entries.size) {
val it = entries[i]
if (it.x in minX..maxX) {
if (result.size === 0 && i > 0) {
result.add((entries[i - 1]))
}
lastIndex = i
result.add(it)
}
}
if (lastIndex < entries.size - 1) {
result.add(entries [lastIndex + 1])
}
return result
} }
abstract override fun onDraw(canvas: Canvas, drawableSpace: RectF) abstract override fun onDraw(canvas: Canvas, drawableSpace: RectF)

View File

@ -0,0 +1,74 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import com.dzeio.charts.ChartView
class LineSerie(
private val view: ChartView
) : BaseSerie(view) {
private companion object {
const val TAG = "Charts/LineSerie"
}
init {
view.series.add(this)
}
val linePaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#123456")
strokeWidth = 5f
}
val textPaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#FC496D")
textSize = 30f
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
val displayedEntries = getDisplayedEntries()
displayedEntries.sortBy { it.x }
val max = view.yAxis.getYMax()
var previousPosX: Float? = null
var previousPosY: Float? = null
for (entry in displayedEntries) {
// calculated height in percent from 0 to 100
val top = (1 - entry.y / max) * drawableSpace.height() + drawableSpace.top
val posX = (drawableSpace.left +
view.xAxis.getPositionOnRect(entry, drawableSpace) +
view.xAxis.getEntryWidth(drawableSpace) / 2f).toFloat()
// handle color recoloration
val paint = Paint(linePaint)
if (entry.color != null) {
paint.color = entry.color!!
}
// draw smol point
if (posX < drawableSpace.right) {
canvas.drawCircle(posX, top, paint.strokeWidth, paint)
}
// draw line
if (previousPosX != null && previousPosY != null) {
canvas.drawLine(previousPosX, previousPosY, posX, top, paint)
}
previousPosX = posX
previousPosY = top
}
}
override fun refresh() {
// TODO("Not yet implemented")
}
}