1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-24 11:52:15 +00:00

Merge branch 'master' of github.com:dzeiocom/OpenHealth

This commit is contained in:
Florian Bouillon 2022-07-26 20:41:41 +02:00
commit 80e614b988
Signed by: Florian Bouillon
GPG Key ID: 0A288052C94BD2C8
8 changed files with 430 additions and 108 deletions

View File

@ -5,10 +5,15 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.charts.Entry
import com.dzeio.openhealth.adapters.StepsAdapter import com.dzeio.openhealth.adapters.StepsAdapter
import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding
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 StepsHomeFragment : class StepsHomeFragment :
@ -41,19 +46,39 @@ class StepsHomeFragment :
viewModel.items.observe(viewLifecycleOwner) { list -> viewModel.items.observe(viewLifecycleOwner) { list ->
adapter.set(list) adapter.set(list)
// chart.numberOfEntries = list.size / 2
chart.numberOfLabels = 2
val strings = ArrayList<String>() // chart.animation.enabled = false
val values = ArrayList<Int>() chart.animation.refreshRate = 60
chart.animation.duration = 500
list.forEach { chart.xAxis.labels.color = MaterialColors.getColor(
strings.add(it.formatTimestamp()) requireView(),
values.add(it.value) com.google.android.material.R.attr.colorOnBackground
)
chart.xAxis.labels.size = 32f
chart.yAxis.color = MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorPrimary
)
chart.list = list.reversed().map {
return@map Entry(it.timestamp.toDouble(), it.value.toFloat())
} as ArrayList<Entry>
chart.xAxis.onValueFormat = onValueFormat@{
val formatter = DateFormat.getDateTimeInstance(
DateFormat.SHORT,
DateFormat.SHORT,
Locale.getDefault()
)
return@onValueFormat formatter.format(Date(it.toLong()))
} }
chart.setBottomTextList(strings) // chart.yAxis.max = (total / list.size).toInt()
chart.setDataList(
values chart.refresh()
)
} }
} }
} }

View File

@ -0,0 +1,51 @@
package com.dzeio.charts
import kotlin.math.abs
data class Animation(
/**
* Enable / Disable the Chart Animations
*/
var enabled: Boolean = true,
/**
* Number of milliseconds the animation is running before it ends
*/
var duration: Int = 1000,
/**
* Number of updates per seconds
*/
var refreshRate: Int = 50
) {
/**
* Update the value depending on the maximum obtainable value
*
* @param maxValue the maximum value the item can obtain
* @param targetValue the value you want to obtain at the end of the animation
* @param currentValue the current value
*
* @return the new updated value
*/
fun updateValue(maxValue: Float, targetValue: Float, currentValue: Float): Float {
if (!enabled) {
return targetValue
}
val moveValue = (maxValue - targetValue) / refreshRate
var result = targetValue
if (currentValue < targetValue) {
result = currentValue + moveValue
} else if (currentValue > targetValue) {
result = currentValue - moveValue
}
if (abs(targetValue - currentValue) < moveValue) {
return targetValue
}
return result
}
fun getDelay() = this.duration / this.refreshRate
}

View File

@ -0,0 +1,6 @@
package com.dzeio.charts
data class Entry(
val x: Double,
val y: Float
)

View File

@ -0,0 +1,22 @@
package com.dzeio.charts
import android.graphics.Color
import android.graphics.Paint
import androidx.annotation.ColorInt
class XAxisLabels {
var size = 25f
@ColorInt
var color: Int = Color.parseColor("#9B9A9B")
fun build(): Paint {
return Paint().also {
it.isAntiAlias = true
it.color = color
it.textSize = size
it.textAlign = Paint.Align.CENTER
}
}
}

View File

@ -0,0 +1,28 @@
package com.dzeio.charts.axis
import com.dzeio.charts.XAxisLabels
class XAxis<T> {
var max: T? = null
var min: T? = null
val labels = XAxisLabels()
/**
* Number of entries displayed in the chart at the same time
*/
var entriesDisplayed = 5
/**
* Offset in the list
*
* WILL CHANGE AS SOON AS SCROLLING IS AVAILABLE
*/
var baseOffset = 0
var onValueFormat: (it: T) -> String = onValueFormat@{
return@onValueFormat it.toString()
}
}

View File

@ -0,0 +1,12 @@
package com.dzeio.charts.axis
import android.graphics.Color
import androidx.annotation.ColorInt
class YAxis<T> {
var max: T? = null
var min: T? = null
@ColorInt
var color = Color.parseColor("#FC496D")
}

View File

