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

fix: Value higher than max being slow

WIP: Better scrolling
This commit is contained in:
Florian Bouillon 2022-07-26 23:32:37 +02:00
parent 3aa0861b1f
commit 4bec653f75
Signed by: Florian Bouillon
GPG Key ID: 0A288052C94BD2C8
10 changed files with 121 additions and 56 deletions

View File

@ -1,11 +1,15 @@
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.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.charts.Entry import com.dzeio.charts.Entry
import com.dzeio.openhealth.Application
import com.dzeio.openhealth.R
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
@ -19,6 +23,10 @@ import java.util.Locale
class StepsHomeFragment : class StepsHomeFragment :
BaseFragment<StepsHomeViewModel, FragmentStepsHomeBinding>(StepsHomeViewModel::class.java) { BaseFragment<StepsHomeViewModel, FragmentStepsHomeBinding>(StepsHomeViewModel::class.java) {
companion object {
const val TAG = "${Application.TAG}/SHFragment"
}
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentStepsHomeBinding = override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentStepsHomeBinding =
FragmentStepsHomeBinding::inflate FragmentStepsHomeBinding::inflate
@ -46,7 +54,7 @@ class StepsHomeFragment :
viewModel.items.observe(viewLifecycleOwner) { list -> viewModel.items.observe(viewLifecycleOwner) { list ->
adapter.set(list) adapter.set(list)
// chart.numberOfEntries = list.size / 2 chart.xAxis.entriesDisplayed = 30
chart.numberOfLabels = 2 chart.numberOfLabels = 2
// chart.animation.enabled = false // chart.animation.enabled = false
@ -80,5 +88,21 @@ 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

@ -35,6 +35,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/scrollView"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">

View File

@ -1,6 +1,7 @@
package com.dzeio.charts package com.dzeio.charts
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max
data class Animation( data class Animation(
/** /**
@ -27,25 +28,39 @@ data class Animation(
* *
* @return the new updated value * @return the new updated value
*/ */
fun updateValue(maxValue: Float, targetValue: Float, currentValue: Float): Float { fun updateValue(
maxValue: Float,
targetValue: Float,
currentValue: Float,
minValue: Float,
minStep: Float
): Float {
if (!enabled) { if (!enabled) {
return targetValue return targetValue
} }
val moveValue = (maxValue - targetValue) / refreshRate if (currentValue < minValue) {
return minValue
}
val moveValue = max(minStep, (maxValue - targetValue) / refreshRate)
var result = targetValue var result = targetValue
if (currentValue < targetValue) { if (currentValue < targetValue) {
result = currentValue + moveValue result = currentValue + moveValue
} else if (currentValue > targetValue) { } else if (currentValue > targetValue) {
result = currentValue - moveValue result = currentValue - moveValue
} }
if (abs(targetValue - currentValue) < moveValue) { if (
abs(targetValue - currentValue) <= moveValue ||
result < minValue ||
result > maxValue
) {
return targetValue return targetValue
} }
return result return result
} }
fun getDelay() = this.duration / this.refreshRate fun getDelay() = this.duration / this.refreshRate
} }

View File

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

View File

@ -19,4 +19,4 @@ class XAxisLabels {
it.textAlign = Paint.Align.CENTER it.textAlign = Paint.Align.CENTER
} }
} }
} }

View File

@ -16,13 +16,10 @@ class XAxis<T> {
/** /**
* Offset in the list * Offset in the list
*
* WILL CHANGE AS SOON AS SCROLLING IS AVAILABLE
*/ */
var baseOffset = 0 var baseOffset = 0
var onValueFormat: (it: T) -> String = onValueFormat@{ var onValueFormat: (it: T) -> String = onValueFormat@{
return@onValueFormat it.toString() return@onValueFormat it.toString()
} }
}
}

View File

@ -9,4 +9,4 @@ class YAxis<T> {
@ColorInt @ColorInt
var color = Color.parseColor("#FC496D") var color = Color.parseColor("#FC496D")
} }

View File

@ -1,4 +0,0 @@
package com.dzeio.charts.builders
class ChartBuilder {
}

View File

