Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'org.jetbrains.kotlin.plugin.serialization'
}

android {
Expand All @@ -24,11 +25,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
}
}

Expand All @@ -38,6 +39,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
Expand Down
190 changes: 190 additions & 0 deletions app/src/main/java/otus/homework/customview/LineChartView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package otus.homework.customview

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.CornerPathEffect
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import java.text.SimpleDateFormat
import java.util.Date

class LineChartView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
private var height = 0f
private var width = 0f

private var mapChartData: MutableList<Map<String, Int>> = mutableListOf()
private var timestampList = listOf<String>()

private var minAmount = 0
private var maxAmount = 0
private val offsetEnd = 50f
private var offsetStart = 50f
private var offsetBottom = 0f
private var offsetTop = 50f
private var widthTime = 0f
private var heightValue = 0f
private var eachTimeN = 1

private val colors = listOf(
Color.argb(255, 246, 156, 156),
Color.argb(255, 246, 198, 156),
Color.argb(255, 246, 236, 156),
Color.argb(255, 207, 246, 156),
Color.argb(255, 156, 246, 161),
Color.argb(255, 156, 246, 205),
Color.argb(255, 156, 241, 246),
Color.argb(255, 156, 185, 246),
Color.argb(255, 196, 156, 246),
Color.argb(255, 246, 156, 225),
)

private val paint = Paint().apply {
strokeWidth = 8f
style = Paint.Style.STROKE
pathEffect = CornerPathEffect(30f)
}

private val paintBackgroundBorder = Paint().apply {
color = Color.GRAY
strokeWidth = 4f
style = Paint.Style.STROKE
}

private val paintBackgroundLine = Paint().apply {
color = Color.GRAY
strokeWidth = 2f
style = Paint.Style.STROKE
}

private val textPaint = Paint().apply {
color = Color.BLACK
textSize = 20f
}

private val path = Path()

fun setData(data: List<DataItem>) {
if (data.isEmpty()) return

val mapChartCategory = data.groupBy { item -> item.category }
timestampList = data.groupBy { item -> getDate(item.time) }.keys.toList()
mapChartData.clear()

mapChartCategory.values.forEach { dataItem ->
mapChartData.add(dataItem.groupBy { item -> getDate(item.time) }
.mapValues { list -> list.value.sumOf { it.amount } })
}

maxAmount = data.maxOf { it.amount }
minAmount = data.minOf { it.amount }

if (maxAmount == minAmount) minAmount = 0

val rectHorizontal = measureTextSize(maxAmount.toString(), textPaint.textSize)
offsetStart = rectHorizontal.width().toFloat()
heightValue = rectHorizontal.height().toFloat()

val rectVertical = measureTextSize(timestampList[0], textPaint.textSize)
offsetBottom = rectVertical.height().toFloat() + 20
widthTime = rectVertical.width().toFloat()

invalidate()
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
height = h - offsetBottom
width = w - offsetStart - offsetEnd
}

override fun onDraw(canvas: Canvas) {
val heightStep = (height - offsetTop) / (maxAmount - minAmount)
val widthStep = if (timestampList.size > 1) width / (timestampList.size - 1) else width

if (widthTime >= widthStep) {
eachTimeN = (widthTime / widthStep).toInt() + 1
}

canvas.drawLine(
0f,
0f,
0f,
height,
paintBackgroundBorder
)
canvas.drawLine(
0f,
height,
measuredWidth.toFloat(),
height,
paintBackgroundBorder
)

mapChartData.forEachIndexed { index, maps ->
paint.color = colors[index]
path.reset()

maps.onEachIndexed { indexMap, map ->
val x = (timestampList.indexOf(map.key) * widthStep) + offsetStart
if (indexMap == 0) path.moveTo(x, height)
val y = height - ((map.value - minAmount) * heightStep)
path.lineTo(x, y)

canvas.drawLine(offsetStart, y, measuredWidth.toFloat(), y, paintBackgroundLine)
canvas.drawLine(x, 0f, x, height, paintBackgroundLine)

canvas.drawText(map.value.toString(), x + 5, y - 5, textPaint)

if (timestampList.indexOf(map.key) % eachTimeN == 0) {
canvas.drawText(map.key, x - offsetStart, measuredHeight.toFloat(), textPaint)
}
}

canvas.drawPath(path, paint)
}

}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)

