diff --git a/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt index 2d3e59a..63b4b7e 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt @@ -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 @@ -88,21 +95,5 @@ class StepsHomeFragment : chart.refresh() } - - val scrollView = requireActivity().findViewById(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 - } } } diff --git a/app/src/main/res/layout/fragment_steps_home.xml b/app/src/main/res/layout/fragment_steps_home.xml index 5061165..79dc7fd 100644 --- a/app/src/main/res/layout/fragment_steps_home.xml +++ b/app/src/main/res/layout/fragment_steps_home.xml @@ -22,7 +22,7 @@ android:orientation="vertical"> () + + val yAxis = YAxis() + + 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 = 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 + } +} diff --git a/charts/src/main/java/com/dzeio/charts/axis/YAxis.kt b/charts/src/main/java/com/dzeio/charts/axis/YAxis.kt index 1d7ec06..a701a5d 100644 --- a/charts/src/main/java/com/dzeio/charts/axis/YAxis.kt +++ b/charts/src/main/java/com/dzeio/charts/axis/YAxis.kt @@ -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 { @@ -9,4 +11,20 @@ class YAxis { @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() + } } diff --git a/charts/src/main/java/com/dzeio/charts/views/BaseChart.kt b/charts/src/main/java/com/dzeio/charts/components/ChartScroll.kt similarity index 84% rename from charts/src/main/java/com/dzeio/charts/views/BaseChart.kt rename to charts/src/main/java/com/dzeio/charts/components/ChartScroll.kt index 6a77faa..378b6ba 100644 --- a/charts/src/main/java/com/dzeio/charts/views/BaseChart.kt +++ b/charts/src/main/java/com/dzeio/charts/components/ChartScroll.kt @@ -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 diff --git a/charts/src/main/java/com/dzeio/charts/components/Sidebar.kt b/charts/src/main/java/com/dzeio/charts/components/Sidebar.kt new file mode 100644 index 0000000..c663e88 --- /dev/null +++ b/charts/src/main/java/com/dzeio/charts/components/Sidebar.kt @@ -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) { + + } + +} diff --git a/charts/src/main/java/com/dzeio/charts/series/BarSerie.kt b/charts/src/main/java/com/dzeio/charts/series/BarSerie.kt new file mode 100644 index 0000000..40d3c2c --- /dev/null +++ b/charts/src/main/java/com/dzeio/charts/series/BarSerie.kt @@ -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() + var percentList = arrayListOf() + + 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 + } +} diff --git a/charts/src/main/java/com/dzeio/charts/series/SerieAbstract.kt b/charts/src/main/java/com/dzeio/charts/series/SerieAbstract.kt new file mode 100644 index 0000000..302730a --- /dev/null +++ b/charts/src/main/java/com/dzeio/charts/series/SerieAbstract.kt @@ -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 = 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) +} diff --git a/charts/src/main/java/com/dzeio/charts/views/BarChartView.kt b/charts/src/main/java/com/dzeio/charts/views/BarChartView.kt index c9a9bc5..72f9261 100644 --- a/charts/src/main/java/com/dzeio/charts/views/BarChartView.kt +++ b/charts/src/main/java/com/dzeio/charts/views/BarChartView.kt @@ -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())