@ -22,6 +22,8 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
const val TAG = "DzeioCharts/BarView" const val TAG = "DzeioCharts/BarView"
} }
var debug: Boolean = false
/** /**
* Number of entries displayed at the same time * Number of entries displayed at the same time
*/ */
@ -47,7 +49,7 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
private var barWidth: Int = 0 private var barWidth: Int = 0
private val percentList: ArrayList<Float> = ArrayList() private var percentList: ArrayList<Float> = ArrayList()
/** /**
* value goes from 1 to 0 (1 at bottom, 0 at top) * value goes from 1 to 0 (1 at bottom, 0 at top)
@ -68,10 +70,24 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
private val animator: Runnable = object : Runnable { private val animator: Runnable = object : Runnable {
override fun run() { override fun run() {
var needNewFrame = false var needNewFrame = false
// var txt = ""
// for (tpl in targetPercentList) {
// txt += "$tpl, "
// }
// Log.d(TAG, txt)
for (i in targetPercentList.indices) { for (i in targetPercentList.indices) {
val value = animation.updateValue(1f, targetPercentList[i], percentList[i]) val value = animation.updateValue(
1f,
targetPercentList[i],
percentList[i],
0f,
0.01f
)
if (value != percentList[i]) { if (value != percentList[i]) {
// if (!needNewFrame) {
// Log.d(TAG, "$i, $value, ${percentList[i]}")
// }
needNewFrame = true needNewFrame = true
percentList[i] = value percentList[i] = value
} }
@ -87,27 +103,17 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
private var bottomTexts: ArrayList<String> = arrayListOf() private var bottomTexts: ArrayList<String> = arrayListOf()
// init { var previousRefresh = movementOffset
// val mockList = ArrayList<Entry>()
// for (i in 0 until 25) {
// mockList.add(Entry(i.toDouble(), i.toFloat()))
// }
//
// list = mockList
//
// this.refresh()
// }
fun refresh() { fun refresh() {
val r = Rect() val r = Rect()
//// prepare bottom texts // // prepare bottom texts
bottomTextDescent = 0 bottomTextDescent = 0
bottomTextHeight = 0 bottomTextHeight = 0
bottomTexts = arrayListOf() bottomTexts = arrayListOf()
//// prepare values // // prepare values
// set the bar Width (also handle div by 0) // set the bar Width (also handle div by 0)
barWidth = measuredWidth / max(min(list.size, getDisplayedEntries()), 1) - spacing barWidth = measuredWidth / max(min(list.size, getDisplayedEntries()), 1) - spacing
@ -115,19 +121,21 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
// calculate max depending on the maximum value displayed or set in the yAxis params // calculate max depending on the maximum value displayed or set in the yAxis params
val max: Float = if (yAxis.max != null) yAxis.max!! else { val max: Float = if (yAxis.max != null) yAxis.max!! else {
var calculatedMax = 0f var calculatedMax = 0f
for (entry in list.subList(this.getXOffset(), getDisplayedEntries() + this.getXOffset())) { for (entry in list.subList(
this.getXOffset(),
getDisplayedEntries() + this.getXOffset()
)) {
if (entry.y > calculatedMax) calculatedMax = entry.y if (entry.y > calculatedMax) calculatedMax = entry.y
} }
calculatedMax if (calculatedMax < 0) 0f else calculatedMax
} }
// make sure the target list // make sure the target list
// Log.d(TAG, list.size.toString()) // Log.d(TAG, list.size.toString())
targetPercentList = arrayListOf() targetPercentList = arrayListOf()
for (item in list.subList(getXOffset(), getXOffset() + getDisplayedEntries())) {
for ((i, item) in list.withIndex()) { // // Process bottom texts
//// Process bottom texts
val text = xAxis.onValueFormat(item.x) val text = xAxis.onValueFormat(item.x)
bottomTexts.add(text) bottomTexts.add(text)
@ -145,13 +153,23 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
bottomTextDescent = descent bottomTextDescent = descent
} }
//// process values // // process values
// add to animations the values // add to animations the values
targetPercentList.add(min(1 - item.y / max, 1f)) targetPercentList.add(1 - item.y / max)
} }
// post list // post list
val movement = movementOffset - previousRefresh
previousRefresh = movementOffset
// 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) { 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) {
@ -165,7 +183,6 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
} }
// Misc operations // Misc operations
fgPaint = Paint().apply { fgPaint = Paint().apply {
isAntiAlias = true isAntiAlias = true
color = yAxis.color color = yAxis.color
@ -178,18 +195,26 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (percentList.isNotEmpty()) { if (percentList.isNotEmpty()) {
// draw each rectangles // draw each rectangles
for (i in 1..getDisplayedEntries()) { for (i in 1..percentList.size) {
// Log.d(TAG, percentList[i - 1].toString()) // Log.d(TAG, percentList[i - 1].toString())
val left = spacing * i + barWidth * (i - 1).toFloat() val left = spacing * i + barWidth * (i - 1).toFloat()
// Log.d(TAG, "$spacing, $i, $barWidth = $left") // Log.d(TAG, "$spacing, $i, $barWidth = $left")
val right = (spacing + barWidth) * i.toFloat() val right = (spacing + barWidth) * i.toFloat()
val bottom = height - bottomTextHeight - textTopMargin.toFloat() val bottom = height - bottomTextHeight - textTopMargin.toFloat()
val top = bottom * percentList[this.getXOffset() + i - 1] val top = bottom * percentList[i - 1]
// create rounded rect // create rounded rect
canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, fgPaint) canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, fgPaint)
// remove the bottom corners DUH // remove the bottom corners DUH
canvas.drawRect(left, max(top, bottom - 8f), right, bottom, fgPaint) canvas.drawRect(left, max(top, bottom - 8f), right, bottom, fgPaint)
if (debug) {
canvas.drawText(
bottomTexts[i - 1].toString(),
left + (right - left) / 2,
top + (bottom - top) / 2,
xAxis.labels.build()
)
}
} }
} }
if (bottomTexts.isNotEmpty() && numberOfLabels > 0) { if (bottomTexts.isNotEmpty() && numberOfLabels > 0) {
@ -206,7 +231,6 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
// Log.i(TAG, "$size / max($numberOfLabels - 2, 2) = $items") // Log.i(TAG, "$size / max($numberOfLabels - 2, 2) = $items")
for (s in bottomTexts) { for (s in bottomTexts) {
if ((numberOfLabels <= 2 || i % items != 0) && i != 1 && i != size) { if ((numberOfLabels <= 2 || i % items != 0) && i != 1 && i != size) {
i++ i++
continue continue
@ -246,17 +270,16 @@ class BarChartView @JvmOverloads constructor(context: Context?, attrs: Attribute
override fun onZoomChanged(scale: Float) { override fun onZoomChanged(scale: Float) {
Log.d(TAG, "New Zoom: $scale") Log.d(TAG, "New Zoom: $scale")
zoom = scale zoom = (scale * 1.2).toFloat()
refresh() refresh()
} }
private fun getXOffset(): Int { private fun getXOffset(): Int {
return min(max(0, xAxis.baseOffset + movementOffset), list.size - 1 - getDisplayedEntries()) return min(max(0, xAxis.baseOffset + movementOffset), list.size - getDisplayedEntries())
} }
private fun getDisplayedEntries(): Int { private fun getDisplayedEntries(): Int {
// Log.d(TAG, "Number of entries displayed ${list.size}, ${xAxis.entriesDisplayed} + (($zoom - 100) * 10) = ${xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()}") // 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())) return max(1, min(list.size, xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()))
} }
} }

View File

@ -6,7 +6,6 @@ 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
import androidx.core.view.MotionEventCompat
abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) { View(context, attrs) {
@ -37,7 +36,6 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
context, 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(lastZoom + -currentZoom + 1)
@ -51,7 +49,8 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
lastZoom += -currentZoom + 1 lastZoom += -currentZoom + 1
} }
}) }
)
/** /**
* Code mostly stolen from https://developer.android.com/training/gestures/scale#drag * Code mostly stolen from https://developer.android.com/training/gestures/scale#drag
@ -63,6 +62,7 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
when (ev.actionMasked) { when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
onToggleScroll?.invoke(false)
ev.actionIndex.also { pointerIndex -> ev.actionIndex.also { pointerIndex ->
// Remember where we started (for dragging) // Remember where we started (for dragging)
lastTouchX = ev.getX(pointerIndex) lastTouchX = ev.getX(pointerIndex)
@ -78,8 +78,7 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
val (x: Float, y: Float) = val (x: Float, y: Float) =
ev.findPointerIndex(activePointerId).let { pointerIndex -> ev.findPointerIndex(activePointerId).let { pointerIndex ->
// Calculate the distance moved // Calculate the distance moved
ev.getX(pointerIndex) to ev.getX(pointerIndex) to ev.getY(pointerIndex)
ev.getY(pointerIndex)
} }
posX += x - lastTouchX posX += x - lastTouchX
@ -92,10 +91,11 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
lastTouchY = y lastTouchY = y
} }
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
onToggleScroll?.invoke(true)
activePointerId = INVALID_POINTER_ID activePointerId = INVALID_POINTER_ID
} }
MotionEvent.ACTION_POINTER_UP -> { MotionEvent.ACTION_POINTER_UP -> {
onToggleScroll?.invoke(true)
ev.actionIndex.also { pointerIndex -> ev.actionIndex.also { pointerIndex ->
ev.getPointerId(pointerIndex) ev.getPointerId(pointerIndex)
.takeIf { it == activePointerId } .takeIf { it == activePointerId }
@ -103,13 +103,22 @@ abstract class BaseChart @JvmOverloads constructor(context: Context?, attrs: Att
// This was our active pointer going up. Choose a new // This was our active pointer going up. Choose a new
// active pointer and adjust accordingly. // active pointer and adjust accordingly.
val newPointerIndex = if (pointerIndex == 0) 1 else 0 val newPointerIndex = if (pointerIndex == 0) 1 else 0
lastTouchX = MotionEventCompat.getX(ev, newPointerIndex) lastTouchX = ev.getX(newPointerIndex)
lastTouchY = MotionEventCompat.getY(ev, newPointerIndex) lastTouchY = ev.getY(newPointerIndex)
activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex) activePointerId = ev.getPointerId(newPointerIndex)
} }
} }
} }
} }
return true return true
} }
}
private var onToggleScroll: ((Boolean) -> Unit)? = null
/**
* @param ev if input is false disable scroll
*/
fun setOnToggleScroll(ev: (Boolean) -> Unit) {
onToggleScroll = ev
}
}