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

feat(charts): Moved to modules based charts

Signed-off-by: Avior <github@avior.me>
This commit is contained in:
Florian Bouillon 2022-07-28 10:01:52 +02:00
parent c2bc9eced2
commit f6b5715572
Signed by: Florian Bouillon
GPG Key ID: 0A288052C94BD2C8
9 changed files with 409 additions and 46 deletions

View File

@ -1,13 +1,13 @@
package com.dzeio.openhealth.ui.steps package com.dzeio.openhealth.ui.steps
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.charts.Entry import com.dzeio.charts.Entry
import com.dzeio.charts.series.BarSerie
import com.dzeio.openhealth.Application import com.dzeio.openhealth.Application
import com.dzeio.openhealth.R import com.dzeio.openhealth.R
import com.dzeio.openhealth.adapters.StepsAdapter import com.dzeio.openhealth.adapters.StepsAdapter
@ -52,10 +52,17 @@ class StepsHomeFragment :
val chart = binding.chart val chart = binding.chart
val serie = BarSerie()
chart.series = arrayListOf(serie)
viewModel.items.observe(viewLifecycleOwner) { list -> viewModel.items.observe(viewLifecycleOwner) { list ->
adapter.set(list) adapter.set(list)
chart.xAxis.entriesDisplayed = 30
chart.numberOfLabels = 2 chart.debug = true
chart.xAxis.entriesDisplayed = 10
// chart.numberOfLabels = 2
// chart.animation.enabled = false // chart.animation.enabled = false
chart.animation.refreshRate = 60 chart.animation.refreshRate = 60
@ -71,7 +78,7 @@ class StepsHomeFragment :
com.google.android.material.R.attr.colorPrimary com.google.android.material.R.attr.colorPrimary
) )
chart.list = list.reversed().map { serie.datas = list.reversed().map {
return@map Entry(it.timestamp.toDouble(), it.value.toFloat()) return@map Entry(it.timestamp.toDouble(), it.value.toFloat())
} as ArrayList<Entry> } as ArrayList<Entry>
@ -88,21 +95,5 @@ class StepsHomeFragment :
chart.refresh() chart.refresh()
} }
val scrollView = requireActivity().findViewById<NestedScrollView>(R.id.scrollView)
var scrollEnabled = false
scrollView.setOnTouchListener { view, _ ->
view.performClick()
if (scrollEnabled) {
} else {
return@setOnTouchListener !scrollEnabled
}
return@setOnTouchListener true
}
binding.chart.setOnToggleScroll {
Log.d(TAG, it.toString())
scrollEnabled = it
}
} }
} }

View File

@ -22,7 +22,7 @@
android:orientation="vertical"> android:orientation="vertical">
<view <view
class="com.dzeio.charts.views.BarChartView" class="com.dzeio.charts.ChartView"
android:id="@+id/chart" android:id="@+id/chart"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="200dp"

View File