val width = when (widthMode) {
MeasureSpec.EXACTLY -> maxOf(minimumWidth, widthSize)
MeasureSpec.AT_MOST -> maxOf(minimumWidth, widthSize)
else -> minimumWidth
}

val height = when (heightMode) {
MeasureSpec.EXACTLY -> maxOf(minimumHeight, heightSize)
MeasureSpec.AT_MOST -> maxOf(minimumHeight, heightSize)
else -> minimumHeight
}

setMeasuredDimension(width, height)
}

private fun measureTextSize(text: String, textSize: Float): Rect {
val paint = Paint().apply {
this.textSize = textSize
}

val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)

return bounds
}

@SuppressLint("SimpleDateFormat")
private fun getDate(timestamp: Long): String {
val formatDate = SimpleDateFormat("dd.MM HH")
val currentDate = Date(timestamp * 1000)
return "${formatDate.format(currentDate)} ч"
}
}
10 changes: 9 additions & 1 deletion app/src/main/java/otus/homework/customview/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package otus.homework.customview

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// val pieChartView: PieChartView = findViewById(R.id.pieChart)
// pieChartView.onClickListener = {
// Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
// }

findViewById<LineChartView>(R.id.LineChartView).setData(getDataItemList(this)!!)
}
}
145 changes: 145 additions & 0 deletions app/src/main/java/otus/homework/customview/PieChartView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package otus.homework.customview

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.math.atan2
import kotlin.math.min

class PieChartView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val dataItemList = getDataItemList(context)
private val categoryMap = mutableMapOf<String, Int>()

private var categories = mutableListOf<String>()
private var amounts = mutableListOf<Int>()

private val paint = Paint().apply {
isAntiAlias = true
}

var onClickListener: ((String) -> Unit)? = null

private val colors = listOf(
Color.argb(255, 246, 156, 156),
Color.argb(255, 246, 198, 156),
Color.argb(255, 246, 236, 156),
Color.argb(255, 207, 246, 156),
Color.argb(255, 156, 246, 161),
Color.argb(255, 156, 246, 205),
Color.argb(255, 156, 241, 246),
Color.argb(255, 156, 185, 246),
Color.argb(255, 196, 156, 246),
Color.argb(255, 246, 156, 225),
)

init {
dataItemList?.forEach { dataItem ->
val currentAmount = categoryMap[dataItem.category] ?: 0
categoryMap[dataItem.category] = currentAmount + dataItem.amount
}
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)

val size = if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
min(widthSize, heightSize)
} else {
resources.displayMetrics.widthPixels
}

setMeasuredDimension(size, size)
}

override fun onDraw(canvas: Canvas) {
val radius = (width.coerceAtMost(height) / 2 * 0.8).toFloat()
val centerX = (width / 2).toFloat()
val centerY = (height / 2).toFloat()
var startAngle = 0f

categories.clear()
categories.addAll(categoryMap.keys.toList())

amounts.clear()
amounts.addAll(categoryMap.values.toList())

amounts.forEachIndexed { index, value ->
val sweepAngle = 360f * value / amounts.sum()

paint.color = try {
colors[index]
} catch (ex: ArrayIndexOutOfBoundsException) {
Color.BLACK
}

canvas.drawArc(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius,
startAngle,
sweepAngle,
true,
paint
)

startAngle += sweepAngle
}
}

override fun onSaveInstanceState(): Parcelable {
return Bundle().apply {
putIntArray("amounts", amounts.toIntArray())
putParcelable("superState", super.onSaveInstanceState())
}
}

override fun onRestoreInstanceState(state: Parcelable?) {
if (state is Bundle) {
super.onRestoreInstanceState(state.getParcelable("superState"))
amounts.clear()
amounts.addAll(state.getIntArray("amounts")?.toList() ?: emptyList())
} else {
super.onRestoreInstanceState(state)
}
}

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
val x = event.x
val y = event.y
val centerX = width / 2f
val centerY = height / 2f
var angle =
Math.toDegrees(atan2(y.toDouble() - centerY, x.toDouble() - centerX)).toFloat()
var startAngle = 0f

if (angle <= 0) angle += 360

for (i in 0 until categories.size) {
val sweepAngle = 360f * amounts[i] / amounts.sum()

if (angle >= startAngle && angle < startAngle + sweepAngle) {
onClickListener?.invoke(categories[i])
return true
}

startAngle += sweepAngle
}
}
return super.onTouchEvent(event)
}
}
Loading