Yuito-app-android/app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt

333 lines
9.0 KiB
Kotlin

/* Copyright 2023 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PathMeasure
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.Dimension
import androidx.core.content.res.use
import com.keylesspalace.tusky.R
import kotlin.math.max
class GraphView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
@get:ColorInt
@ColorInt
var primaryLineColor = 0
@get:ColorInt
@ColorInt
var secondaryLineColor = 0
@get:Dimension
var lineWidth = 0f
@get:ColorInt
@ColorInt
var graphColor = 0
@get:ColorInt
@ColorInt
var metaColor = 0
private var proportionalTrending = false
private lateinit var primaryLinePaint: Paint
private lateinit var secondaryLinePaint: Paint
private lateinit var primaryCirclePaint: Paint
private lateinit var secondaryCirclePaint: Paint
private lateinit var graphPaint: Paint
private lateinit var metaPaint: Paint
private lateinit var sizeRect: Rect
private var primaryLinePath: Path = Path()
private var secondaryLinePath: Path = Path()
var maxTrendingValue: Long = 300
var primaryLineData: List<Long> = if (isInEditMode) {
listOf(
30,
60,
70,
80,
130,
190,
80
)
} else {
listOf(
1,
1,
1,
1,
1,
1,
1
)
}
set(value) {
field = value.map { max(1, it) }
primaryLinePath.reset()
invalidate()
}
var secondaryLineData: List<Long> = if (isInEditMode) {
listOf(
10,
20,
40,
60,
100,
132,
20
)
} else {
listOf(
1,
1,
1,
1,
1,
1,
1
)
}
set(value) {
field = value.map { max(1, it) }
secondaryLinePath.reset()
invalidate()
}
init {
initFromXML(attrs)
}
private fun initFromXML(attr: AttributeSet?) {
context.obtainStyledAttributes(attr, R.styleable.GraphView).use { a ->
primaryLineColor = context.getColor(
a.getResourceId(
R.styleable.GraphView_primaryLineColor,
R.color.tusky_blue
)
)
secondaryLineColor = context.getColor(
a.getResourceId(
R.styleable.GraphView_secondaryLineColor,
R.color.tusky_red
)
)
lineWidth = a.getDimensionPixelSize(
R.styleable.GraphView_lineWidth,
R.dimen.graph_line_thickness
).toFloat()
graphColor = context.getColor(
a.getResourceId(
R.styleable.GraphView_graphColor,
R.color.colorBackground
)
)
metaColor = context.getColor(
a.getResourceId(
R.styleable.GraphView_metaColor,
R.color.dividerColor
)
)
proportionalTrending = a.getBoolean(
R.styleable.GraphView_proportionalTrending,
proportionalTrending
)
}
primaryLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = primaryLineColor
strokeWidth = lineWidth
style = Paint.Style.STROKE
}
primaryCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = primaryLineColor
style = Paint.Style.FILL
}
secondaryLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = secondaryLineColor
strokeWidth = lineWidth
style = Paint.Style.STROKE
}
secondaryCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = secondaryLineColor
style = Paint.Style.FILL
}
graphPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = graphColor
}
metaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = metaColor
strokeWidth = 0f
style = Paint.Style.STROKE
}
}
private fun initializeVertices() {
sizeRect = Rect(0, 0, width, height)
initLine(primaryLineData, primaryLinePath)
initLine(secondaryLineData, secondaryLinePath)
}
private fun initLine(lineData: List<Long>, path: Path) {
val max = if (proportionalTrending) {
maxTrendingValue
} else {
max(primaryLineData.max(), 1)
}
val mainRatio = height.toFloat() / max.toFloat()
val ratioedData = lineData.map { it.toFloat() * mainRatio }
val pointDistance = dataSpacing(ratioedData)
/** X coord of the start of this path segment */
var startX = 0F
/** Y coord of the start of this path segment */
var startY = 0F
/** X coord of the end of this path segment */
var endX: Float
/** Y coord of the end of this path segment */
var endY: Float
/** X coord of bezier control point #1 */
var controlX1: Float
/** X coord of bezier control point #2 */
var controlX2: Float
// Draw cubic bezier curves between each pair of points.
ratioedData.forEachIndexed { index, magnitude ->
val x = pointDistance * index.toFloat()
val y = height.toFloat() - magnitude
if (index == 0) {
path.reset()
path.moveTo(x, y)
startX = x
startY = y
} else {
endX = x
endY = y
// X-coord for a control point is placed one third of the distance between the
// two points.
val offsetX = (endX - startX) / 3
controlX1 = startX + offsetX
controlX2 = endX - offsetX
path.cubicTo(controlX1, startY, controlX2, endY, x, y)
startX = x
startY = y
}
}
}
private fun dataSpacing(data: List<Any>) = width.toFloat() / max(data.size - 1, 1).toFloat()
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (primaryLinePath.isEmpty && width > 0) {
initializeVertices()
}
canvas?.apply {
drawRect(sizeRect, graphPaint)
val pointDistance = dataSpacing(primaryLineData)
// Vertical tick marks
for (i in 0 until primaryLineData.size + 1) {
drawLine(
i * pointDistance,
height.toFloat(),
i * pointDistance,
height - (height.toFloat() / 20),
metaPaint
)
}
// X-axis
drawLine(0f, height.toFloat(), width.toFloat(), height.toFloat(), metaPaint)
// Data lines
drawLine(
canvas = canvas,
linePath = secondaryLinePath,
linePaint = secondaryLinePaint,
circlePaint = secondaryCirclePaint,
lineThickness = lineWidth
)
drawLine(
canvas = canvas,
linePath = primaryLinePath,
linePaint = primaryLinePaint,
circlePaint = primaryCirclePaint,
lineThickness = lineWidth
)
}
}
private fun drawLine(
canvas: Canvas,
linePath: Path,
linePaint: Paint,
circlePaint: Paint,
lineThickness: Float
) {
canvas.apply {
drawPath(
linePath,
linePaint
)
val pm = PathMeasure(linePath, false)
val coord = floatArrayOf(0f, 0f)
pm.getPosTan(pm.length * 1f, coord, null)
drawCircle(coord[0], coord[1], lineThickness * 2f, circlePaint)
}
}
}