@ -0,0 +1,152 @@
package com.dzeio.charts
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import com.dzeio.charts.axis.XAxis
import com.dzeio.charts.axis.YAxis
import com.dzeio.charts.components.ChartScroll
import com.dzeio.charts.series.SerieAbstract
import kotlin.math.max
import kotlin.math.min
class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) {
companion object {
const val TAG = "DzeioCharts/ChartView"
}
var debug = false
val xAxis = XAxis<Float>()
val yAxis = YAxis<Float>()
val animation = Animation()
val scroller = ChartScroll(this).apply {
setOnChartMoved { movementX, movementY ->
// Log.d(TAG, "scrolled: $movementX")
movementOffset = movementX / 100
refresh()
}
setOnZoomChanged {
Log.d(TAG, "New Zoom: $it")
zoom = (it * 1.2).toFloat()
refresh()
}
}
private val animator: Runnable = object : Runnable {
override fun run() {
var needNewFrame = false
for (serie in series) {
val result = serie.onUpdate()
if (result) {
needNewFrame = true
}
}
if (needNewFrame) {
postDelayed(this, animation.getDelay().toLong())
}
invalidate()
}
}
/**
* global padding
*/
var padding: Float = 8f
var series: ArrayList<SerieAbstract> = arrayListOf()
set(value) {
for (serie in value) {
serie.view = this
}
field = value
}
/**
* Number of entries displayed at the same time
*/
private var zoom = 100f
var movementOffset: Float = 0f
private val rectF = RectF()
private val otherUseRectF = RectF()
fun refresh() {
for (serie in series) {
serie.prepareData()
}
rectF.set(
padding,
padding,
measuredWidth - padding - yAxis.getWidth(longestSerie().toFloat()) - padding,
height - padding
)
removeCallbacks(animator)
post(animator)
}
private val fgPaint: Paint = Paint().also {
it.isAntiAlias = true
it.color = Color.parseColor("#123456")
}
override fun onDraw(canvas: Canvas) {
for (serie in series) {
serie.displayData(canvas, rectF)
}
canvas.drawRect(
measuredWidth - padding - yAxis.getWidth(longestSerie().toFloat()),
0f,
measuredWidth - padding,
height - padding,
fgPaint
)
super.onDraw(canvas)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
performClick()
return scroller.onTouchEvent(event)
}
fun getXOffset(): Int {
// Log.d(
// TAG,
// "baseOffset: ${xAxis.baseOffset}, mOffset: $movementOffset = ${xAxis.baseOffset + movementOffset}"
// )
// Log.d(
// TAG,
// "longestOffset: ${longestSerie()}, displayedEntries: ${getDisplayedEntries()} = ${longestSerie() - getDisplayedEntries()}"
// )
return min(
max(0f, xAxis.baseOffset + movementOffset).toInt(),
longestSerie() - getDisplayedEntries()
)
}
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(0, xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt())
}
private fun longestSerie(): Int {
var size = 0
for (serie in series) {
if (serie.datas.size > size) size = serie.datas.size
}
return size
}
}

View File