@ -2,128 +2,156 @@ package com.dzeio.charts.views
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
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.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.View import com.dzeio.charts.Animation
import com.dzeio.charts.Entry
import com.dzeio.charts.axis.XAxis
import com.dzeio.charts.axis.YAxis
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class BarChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : class BarChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) { BaseChart(context, attrs) {
companion object { companion object {
const val TAG = "DzeioCharts/BarView" const val TAG = "DzeioCharts/BarView"
} }
/** /**
* Nunber of entries displayed at the same time * Number of entries displayed at the same time
*/ */
val numberOfEntries = 5 private var zoom = 100f
/** /**
* Number of labels displayed at the same time * Number of labels displayed at the same time
*/ */
val numberOfLabels = 3 var numberOfLabels = 3
/** /**
* Spacing between entries * Spacing between entries
*/ */
val spacing = 22 var spacing = 22
/** val xAxis: XAxis<Double> = XAxis()
* top margin from the canvas
*/ val yAxis: YAxis<Float> = YAxis()
@Deprecated("Not needed anymore, Use the parent Padding/Margin")
private val topMargin: Int = 5 val animation = Animation()
private val textTopMargin = 5 private val textTopMargin = 5
private var barWidth: Int = 0 private var barWidth: Int = 0
private val textColor = Color.parseColor("#9B9A9B")
private val foregroundColor = Color.parseColor("#FC496D")
private val percentList: ArrayList<Float> = ArrayList() private val percentList: ArrayList<Float> = ArrayList()
private var targetPercentList: ArrayList<Float> = ArrayList()
private val textPaint: Paint = Paint().also {
it.isAntiAlias = true
it.color = textColor
it.textSize = 25f
it.textAlign = Paint.Align.CENTER
}
private val fgPaint: Paint = Paint().also { /**
* value goes from 1 to 0 (1 at bottom, 0 at top)
*/
private var targetPercentList: ArrayList<Float> = ArrayList()
private var fgPaint: Paint = Paint().also {
it.isAntiAlias = true it.isAntiAlias = true
it.color = foregroundColor it.color = yAxis.color
} }
private val rect: Rect = Rect() private val rect: RectF = RectF()
private var bottomTextDescent = 0 private var bottomTextDescent = 0
private var bottomTextHeight = 0 private var bottomTextHeight = 0
private var bottomTextList: ArrayList<String>? = ArrayList()
private var movementOffset: Int = 0
private val animator: Runnable = object : Runnable { private val animator: Runnable = object : Runnable {
override fun run() { override fun run() {
var needNewFrame = false var needNewFrame = false
for (i in targetPercentList.indices) { for (i in targetPercentList.indices) {
if (percentList[i] < targetPercentList[i]) { val value = animation.updateValue(1f, targetPercentList[i], percentList[i])
percentList[i] = percentList[i] + 0.02f
if (value != percentList[i]) {
needNewFrame = true needNewFrame = true
} else if (percentList[i] > targetPercentList[i]) { percentList[i] = value
percentList[i] = percentList[i] - 0.02f
needNewFrame = true
}
if (abs(targetPercentList[i] - percentList[i]) < 0.02f) {
percentList[i] = targetPercentList[i]
} }
} }
if (needNewFrame) { if (needNewFrame) {
postDelayed(this, 20) postDelayed(this, animation.getDelay().toLong())
} }
invalidate() invalidate()
} }
} }
/** var list: ArrayList<Entry> = arrayListOf()
* dataList will be reset when called is method.
* private var bottomTexts: ArrayList<String> = arrayListOf()
* @param bottomStringList The String ArrayList in the bottom.
*/ // init {
fun setBottomTextList(bottomStringList: ArrayList<String>?) { // val mockList = ArrayList<Entry>()
barWidth = measuredWidth / numberOfEntries - spacing // for (i in 0 until 25) {
bottomTextList = bottomStringList // mockList.add(Entry(i.toDouble(), i.toFloat()))
// }
//
// list = mockList
//
// this.refresh()
// }
fun refresh() {
val r = Rect() val r = Rect()
//// prepare bottom texts
bottomTextDescent = 0 bottomTextDescent = 0
for (s in bottomTextList!!) { bottomTextHeight = 0
textPaint.getTextBounds(s, 0, s.length, r) bottomTexts = arrayListOf()
//// prepare values
// set the bar Width (also handle div by 0)
barWidth = measuredWidth / max(min(list.size, getDisplayedEntries()), 1) - spacing
// calculate max depending on the maximum value displayed or set in the yAxis params
val max: Float = if (yAxis.max != null) yAxis.max!! else {
var calculatedMax = 0f
for (entry in list.subList(this.getXOffset(), getDisplayedEntries() + this.getXOffset())) {
if (entry.y > calculatedMax) calculatedMax = entry.y
}
calculatedMax
}
// make sure the target list
// Log.d(TAG, list.size.toString())
targetPercentList = arrayListOf()
for ((i, item) in list.withIndex()) {
//// Process bottom texts
val text = xAxis.onValueFormat(item.x)
bottomTexts.add(text)
// get Text boundaries
xAxis.labels.build().getTextBounds(text, 0, text.length, r)
// get height of text
if (bottomTextHeight < r.height()) { if (bottomTextHeight < r.height()) {
bottomTextHeight = r.height() bottomTextHeight = r.height()
} }
Log.d(TAG, measuredWidth.toString())
// if (autoSetWidth && barWidth < r.width()) { // get text descent
// barWidth = r.width() val descent = abs(r.bottom)
// } if (bottomTextDescent < descent) {
if (bottomTextDescent < abs(r.bottom)) { bottomTextDescent = descent
bottomTextDescent = abs(r.bottom)
} }
}
postInvalidate()
}
/** //// process values
* @param list The ArrayList of Integer with the range of [0-max].
*/
fun setDataList(list: ArrayList<Int>) {
barWidth = measuredWidth / numberOfEntries - spacing
// Calculate max
val max = list.reduce { acc, i -> if (acc > i) return@reduce acc else return@reduce i }
targetPercentList = ArrayList() // add to animations the values
for (integer in list) { targetPercentList.add(min(1 - item.y / max, 1f))
targetPercentList.add(1 - integer.toFloat() / max.toFloat())
} }
// Make sure percentList.size() == targetPercentList.size() // post list
if (percentList.isEmpty() || percentList.size < targetPercentList.size) { if (percentList.isEmpty() || percentList.size < targetPercentList.size) {
val temp = targetPercentList.size - percentList.size val temp = targetPercentList.size - percentList.size
for (i in 0 until temp) { for (i in 0 until temp) {
@ -136,64 +164,99 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
} }
} }
// Misc operations
fgPaint = Paint().apply {
isAntiAlias = true
color = yAxis.color
}
removeCallbacks(animator) removeCallbacks(animator)
post(animator) post(animator)
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (percentList.isNotEmpty()) { if (percentList.isNotEmpty()) {
for (i in 1 until percentList.size) { // draw each rectangles
val left = spacing * i + barWidth * (i - 1) for (i in 1..getDisplayedEntries()) {
Log.d(TAG, "$spacing, $i, $barWidth = $left") // Log.d(TAG, percentList[i - 1].toString())
val right = (spacing + barWidth) * i val left = spacing * i + barWidth * (i - 1).toFloat()
val bottom = height - bottomTextHeight - textTopMargin // Log.d(TAG, "$spacing, $i, $barWidth = $left")
val top = topMargin + ((bottom - topMargin) * percentList[i - 1]) val right = (spacing + barWidth) * i.toFloat()
val bottom = height - bottomTextHeight - textTopMargin.toFloat()
val top = bottom * percentList[this.getXOffset() + i - 1]
rect.set(left, top.toInt(), right, bottom) // create rounded rect
canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, fgPaint)
canvas.drawRect(rect, fgPaint) // remove the bottom corners DUH
canvas.drawRect(left, max(top, bottom - 8f), right, bottom, fgPaint)
} }
} }
if (bottomTextList != null && !bottomTextList!!.isEmpty()) { if (bottomTexts.isNotEmpty() && numberOfLabels > 0) {
val size = bottomTexts.size
var i = 1 var i = 1
for (s in bottomTextList!!) { var items = size / max(2, (numberOfLabels - 2))
// handle cases where size is even and numberOfLabels is 3
if (size % 2 != 0) {
items += 1
}
val rect = Rect()
// Log.i(TAG, "$size / max($numberOfLabels - 2, 2) = $items")
for (s in bottomTexts) {
if ((numberOfLabels <= 2 || i % items != 0) && i != 1 && i != size) {
i++
continue
}
// Log.i(TAG, "Drawing $i")
xAxis.labels.build().getTextBounds(s, 0, s.length, rect)
canvas.drawText( canvas.drawText(
s, s,
(spacing * i + barWidth * (i - 1) + barWidth / 2).toFloat(), // handle last entry overflowing
min(
// handle first entry overflowing
max(
(spacing * i + barWidth * (i - 1) + barWidth / 2).toFloat(),
rect.width() / 2f
),
measuredWidth - rect.width() / 2f
),
(height - bottomTextDescent).toFloat(), (height - bottomTextDescent).toFloat(),
textPaint xAxis.labels.build()
) )
i++ i++
if (numberOfLabels == 1) {
break
}
} }
} }
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onChartMoved(movementX: Float, movementY: Float) {
val mViewWidth = measureWidth(widthMeasureSpec) movementOffset = (movementX / 100).toInt()
val mViewHeight = measureHeight(heightMeasureSpec) refresh()
setMeasuredDimension(mViewWidth, mViewHeight)
} }
private fun measureWidth(measureSpec: Int): Int { override fun onZoomChanged(scale: Float) {
var preferred = 0 Log.d(TAG, "New Zoom: $scale")
if (bottomTextList != null) { zoom = scale
preferred = bottomTextList!!.size * (barWidth + spacing) refresh()
}
return getMeasurement(measureSpec, preferred)
} }
private fun measureHeight(measureSpec: Int): Int { private fun getXOffset(): Int {
val preferred = 222 return min(max(0, xAxis.baseOffset + movementOffset), list.size - 1 - getDisplayedEntries())
return getMeasurement(measureSpec, preferred)
} }
private fun getMeasurement(measureSpec: Int, preferred: Int): Int { private fun getDisplayedEntries(): Int {
val specSize = MeasureSpec.getSize(measureSpec) // Log.d(TAG, "Number of entries displayed ${list.size}, ${xAxis.entriesDisplayed} + (($zoom - 100) * 10) = ${xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()}")
val measurement = when (MeasureSpec.getMode(measureSpec)) { return max(1, min(list.size, xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()))
MeasureSpec.EXACTLY -> specSize
MeasureSpec.AT_MOST -> Math.min(preferred, specSize)
else -> preferred
}
return measurement
} }
} }

