diff --git a/app/src/main/java/otus/homework/customview/Expense.kt b/app/src/main/java/otus/homework/customview/Expense.kt new file mode 100644 index 00000000..52685ba2 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/Expense.kt @@ -0,0 +1,19 @@ +package otus.homework.customview + +import android.content.Context +import com.google.gson.Gson + +data class Expense( + val id: Int, + val name: String, + val amount: Float, + val category: String, + val time: Long +) + +fun Context.loadExpenses(): List { + val input = resources.openRawResource(R.raw.payload) + val json = input.bufferedReader().use { it.readText() } + return Gson().fromJson(json, Array::class.java).toList() +} + diff --git a/app/src/main/java/otus/homework/customview/MainActivity.kt b/app/src/main/java/otus/homework/customview/MainActivity.kt index 78cb9448..b7875b98 100644 --- a/app/src/main/java/otus/homework/customview/MainActivity.kt +++ b/app/src/main/java/otus/homework/customview/MainActivity.kt @@ -2,10 +2,20 @@ package otus.homework.customview import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.widget.Toast class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + val pie = findViewById(R.id.pieChart) + pie.setExpenses(loadExpenses()) + + pie.listener = object : PieChartView.OnSliceClickListener { + override fun onCategoryClick(category: String) { + Toast.makeText(this@MainActivity, category, Toast.LENGTH_SHORT).show() + } + } } } \ 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..91051c74 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/PieChartView.kt @@ -0,0 +1,113 @@ +package otus.homework.customview + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +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 @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { + + interface OnSliceClickListener { + fun onCategoryClick(category: String) + } + + var listener: OnSliceClickListener? = null + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val rect = RectF() + + private val colors = listOf( + Color.RED, Color.BLUE, Color.GREEN, Color.CYAN, Color.MAGENTA, + Color.YELLOW, Color.DKGRAY, Color.GRAY, Color.LTGRAY, Color.BLACK + ) + + private var data: Map = emptyMap() + private var angles = mutableListOf>() + + fun setExpenses(expenses: List) { + data = expenses.groupBy { it.category } + .mapValues { it.value.sumOf { e -> e.amount.toDouble() }.toFloat() } + calculateAngles() + invalidate() + } + + private fun calculateAngles() { + angles.clear() + val total = data.values.sum() + data.forEach { (cat, value) -> + angles += cat to (value / total * 360f) + } + } + + // --- onMeasure (учтены все MeasureSpec) --- + override fun onMeasure(w: Int, h: Int) { + val size = min( + MeasureSpec.getSize(w), + MeasureSpec.getSize(h) + ) + + val finalSize = when { + MeasureSpec.getMode(w) == MeasureSpec.EXACTLY -> MeasureSpec.getSize(w) + MeasureSpec.getMode(h) == MeasureSpec.EXACTLY -> MeasureSpec.getSize(h) + else -> size + } + setMeasuredDimension(finalSize, finalSize) + } + + override fun onDraw(canvas: Canvas) { + var startAngle = -90f + rect.set(0f, 0f, width.toFloat(), height.toFloat()) + + angles.forEachIndexed { i, (cat, sweep) -> + paint.color = colors[i % colors.size] + canvas.drawArc(rect, startAngle, sweep, true, paint) + startAngle += sweep + } + } + + // --- обработка клика --- + override fun onTouchEvent(e: MotionEvent): Boolean { + if (e.action != MotionEvent.ACTION_DOWN) return true + + val cx = width / 2f + val cy = height / 2f + val angle = (Math.toDegrees( + atan2(e.y - cy, e.x - cx).toDouble() + ) + 360 + 90) % 360 + + var acc = 0f + for ((cat, sweep) in angles) { + acc += sweep + if (angle <= acc) { + listener?.onCategoryClick(cat) + break + } + } + return true + } + + // --- сохранение состояния --- + override fun onSaveInstanceState(): Parcelable = + Bundle().apply { + putParcelable("super", super.onSaveInstanceState()) + putSerializable("data", HashMap(data)) + } + + override fun onRestoreInstanceState(state: Parcelable) { + val b = state as Bundle + super.onRestoreInstanceState(b.getParcelable("super")) + data = (b.getSerializable("data") as HashMap) + calculateAngles() + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 79ae6993..db225aad 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,13 +7,13 @@ android:layout_height="match_parent" tools:context=".MainActivity"> - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> - \ No newline at end of file + diff --git a/art/Screenshot_20260202_230456.png b/art/Screenshot_20260202_230456.png new file mode 100644 index 00000000..43e0abd8 Binary files /dev/null and b/art/Screenshot_20260202_230456.png differ