@ -1,6 +1,8 @@
package com.dzeio.charts.axis package com.dzeio.charts.axis
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
class YAxis<T> { class YAxis<T> {
@ -9,4 +11,20 @@ class YAxis<T> {
@ColorInt @ColorInt
var color = Color.parseColor("#FC496D") var color = Color.parseColor("#FC496D")
var paint: Paint = Paint().also {
it.isAntiAlias = true
it.color = color
it.textSize = 30f
it.textAlign = Paint.Align.CENTER
}
var legendEnabled = true
private val rect: Rect = Rect()
fun getWidth(max: Float): Int {
paint.getTextBounds(max.toString(), 0, max.toString().length, rect)
return rect.width()
}
} }

View File

@ -1,14 +1,11 @@
package com.dzeio.charts.views package com.dzeio.charts.components
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.MotionEvent.INVALID_POINTER_ID import android.view.MotionEvent.INVALID_POINTER_ID
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.view.View import android.view.View
abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : class ChartScroll(view: View) {
View(context, attrs) {
// The active pointer is the one currently moving our object. // The active pointer is the one currently moving our object.
private var activePointerId = INVALID_POINTER_ID private var activePointerId = INVALID_POINTER_ID
@ -22,23 +19,30 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
private var lastZoom: Float = 100f private var lastZoom: Float = 100f
private var currentZoom: Float = 0f private var currentZoom: Float = 0f
open fun onChartMoved(movementX: Float, movementY: Float) {} private var onChartMoved: ((movementX: Float, movementY: Float) -> Unit)? = null
fun setOnChartMoved(fn: (movementX: Float, movementY: Float) -> Unit) {
onChartMoved = fn
}
private var onZoomChanged: ((scale: Float) -> Unit)? = null
/** /**
* @param scale Float starting from 100% * @param fn.scale Float starting from 100%
* *
* 99-% zoom out, * 99-% zoom out,
* 101+% zoom in * 101+% zoom in
*/ */
open fun onZoomChanged(scale: Float) {} fun setOnZoomChanged(fn: (scale: Float) -> Unit) {
onZoomChanged = fn
}
private val scaleGestureDetector = ScaleGestureDetector( private val scaleGestureDetector = ScaleGestureDetector(
context, view.context,
object : ScaleGestureDetector.SimpleOnScaleGestureListener() { object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean { override fun onScale(detector: ScaleGestureDetector): Boolean {
if (currentZoom != detector.scaleFactor) { if (currentZoom != detector.scaleFactor) {
currentZoom = detector.scaleFactor currentZoom = detector.scaleFactor
onZoomChanged(lastZoom + -currentZoom + 1) onZoomChanged?.invoke(lastZoom + -currentZoom + 1)
} }
return super.onScale(detector) return super.onScale(detector)
@ -55,9 +59,7 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
/** /**
* Code mostly stolen from https://developer.android.com/training/gestures/scale#drag * Code mostly stolen from https://developer.android.com/training/gestures/scale#drag
*/ */
override fun onTouchEvent(ev: MotionEvent): Boolean { fun onTouchEvent(ev: MotionEvent): Boolean {
super.onTouchEvent(ev)
scaleGestureDetector.onTouchEvent(ev) scaleGestureDetector.onTouchEvent(ev)
when (ev.actionMasked) { when (ev.actionMasked) {
@ -84,7 +86,7 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
posX += x - lastTouchX posX += x - lastTouchX
posY += y - lastTouchY posY += y - lastTouchY
onChartMoved(-posX, posY) onChartMoved?.invoke(-posX, posY)
// Remember this touch position for the next move event // Remember this touch position for the next move event
lastTouchX = x lastTouchX = x

View File

@ -0,0 +1,15 @@
package com.dzeio.charts.components
import android.graphics.RectF
class Sidebar {
var enabled = true
private val rect: RectF = RectF()
fun refresh(max: Float) {
}
}

View File

@ -0,0 +1,154 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.Log
import kotlin.math.max
class BarSerie : SerieAbstract() {
companion object {
const val TAG = "DzeioCharts/BarSerie"
}
var spacing: Float = 8f
var targetPercentList = arrayListOf<Float>()
var percentList = arrayListOf<Float>()
private var fgPaint: Paint = Paint().also {
it.isAntiAlias = true
}
var previousRefresh = 0
override fun onUpdate(): Boolean {
var needNewFrame = false
for (i in targetPercentList.indices) {
val value = view.animation.updateValue(
1f,
targetPercentList[i],
percentList[i],
0f,
0.01f
)
if (value != percentList[i]) {
needNewFrame = true
percentList[i] = value
}
}
return needNewFrame
}
override fun prepareData() {
val max: Float = if (view.yAxis.max != null) view.yAxis.max!! else {
getMax()
}
targetPercentList = arrayListOf()
// Log.d(TAG, "offset: ${view.getXOffset()}, displayed: ${view.getDisplayedEntries()}")
for (item in datas.subList(
view.getXOffset(),
view.getXOffset() + view.getDisplayedEntries()
)) {
// // // Process bottom texts
// val text = view.xAxis.onValueFormat(item.x)
// bottomTexts.add(text)
//
// // get Text boundaries
// view.xAxis.labels.build().getTextBounds(text, 0, text.length, r)
//
// // get height of text
// if (bottomTextHeight < r.height()) {
// bottomTextHeight = r.height()
// }
//
// // get text descent
// val descent = abs(r.bottom)
// if (bottomTextDescent < descent) {
// bottomTextDescent = descent
// }
// // process values
// Log.d(TAG, item.y.toString())
// add to animations the values
targetPercentList.add(1 - item.y / max)
}
// post list
val offset = view.getXOffset()
val movement = offset - previousRefresh
Log.d(TAG, "$offset - $previousRefresh = $movement")
if (movement != 0) {
previousRefresh = offset
}
// if (movement != 0) {
// Log.d(TAG, movement.toString())
// }
if (movement >= 1) {
percentList = percentList.subList(1, percentList.size).toCollection(ArrayList())
percentList.add(1f)
} else if (movement <= -1) {
percentList = percentList.subList(0, percentList.size - 1).toCollection(ArrayList())
percentList.add(0, 1f)
}
if (percentList.isEmpty() || percentList.size < targetPercentList.size) {
val temp = targetPercentList.size - percentList.size
for (i in 0 until temp) {
percentList.add(1f)
}
} else if (percentList.size > targetPercentList.size) {
val temp = percentList.size - targetPercentList.size
for (i in 0 until temp) {
percentList.removeAt(percentList.size - 1)
}
}
fgPaint.color = view.yAxis.color
}
override fun displayData(canvas: Canvas, rect: RectF) {
val barWidth = rect.width() / view.getDisplayedEntries() - spacing
if (percentList.isNotEmpty()) {
// draw each rectangles
for (i in 1..percentList.size) {
// Log.d(TAG, percentList[i - 1].toString())
val left = rect.left + spacing * i + barWidth * (i - 1).toFloat()
// Log.d(TAG, "$spacing, $i, $barWidth = $left")
val right = rect.left + (spacing + barWidth) * i.toFloat()
val bottom = rect.top + rect.height()
val top = (bottom - rect.top) * percentList[i - 1]
// 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 (view.debug) {
canvas.drawText(
(getMax() - getMax() * targetPercentList[i - 1]).toString(),
left + (right - left) / 2,
top + (bottom - top) / 2,
view.xAxis.labels.build()
)
}
}
}
}
private fun getMax(): Float {
var calculatedMax = 0f
for (entry in datas.subList(
view.getXOffset(),
view.getDisplayedEntries() + view.getXOffset()
)) {
if (entry.y > calculatedMax) calculatedMax = entry.y
}
return if (calculatedMax < 0) 0f else calculatedMax
}
}

View File

@ -0,0 +1,31 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.RectF
import com.dzeio.charts.ChartView
import com.dzeio.charts.Entry
abstract class SerieAbstract {
var datas: ArrayList<Entry> = arrayListOf()
lateinit var view: ChartView
/**
* Animation updates
*/
abstract fun onUpdate(): Boolean
/**
* Function to prepare for an update
*/
abstract fun prepareData()
/**
* Function to display data on the graph
*
* @param canvas the canvas to draw on
* @param rect the rectangle in which you have to draw data
*/
abstract fun displayData(canvas: Canvas, rect: RectF)
}

View File

@ -6,7 +6,7 @@ import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.view.View
import com.dzeio.charts.Animation import com.dzeio.charts.Animation
import com.dzeio.charts.Entry import com.dzeio.charts.Entry
import com.dzeio.charts.axis.XAxis import com.dzeio.charts.axis.XAxis
@ -16,7 +16,7 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class BarChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : class BarChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
BaseChart(context, attrs) { View(context, attrs) {
companion object { companion object {
const val TAG = "DzeioCharts/BarView" const val TAG = "DzeioCharts/BarView"
@ -263,16 +263,16 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
} }
} }
override fun onChartMoved(movementX: Float, movementY: Float) { // override fun onChartMoved(movementX: Float, movementY: Float) {
movementOffset = (movementX / 100).toInt() // movementOffset = (movementX / 100).toInt()
refresh() // refresh()
} // }
override fun onZoomChanged(scale: Float) { // override fun onZoomChanged(scale: Float) {
Log.d(TAG, "New Zoom: $scale") // Log.d(TAG, "New Zoom: $scale")
zoom = (scale * 1.2).toFloat() // zoom = (scale * 1.2).toFloat()
refresh() // refresh()
} // }
private fun getXOffset(): Int { private fun getXOffset(): Int {
return min(max(0, xAxis.baseOffset + movementOffset), list.size - getDisplayedEntries()) return min(max(0, xAxis.baseOffset + movementOffset), list.size - getDisplayedEntries())