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.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.charts.Entry
import com.dzeio.openhealth.adapters.StepsAdapter
import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding
import com.google.android.material.color.MaterialColors
import dagger.hilt.android.AndroidEntryPoint
import java.text.DateFormat
import java.util.Date
import java.util.Locale
@AndroidEntryPoint
class StepsHomeFragment :
@ -41,19 +46,39 @@ class StepsHomeFragment :
viewModel.items.observe(viewLifecycleOwner) { list ->
adapter.set(list)
// chart.numberOfEntries = list.size / 2
chart.numberOfLabels = 2
val strings = ArrayList<String>()
val values = ArrayList<Int>()
// chart.animation.enabled = false
chart.animation.refreshRate = 60
chart.animation.duration = 500
list.forEach {
strings.add(it.formatTimestamp())
values.add(it.value)
chart.xAxis.labels.color = MaterialColors.getColor(
requireView(),
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.setDataList(
values
)
// chart.yAxis.max = (total / list.size).toInt()
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.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
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.max
import kotlin.math.min
class BarChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) {
BaseChart(context, attrs) {
companion object {
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
*/
val numberOfLabels = 3
var numberOfLabels = 3
/**
* Spacing between entries
*/
val spacing = 22
var spacing = 22
/**
* top margin from the canvas
*/
@Deprecated("Not needed anymore, Use the parent Padding/Margin")
private val topMargin: Int = 5
val xAxis: XAxis<Double> = XAxis()
val yAxis: YAxis<Float> = YAxis()
val animation = Animation()
private val textTopMargin = 5
private var barWidth: Int = 0
private val textColor = Color.parseColor("#9B9A9B")
private val foregroundColor = Color.parseColor("#FC496D")
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.color = foregroundColor
it.color = yAxis.color
}
private val rect: Rect = Rect()
private val rect: RectF = RectF()
private var bottomTextDescent = 0
private var bottomTextHeight = 0
private var bottomTextList: ArrayList<String>? = ArrayList()
private var movementOffset: Int = 0
private val animator: Runnable = object : Runnable {
override fun run() {
var needNewFrame = false
for (i in targetPercentList.indices) {
if (percentList[i] < targetPercentList[i]) {
percentList[i] = percentList[i] + 0.02f
val value = animation.updateValue(1f, targetPercentList[i], percentList[i])
if (value != percentList[i]) {
needNewFrame = true
} else if (percentList[i] > targetPercentList[i]) {
percentList[i] = percentList[i] - 0.02f
needNewFrame = true
}
if (abs(targetPercentList[i] - percentList[i]) < 0.02f) {
percentList[i] = targetPercentList[i]
percentList[i] = value
}
}
if (needNewFrame) {
postDelayed(this, 20)
postDelayed(this, animation.getDelay().toLong())
}
invalidate()
}
}
/**
* dataList will be reset when called is method.
*
* @param bottomStringList The String ArrayList in the bottom.
*/
fun setBottomTextList(bottomStringList: ArrayList<String>?) {
barWidth = measuredWidth / numberOfEntries - spacing
bottomTextList = bottomStringList
var list: ArrayList<Entry> = arrayListOf()
private var bottomTexts: ArrayList<String> = arrayListOf()
// init {
// val mockList = ArrayList<Entry>()
// for (i in 0 until 25) {
// mockList.add(Entry(i.toDouble(), i.toFloat()))
// }
//
// list = mockList
//
// this.refresh()
// }
fun refresh() {
val r = Rect()
//// prepare bottom texts
bottomTextDescent = 0
for (s in bottomTextList!!) {
textPaint.getTextBounds(s, 0, s.length, r)
bottomTextHeight = 0
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()) {
bottomTextHeight = r.height()
}
Log.d(TAG, measuredWidth.toString())
// if (autoSetWidth && barWidth < r.width()) {
// barWidth = r.width()
// }
if (bottomTextDescent < abs(r.bottom)) {
bottomTextDescent = abs(r.bottom)
// get text descent
val descent = abs(r.bottom)
if (bottomTextDescent < descent) {
bottomTextDescent = descent
}
}
postInvalidate()
}
/**
* @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 }
//// process values
targetPercentList = ArrayList()
for (integer in list) {
targetPercentList.add(1 - integer.toFloat() / max.toFloat())
// add to animations the values
targetPercentList.add(min(1 - item.y / max, 1f))
}
// Make sure percentList.size() == targetPercentList.size()
// post list
if (percentList.isEmpty() || percentList.size < targetPercentList.size) {
val temp = targetPercentList.size - percentList.size
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)
post(animator)
}
override fun onDraw(canvas: Canvas) {
if (percentList.isNotEmpty()) {
for (i in 1 until percentList.size) {
val left = spacing * i + barWidth * (i - 1)
Log.d(TAG, "$spacing, $i, $barWidth = $left")
val right = (spacing + barWidth) * i
val bottom = height - bottomTextHeight - textTopMargin
val top = topMargin + ((bottom - topMargin) * percentList[i - 1])
// draw each rectangles
for (i in 1..getDisplayedEntries()) {
// Log.d(TAG, percentList[i - 1].toString())
val left = spacing * i + barWidth * (i - 1).toFloat()
// Log.d(TAG, "$spacing, $i, $barWidth = $left")
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)
canvas.drawRect(rect, fgPaint)
// create rounded rect
canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, 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
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(
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(),
textPaint
xAxis.labels.build()
)
i++
if (numberOfLabels == 1) {
break
}
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val mViewWidth = measureWidth(widthMeasureSpec)
val mViewHeight = measureHeight(heightMeasureSpec)
setMeasuredDimension(mViewWidth, mViewHeight)
override fun onChartMoved(movementX: Float, movementY: Float) {
movementOffset = (movementX / 100).toInt()
refresh()
}
private fun measureWidth(measureSpec: Int): Int {
var preferred = 0
if (bottomTextList != null) {
preferred = bottomTextList!!.size * (barWidth + spacing)
}
return getMeasurement(measureSpec, preferred)
override fun onZoomChanged(scale: Float) {
Log.d(TAG, "New Zoom: $scale")
zoom = scale
refresh()
}
private fun measureHeight(measureSpec: Int): Int {
val preferred = 222
return getMeasurement(measureSpec, preferred)
private fun getXOffset(): Int {
return min(max(0, xAxis.baseOffset + movementOffset), list.size - 1 - getDisplayedEntries())
}
private fun getMeasurement(measureSpec: Int, preferred: Int): Int {
val specSize = MeasureSpec.getSize(measureSpec)
val measurement = when (MeasureSpec.getMode(measureSpec)) {
MeasureSpec.EXACTLY -> specSize
MeasureSpec.AT_MOST -> Math.min(preferred, specSize)
else -> preferred
}
return measurement
private fun getDisplayedEntries(): Int {
// Log.d(TAG, "Number of entries displayed ${list.size}, ${xAxis.entriesDisplayed} + (($zoom - 100) * 10) = ${xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()}")
return max(1, min(list.size, xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()))
}
}

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
}
}