mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-23 03:12:14 +00:00
feat(charts): Moved to modules based charts
Signed-off-by: Avior <github@avior.me>
This commit is contained in:
parent
c2bc9eced2
commit
f6b5715572
@ -1,13 +1,13 @@
|
||||
package com.dzeio.openhealth.ui.steps
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.dzeio.charts.Entry
|
||||
import com.dzeio.charts.series.BarSerie
|
||||
import com.dzeio.openhealth.Application
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.adapters.StepsAdapter
|
||||
@ -52,10 +52,17 @@ class StepsHomeFragment :
|
||||
|
||||
val chart = binding.chart
|
||||
|
||||
val serie = BarSerie()
|
||||
|
||||
chart.series = arrayListOf(serie)
|
||||
|
||||
viewModel.items.observe(viewLifecycleOwner) { 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.refreshRate = 60
|
||||
@ -71,7 +78,7 @@ class StepsHomeFragment :
|
||||
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())
|
||||
} as ArrayList<Entry>
|
||||
|
||||
@ -88,21 +95,5 @@ class StepsHomeFragment :
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<view
|
||||
class="com.dzeio.charts.views.BarChartView"
|
||||
class="com.dzeio.charts.ChartView"
|
||||
android:id="@+id/chart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
|
152
charts/src/main/java/com/dzeio/charts/ChartView.kt
Normal file
152
charts/src/main/java/com/dzeio/charts/ChartView.kt
Normal 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
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package com.dzeio.charts.axis
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import androidx.annotation.ColorInt
|
||||
|
||||
class YAxis<T> {
|
||||
@ -9,4 +11,20 @@ class YAxis<T> {
|
||||
|
||||
@ColorInt
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -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.INVALID_POINTER_ID
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
|
||||
abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
|
||||
View(context, attrs) {
|
||||
class ChartScroll(view: View) {
|
||||
|
||||
// The ‘active pointer’ is the one currently moving our object.
|
||||
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 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,
|
||||
* 101+% zoom in
|
||||
*/
|
||||
open fun onZoomChanged(scale: Float) {}
|
||||
fun setOnZoomChanged(fn: (scale: Float) -> Unit) {
|
||||
onZoomChanged = fn
|
||||
}
|
||||
|
||||
private val scaleGestureDetector = ScaleGestureDetector(
|
||||
context,
|
||||
view.context,
|
||||
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
if (currentZoom != detector.scaleFactor) {
|
||||
currentZoom = detector.scaleFactor
|
||||
onZoomChanged(lastZoom + -currentZoom + 1)
|
||||
onZoomChanged?.invoke(lastZoom + -currentZoom + 1)
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
super.onTouchEvent(ev)
|
||||
|
||||
fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
scaleGestureDetector.onTouchEvent(ev)
|
||||
|
||||
when (ev.actionMasked) {
|
||||
@ -84,7 +86,7 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
|
||||
posX += x - lastTouchX
|
||||
posY += y - lastTouchY
|
||||
|
||||
onChartMoved(-posX, posY)
|
||||
onChartMoved?.invoke(-posX, posY)
|
||||
|
||||
// Remember this touch position for the next move event
|
||||
lastTouchX = x
|
15
charts/src/main/java/com/dzeio/charts/components/Sidebar.kt
Normal file
15
charts/src/main/java/com/dzeio/charts/components/Sidebar.kt
Normal 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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
154
charts/src/main/java/com/dzeio/charts/series/BarSerie.kt
Normal file
154
charts/src/main/java/com/dzeio/charts/series/BarSerie.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -6,7 +6,7 @@ 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
|
||||
@ -16,7 +16,7 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class BarChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
|
||||
BaseChart(context, attrs) {
|
||||
View(context, attrs) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "DzeioCharts/BarView"
|
||||
@ -263,16 +263,16 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChartMoved(movementX: Float, movementY: Float) {
|
||||
movementOffset = (movementX / 100).toInt()
|
||||
refresh()
|
||||
}
|
||||
// override fun onChartMoved(movementX: Float, movementY: Float) {
|
||||
// movementOffset = (movementX / 100).toInt()
|
||||
// refresh()
|
||||
// }
|
||||
|
||||
override fun onZoomChanged(scale: Float) {
|
||||
Log.d(TAG, "New Zoom: $scale")
|
||||
zoom = (scale * 1.2).toFloat()
|
||||
refresh()
|
||||
}
|
||||
// override fun onZoomChanged(scale: Float) {
|
||||
// Log.d(TAG, "New Zoom: $scale")
|
||||
// zoom = (scale * 1.2).toFloat()
|
||||
// refresh()
|
||||
// }
|
||||
|
||||
private fun getXOffset(): Int {
|
||||
return min(max(0, xAxis.baseOffset + movementOffset), list.size - getDisplayedEntries())
|
||||
|
Loading…
x
Reference in New Issue
Block a user