diff --git a/app/src/main/java/otus/homework/customview/LineChartView.kt b/app/src/main/java/otus/homework/customview/LineChartView.kt new file mode 100644 index 00000000..cbdf9ad5 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/LineChartView.kt @@ -0,0 +1,168 @@ +package otus.homework.customview + +import android.content.Context +import android.graphics.* +import android.os.Parcel +import android.os.Parcelable +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat +import kotlin.math.min + +class LineChartView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var data: List> = emptyList() + + fun setData(data: List>) { + this.data = data + invalidate() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredWidth = 100 + val desiredHeight = 100 + + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width = when (widthMode) { + MeasureSpec.EXACTLY -> widthSize + MeasureSpec.AT_MOST -> min(desiredWidth, widthSize) + else -> desiredWidth + } + + val height = when (heightMode) { + MeasureSpec.EXACTLY -> heightSize + MeasureSpec.AT_MOST -> min(desiredHeight, heightSize) + else -> desiredHeight + } + setMeasuredDimension(width, height) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (data.isEmpty()) return + + val padding = 150f + val chartWidth = width - 2 * padding + val chartHeight = height - 2 * padding + + // Определяем максимальное значение для оси Y + val maxAmount = data.maxOfOrNull { it.second } ?: 0f + val yStep = if (maxAmount == 0f) 0f else chartHeight / maxAmount + var xStep = chartWidth / data.size + if (data.size == 1) { + xStep = chartWidth /2 + } + + // Рисуем ось Y + canvas.drawLine(padding, padding, padding, height - padding, paintAxis) + + // Рисуем метки на оси Y + val stepCount = 5 + for (i in 0..stepCount) { + val value = maxAmount / stepCount * i + val y = height - padding - (value * yStep) + + // Линии на графике (сетку) + canvas.drawLine(padding, y, width - padding, y, paintGrid) + + // Подпись значений на оси Y + canvas.drawText(value.toInt().toString(), padding - 50f, y + 10f, paintText) + } + + // Рисуем столбцы + for (i in data.indices) { + val (date, amount) = data[i] + + val x = padding + i * xStep + xStep / 2 + val y = height - padding - (amount * yStep) + val barWidth = xStep * 0.8f + + canvas.drawRect(x - barWidth / 2, y, x + barWidth / 2, height - padding, paintBar) + + // Подпись даты под столбцом + canvas.drawText(date, x - 30f, height - padding + 40f, paintText) + } + } + + private val paintAxis = Paint().apply { + color = Color.BLACK + strokeWidth = 5f + style = Paint.Style.STROKE + } + + private val paintGrid = Paint().apply { + color = Color.LTGRAY + strokeWidth = 2f + style = Paint.Style.STROKE + } + + private val paintText = Paint().apply { + color = Color.BLACK + textSize = 40f + textAlign = Paint.Align.RIGHT + } + + private val paintBar = Paint().apply { + color = ContextCompat.getColor(context,R.color.colorCategory2) + style = Paint.Style.FILL + } + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + return SavedState(superState, data) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is SavedState) { + super.onRestoreInstanceState(state.superState) + data = state.data + } else { + super.onRestoreInstanceState(state) + } + } + + private class SavedState : BaseSavedState { + val data: List> + + constructor(superState: Parcelable?, data: List>) : super(superState) { + this.data = data + } + + private constructor(parcel: Parcel) : super(parcel) { + data = mutableListOf>().apply { + val size = parcel.readInt() + repeat(size) { + add(parcel.readString()!! to parcel.readFloat()) + } + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + super.writeToParcel(parcel, flags) + parcel.writeInt(data.size) + data.forEach { + parcel.writeString(it.first) + parcel.writeFloat(it.second) + } + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SavedState { + return SavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/customview/MainActivity.kt b/app/src/main/java/otus/homework/customview/MainActivity.kt index 78cb9448..856450e9 100644 --- a/app/src/main/java/otus/homework/customview/MainActivity.kt +++ b/app/src/main/java/otus/homework/customview/MainActivity.kt @@ -2,10 +2,68 @@ package otus.homework.customview import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.util.Log +import android.view.View +import org.json.JSONArray +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + val pieChartView = findViewById(R.id.pie_chart_view) + val lineChartView = findViewById(R.id.lineChartView) + + val jsonString = this.resources.openRawResource(R.raw.payload).bufferedReader().use { it.readText() } + val data = parsePayload(jsonString) + pieChartView.setData(data) + + pieChartView.setOnSectorClickListener { category -> + val categoryData = parseCategoryData(jsonString, category) + Log.d("BarChartData", "Data for $category: $categoryData") + lineChartView.setData(categoryData) + + lineChartView.visibility = View.VISIBLE + } + } + +} +fun parsePayload(jsonString: String): List> { + val jsonArray = JSONArray(jsonString) + val categorySums = mutableMapOf() + + for (i in 0 until jsonArray.length()) { + val obj = jsonArray.getJSONObject(i) + val category = obj.getString("category") + val amount = obj.getDouble("amount").toFloat() + categorySums[category] = (categorySums[category] ?: 0f) + amount } + + // Преобразуем в список пар: категория - сумма + return categorySums.map { it.key to it.value } +} + +fun parseCategoryData(jsonString: String, category: String): List> { + val jsonArray = JSONArray(jsonString) + val categoryData = mutableListOf>() + + val dateFormatter = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.getDefault()) + + for (i in 0 until jsonArray.length()) { + val item = jsonArray.getJSONObject(i) + val itemCategory = item.getString("category") + val amount = item.getDouble("amount").toFloat() + val time = item.getLong("time") * 1000 + val date = dateFormatter.format(Date(time)) + + // Если категория совпадает, добавляем в список + if (itemCategory == category) { + categoryData.add(date to amount) + } + } + + return categoryData } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/customview/PieChartView.kt b/app/src/main/java/otus/homework/customview/PieChartView.kt new file mode 100644 index 00000000..27de6a74 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/PieChartView.kt @@ -0,0 +1,201 @@ +package otus.homework.customview + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF + +import android.os.Parcel +import android.os.Parcelable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import androidx.core.content.ContextCompat +import kotlin.math.atan2 +import kotlin.math.min +import kotlin.math.sqrt + +class PieChartView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val rectF = RectF() + private var data: List> = emptyList() + private var colors: List = listOf( + ContextCompat.getColor(context,R.color.colorCategory1), + ContextCompat.getColor(context,R.color.colorCategory2), + ContextCompat.getColor(context,R.color.colorCategory3), + ContextCompat.getColor(context,R.color.colorCategory4), + ContextCompat.getColor(context,R.color.colorCategory5), + ContextCompat.getColor(context,R.color.colorCategory6), + ContextCompat.getColor(context,R.color.colorCategory7), + ContextCompat.getColor(context,R.color.colorCategory8), + ContextCompat.getColor(context,R.color.colorCategory9), + ContextCompat.getColor(context,R.color.colorCategory10) + ) + private var onSectorClickListener: ((String) -> Unit)? = null + private var angles = + mutableListOf>() + + + private fun findClickedSector(x: Float, y: Float): String? { + val centerX = width / 2f + val centerY = height / 2f + val dx = x - centerX + val dy = y - centerY + + // Проверяем, попадает ли клик в круг + val distance = Math.sqrt((dx * dx + dy * dy).toDouble()) + val radius = width / 2f + if (distance > radius) return null + + // Вычисляем угол клика + val clickAngle = Math.toDegrees(Math.atan2(dy.toDouble(), dx.toDouble())).toFloat() + val normalizedAngle = (clickAngle + 360) % 360 + + // Проверяем, попадает ли угол в сектор + for (i in angles.indices) { + val (startAngle, sweepAngle) = angles[i] + val endAngle = (startAngle + sweepAngle) % 360 + if (normalizedAngle in startAngle..endAngle || + (startAngle > endAngle && (normalizedAngle in startAngle..360F || normalizedAngle in 0f..endAngle))) { + return data[i].first + } + } + return null + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_DOWN) { + val clickedCategory = findClickedSector(event.x, event.y) + clickedCategory?.let { onSectorClickListener?.invoke(it) } + return true + } + return super.onTouchEvent(event) + } + fun setData(newData: List>) { + data = newData + invalidate() + } + + fun setOnSectorClickListener(listener: (String) -> Unit) { + onSectorClickListener = listener + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredWidth = 100 + val desiredHeight = 100 + + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width = when (widthMode) { + MeasureSpec.EXACTLY -> widthSize + MeasureSpec.AT_MOST -> min(desiredWidth, widthSize) + else -> desiredWidth + } + + val height = when (heightMode) { + MeasureSpec.EXACTLY -> heightSize + MeasureSpec.AT_MOST -> min(desiredHeight, heightSize) + else -> desiredHeight + } + + setMeasuredDimension(width, height) + + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (data.isEmpty()) return + + val total = data.map { it.second }.sum() + val centerX = width / 2f + val centerY = height / 2f + val radius = width / 2f + rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius) + + var startAngle = -90f + angles.clear() + + data.forEachIndexed { index, (category, value) -> + val sweepAngle = (value / total) * 360f + paint.color = colors[index % colors.size] + canvas.drawArc(rectF, startAngle, sweepAngle, true, paint) + angles.add(startAngle to sweepAngle) + startAngle += sweepAngle + } + } + + private fun handleTouch(x: Float, y: Float) { + val centerX = width / 2f + val centerY = height / 2f + val dx = x - centerX + val dy = y - centerY + val distance = sqrt(dx * dx + dy * dy) + + if (distance > width / 2f) return + + val angle = (atan2(dy, dx) * (180 / Math.PI) + 360) % 360 + val clickedSectorIndex = + angles.indexOfFirst { angle >= it.first && angle < it.first + it.second } + + if (clickedSectorIndex != -1) { + onSectorClickListener?.invoke(data[clickedSectorIndex].first) + } + } + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + return SavedState(superState, data) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is SavedState) { + super.onRestoreInstanceState(state.superState) + data = state.data + } else { + super.onRestoreInstanceState(state) + } + } + + private class SavedState : BaseSavedState { + val data: List> + + constructor(superState: Parcelable?, data: List>) : super(superState) { + this.data = data + } + + private constructor(parcel: Parcel) : super(parcel) { + data = mutableListOf>().apply { + val size = parcel.readInt() + repeat(size) { + add(parcel.readString()!! to parcel.readFloat()) + } + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + super.writeToParcel(parcel, flags) + parcel.writeInt(data.size) + data.forEach { + parcel.writeString(it.first) + parcel.writeFloat(it.second) + } + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SavedState { + return SavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 79ae6993..2fba1de0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,13 +7,19 @@ android:layout_height="match_parent" tools:context=".MainActivity"> - + + + app:layout_constraintBottom_toBottomOf="parent"/> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127d..d936441d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,14 @@ #FF018786 #FF000000 #FFFFFFFF + #FF5722 + #3F51B5 + #4CAF50 + #FFC107 + #E91E63 + #9C27B0 + #2196F3 + #FFEB3B + #795548 + #607D8B \ No newline at end of file