From 2fe12d8788b7035927a063b8f5ddd31163e5d723 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 11 Jan 2021 21:04:08 +0100 Subject: [PATCH] EN-UI: Add popup to ID chart --- .../microg/gms/nearby/core/ui/DotChartView.kt | 126 ++++++++++++++++-- .../src/main/res/values-de/strings.xml | 2 +- .../src/main/res/values/strings.xml | 2 +- 3 files changed, 116 insertions(+), 14 deletions(-) diff --git a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartView.kt b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartView.kt index ab4882c5..9946dc3b 100644 --- a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartView.kt +++ b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartView.kt @@ -9,15 +9,18 @@ import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.Context import android.graphics.* +import android.os.Build import android.provider.Settings import android.text.TextUtils import android.util.AttributeSet +import android.view.MotionEvent import android.view.View import org.microg.gms.nearby.exposurenotification.ExposureScanSummary import org.microg.gms.ui.resolveColor import java.text.SimpleDateFormat import java.util.* import kotlin.math.max +import kotlin.math.min class DotChartView : View { @@ -63,10 +66,12 @@ class DotChartView : View { displayData[14]?.second?.set(hour, displayData[14]?.second?.get(hour) ?: -1) } this.displayData = displayData + this.displayDataList = displayData.values.map { it.second.values }.flatten().sorted() invalidate() } private var displayData: Map>> = emptyMap() + private var displayDataList: List = emptyList() private val drawPaint = Paint() private val drawTempRect = RectF() private val fontPaint = Paint() @@ -89,6 +94,9 @@ class DotChartView : View { } } + private fun List.relativePosition(element: T): Double? = indexOf(element).takeIf { it >= 0 }?.toDouble()?.let { it / size.toDouble() } + + private var focusPoint: PointF? = null override fun onDraw(canvas: Canvas) { if (data == null) data = emptySet() val d = resources.displayMetrics.scaledDensity @@ -114,20 +122,21 @@ class DotChartView : View { val perHeight = distHeight / 15.0 val perWidth = distWidth / 24.0 - val maxValue = displayData.values.mapNotNull { it.second.values.maxOrNull() }.maxOrNull() ?: 0 - val averageValue = (displayData.values.mapNotNull { it.second.values.average().takeIf { !it.isNaN() } }.average().takeIf { !it.isNaN() } - ?: 0.0).toInt() + val maxValue = displayDataList.last() val accentColor = context.resolveColor(androidx.appcompat.R.attr.colorAccent) ?: 0 val fontColor = context.resolveColor(android.R.attr.textColorSecondary) ?: 0 val grayBoxColor = fontColor or (255 shl 24) and (80 shl 24 or 0xffffff) fontPaint.textAlign = Paint.Align.RIGHT fontPaint.color = fontColor + var focusDay = -1 + var focusHour = -1 for (day in 0 until 15) { val (dateString, hours) = displayData[day] ?: "" to emptyMap() val top = day * (perHeight + innerPadding) + paddingTop if (day % 2 == 0) { canvas.drawText(dateString, (paddingLeft + legendLeft - 4 * d), (top + perHeight / 2.0 + maxTextHeight / 2.0).toFloat(), fontPaint) } + focusPoint?.let { if (it.y > top && it.y < top + perHeight) focusDay = day } for (hour in 0 until 24) { val value = hours[hour] val left = hour * (perWidth + innerPadding) + paddingLeft + legendLeft @@ -139,14 +148,18 @@ class DotChartView : View { canvas.drawMyRect(left.toFloat(), top.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor and 0xffffff) } value >= 0 -> { - val alpha = if (value < averageValue) { - value.toDouble() / averageValue.toDouble() * 127 - } else { - (value - averageValue).toDouble() / (maxValue - averageValue).toDouble() * 128 + 127 - }.toInt() + val byBucket = ((displayDataList.relativePosition(value) ?: 0.0) * 180.0).toInt() + val byMax = (value.toDouble() / maxValue.toDouble() * 80.0).toInt() + val alpha = max(0, min(byBucket + byMax, 255)) canvas.drawMyRect(left.toFloat(), top.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor and (alpha shl 24 or 0xffffff)) } } + if (focusDay == day && (value == null || value >= 0)) focusPoint?.let { + if (it.x > left && it.x < left + perWidth) { + focusHour = hour + canvas.drawMyRect(left.toFloat(), top.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor and 0xffffff) + } + } } } val legendTop = 15 * (perHeight + innerPadding) + paddingTop + maxTextHeight + 4 * d @@ -169,10 +182,99 @@ class DotChartView : View { canvas.drawText(strNoRecords, (subLeft + perWidth + 4 * d).toFloat(), (subTop + perHeight / 2.0 + fontTempRect.height() / 2.0).toFloat(), fontPaint) canvas.drawMyRect((subLeft + (perWidth + innerPadding) * 1 + 12 * d + fontTempRect.width()).toFloat(), subTop.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor and 0xffffff) - canvas.drawMyRect((subLeft + (perWidth + innerPadding) * 2 + 12 * d + fontTempRect.width()).toFloat(), subTop.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor and (128 shl 24 or 0xffffff)) - canvas.drawMyRect((subLeft + (perWidth + innerPadding) * 3 + 12 * d + fontTempRect.width()).toFloat(), subTop.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor) + canvas.drawMyRect((subLeft + (perWidth + innerPadding) * 2 + 12 * d + fontTempRect.width()).toFloat(), subTop.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor and (80 shl 24 or 0xffffff)) + canvas.drawMyRect((subLeft + (perWidth + innerPadding) * 3 + 12 * d + fontTempRect.width()).toFloat(), subTop.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor and (170 shl 24 or 0xffffff)) + canvas.drawMyRect((subLeft + (perWidth + innerPadding) * 4 + 12 * d + fontTempRect.width()).toFloat(), subTop.toFloat(), perWidth.toFloat(), perHeight.toFloat(), accentColor) - val strRecords = context.getString(R.string.pref_exposure_rpis_histogram_legend_records, 0, averageValue, maxValue) - canvas.drawText(strRecords, (subLeft + (perWidth + innerPadding) * 3 + 16 * d + fontTempRect.width() + perWidth).toFloat(), (subTop + perHeight / 2.0 + fontTempRect.height() / 2.0).toFloat(), fontPaint) + val strRecords = context.getString(R.string.pref_exposure_rpis_histogram_legend_records, "0 - $maxValue") + canvas.drawText(strRecords, (subLeft + (perWidth + innerPadding) * 4 + 16 * d + fontTempRect.width() + perWidth).toFloat(), (subTop + perHeight / 2.0 + fontTempRect.height() / 2.0).toFloat(), fontPaint) + + if (focusHour != -1 && Build.VERSION.SDK_INT >= 23) { + val floatingColor = context.resolveColor(androidx.appcompat.R.attr.colorBackgroundFloating) ?: 0 + val line1 = "${displayData[focusDay]?.first}, $focusHour:00" + val line2 = displayData[focusDay]?.second?.get(focusHour)?.let { context.getString(R.string.pref_exposure_rpis_histogram_legend_records, it.toString()) } + ?: strNoRecords + fontPaint.textSize = 14 * d + fontPaint.isFakeBoldText = true + fontPaint.getTextBounds(line1, 0, line1.length, fontTempRect) + var fontWidth = fontTempRect.width() + val line1Height = fontTempRect.height() + innerPadding + fontPaint.isFakeBoldText = false + fontPaint.getTextBounds(line2, 0, line2.length, fontTempRect) + fontWidth = max(fontWidth, fontTempRect.width()) + val totalHeight = line1Height + innerPadding + fontTempRect.height() + drawPaint.color = floatingColor + drawPaint.style = Paint.Style.FILL_AND_STROKE + val refTop = focusDay * (perHeight + innerPadding) + paddingTop + perHeight / 2 + val refLeft = focusHour * (perWidth + innerPadding) + paddingLeft + legendLeft + if (refLeft - fontWidth < 50 * d) { + // To the right + drawTempRect.set((refLeft + perWidth + innerPadding).toFloat(), (refTop - innerPadding - 5 * d - totalHeight / 2f).toFloat(), (refLeft + perWidth + innerPadding + 10 * d + fontWidth).toFloat(), (refTop + innerPadding + 5 * d + totalHeight / 2f).toFloat()) + } else { + // To the left + drawTempRect.set((refLeft - innerPadding - 10 * d - fontWidth).toFloat(), (refTop - innerPadding - 5 * d - totalHeight / 2f).toFloat(), (refLeft - innerPadding).toFloat(), (refTop + innerPadding + 5 * d + totalHeight / 2f).toFloat()) + } + canvas.drawRoundRect(drawTempRect, drawPaint.strokeWidth, drawPaint.strokeWidth, drawPaint) + val path = Path() + if (refLeft - fontWidth < 50 * d) { + // To the right + val off = refLeft + perWidth + innerPadding + drawPaint.strokeWidth + val corr = (perWidth / 2 - innerPadding + drawPaint.strokeWidth) + path.moveTo(off.toFloat(), (refTop - corr).toFloat()) + path.lineTo((refLeft + perWidth / 2).toFloat(), refTop.toFloat()) + path.lineTo(off.toFloat(), (refTop + corr).toFloat()) + } else { + // To the left + val off = refLeft - innerPadding - drawPaint.strokeWidth + val corr = (perWidth / 2 - innerPadding + drawPaint.strokeWidth) + path.moveTo(off.toFloat(), (refTop - corr).toFloat()) + path.lineTo((refLeft + perWidth / 2).toFloat(), refTop.toFloat()) + path.lineTo(off.toFloat(), (refTop + corr).toFloat()) + } + drawPaint.style = Paint.Style.STROKE + drawPaint.color = accentColor and (130 shl 24 or 0xffffff) + canvas.drawRoundRect(drawTempRect, drawPaint.strokeWidth, drawPaint.strokeWidth, drawPaint) + + drawPaint.color = floatingColor + drawPaint.style = Paint.Style.FILL_AND_STROKE + path.fillType = Path.FillType.EVEN_ODD + canvas.drawPath(path, drawPaint) + drawPaint.style = Paint.Style.STROKE + drawPaint.color = accentColor and (130 shl 24 or 0xffffff) + path.fillType = Path.FillType.EVEN_ODD + canvas.drawPath(path, drawPaint) + + canvas.drawText(line2, drawTempRect.left + 5 * d, drawTempRect.top + 5 * d + totalHeight, fontPaint) + fontPaint.isFakeBoldText = true + canvas.drawText(line1, drawTempRect.left + 5 * d, drawTempRect.top + 5 * d + line1Height, fontPaint) + fontPaint.isFakeBoldText = false + } + } + + private val removeFocusPoint = Runnable { unfocus() } + + fun unfocus() { + focusPoint = null + invalidate() + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.actionMasked) { + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> { + postDelayed(removeFocusPoint, POPUP_DELAY) + return true + } + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_MOVE -> { + removeCallbacks(removeFocusPoint) + focusPoint = PointF(event.x, event.y) + invalidate() + return true + } + } + return false + } + + companion object { + const val POPUP_DELAY = 3000L } } diff --git a/play-services-nearby-core-ui/src/main/res/values-de/strings.xml b/play-services-nearby-core-ui/src/main/res/values-de/strings.xml index cb705e43..f53eb12f 100644 --- a/play-services-nearby-core-ui/src/main/res/values-de/strings.xml +++ b/play-services-nearby-core-ui/src/main/res/values-de/strings.xml @@ -31,7 +31,7 @@ %1$d Aufrufe von %2$s %1$d gesammelte IDs Keine Daten - %1$d / %2$d / %3$d IDs pro Stunde + %1$s IDs pro Stunde Löschen Alle gesammelten IDs löschen Nach dem Löschen der gesammelten IDs kannst du nicht mehr informiert werden, falls einer deiner Kontakte der letzten 14 Tage positiv getested wurde. diff --git a/play-services-nearby-core-ui/src/main/res/values/strings.xml b/play-services-nearby-core-ui/src/main/res/values/strings.xml index e639afd3..ddf2dfcc 100644 --- a/play-services-nearby-core-ui/src/main/res/values/strings.xml +++ b/play-services-nearby-core-ui/src/main/res/values/strings.xml @@ -41,7 +41,7 @@ %1$d calls to %2$s %1$d IDs collected No records - %1$d / %2$d / %3$d IDs per hour + %1$s IDs per hour Delete Delete all collected IDs Deleting collected IDs will make it impossible to notify you in case any of your contacts of the last 14 days is diagnosed.