View File

@ -0,0 +1,115 @@
package com.dzeio.charts.views
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.MotionEvent.INVALID_POINTER_ID
import android.view.ScaleGestureDetector
import android.view.View
import androidx.core.view.MotionEventCompat
abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) {
// The active pointer is the one currently moving our object.
private var activePointerId = INVALID_POINTER_ID
private var lastTouchX: Float = 0f
private var lastTouchY: Float = 0f
private var posX: Float = 0f
private var posY: Float = 0f
private var lastZoom: Float = 100f
private var currentZoom: Float = 0f
open fun onChartMoved(movementX: Float, movementY: Float) {}
/**
* @param scale Float starting from 100%
*
* 99-% zoom out,
* 101+% zoom in
*/
open fun onZoomChanged(scale: Float) {}
private val scaleGestureDetector = ScaleGestureDetector(
context,
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (currentZoom != detector.scaleFactor) {
currentZoom = detector.scaleFactor
onZoomChanged(lastZoom + -currentZoom + 1)
}
return super.onScale(detector)
}
override fun onScaleEnd(detector: ScaleGestureDetector?) {
super.onScaleEnd(detector)
lastZoom += -currentZoom + 1
}
})
/**
* Code mostly stolen from https://developer.android.com/training/gestures/scale#drag
*/
override fun onTouchEvent(ev: MotionEvent): Boolean {
super.onTouchEvent(ev)
scaleGestureDetector.onTouchEvent(ev)
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
ev.actionIndex.also { pointerIndex ->
// Remember where we started (for dragging)
lastTouchX = ev.getX(pointerIndex)
lastTouchY = ev.getY(pointerIndex)
}
// Save the ID of this pointer (for dragging)
activePointerId = ev.getPointerId(0)
}
MotionEvent.ACTION_MOVE -> {
// Find the index of the active pointer and fetch its position
val (x: Float, y: Float) =
ev.findPointerIndex(activePointerId).let { pointerIndex ->
// Calculate the distance moved
ev.getX(pointerIndex) to
ev.getY(pointerIndex)
}
posX += x - lastTouchX
posY += y - lastTouchY
onChartMoved(-posX, posY)
// Remember this touch position for the next move event
lastTouchX = x
lastTouchY = y
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
activePointerId = INVALID_POINTER_ID
}
MotionEvent.ACTION_POINTER_UP -> {
ev.actionIndex.also { pointerIndex ->
ev.getPointerId(pointerIndex)
.takeIf { it == activePointerId }
?.run {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
val newPointerIndex = if (pointerIndex == 0) 1 else 0
lastTouchX = MotionEventCompat.getX(ev, newPointerIndex)
lastTouchY = MotionEventCompat.getY(ev, newPointerIndex)
activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex)
}
}
}
}
return true
}
}