From 26860dd23c8b8c82f88d0e039d3543a8cdf2abd0 Mon Sep 17 00:00:00 2001 From: "a.batrakov" Date: Tue, 28 Apr 2026 00:36:44 +0300 Subject: [PATCH 1/2] do canvas homework --- app/build.gradle | 1 + .../otus/homework/customview/CategoryModel.kt | 9 ++ .../otus/homework/customview/MainActivity.kt | 15 ++ .../java/otus/homework/customview/PieChart.kt | 133 ++++++++++++++++++ .../otus/homework/customview/PieViewModel.kt | 40 ++++++ .../java/otus/homework/customview/Sector.kt | 7 + .../homework/customview/data/JsonMapper.kt | 20 +++ .../homework/customview/dto/CategoryDTO.kt | 8 ++ app/src/main/res/layout/activity_main.xml | 24 ++-- 9 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/otus/homework/customview/CategoryModel.kt create mode 100644 app/src/main/java/otus/homework/customview/PieChart.kt create mode 100644 app/src/main/java/otus/homework/customview/PieViewModel.kt create mode 100644 app/src/main/java/otus/homework/customview/Sector.kt create mode 100644 app/src/main/java/otus/homework/customview/data/JsonMapper.kt create mode 100644 app/src/main/java/otus/homework/customview/dto/CategoryDTO.kt diff --git a/app/build.gradle b/app/build.gradle index f22e7497..df0bfe8b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,4 +54,5 @@ dependencies { testImplementation libs.junit androidTestImplementation libs.androidx.test.ext.junit androidTestImplementation libs.espresso.core + implementation("com.google.code.gson:gson:2.11.0") } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/customview/CategoryModel.kt b/app/src/main/java/otus/homework/customview/CategoryModel.kt new file mode 100644 index 00000000..dd1da121 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/CategoryModel.kt @@ -0,0 +1,9 @@ +package otus.homework.customview + +data class CategoryModel( + val id: Int, + val name: String, + val amount: Int, + val category: String, + val color: Int, +) diff --git a/app/src/main/java/otus/homework/customview/MainActivity.kt b/app/src/main/java/otus/homework/customview/MainActivity.kt index 78cb9448..3ddcb4de 100644 --- a/app/src/main/java/otus/homework/customview/MainActivity.kt +++ b/app/src/main/java/otus/homework/customview/MainActivity.kt @@ -2,10 +2,25 @@ package otus.homework.customview import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.widget.TextView +import androidx.activity.viewModels class MainActivity : AppCompatActivity() { + private val viewModel: PieViewModel by viewModels { + PieViewModelFactory(context = this) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + val pieChartView = findViewById(R.id.pieChartView) + val contentNameTextView = findViewById(R.id.categoryName) + + val data = viewModel.data + pieChartView.onSliceClick = { categoryName -> + contentNameTextView.text = categoryName + } + + pieChartView.setData(data) } } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/customview/PieChart.kt b/app/src/main/java/otus/homework/customview/PieChart.kt new file mode 100644 index 00000000..2a8351d9 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/PieChart.kt @@ -0,0 +1,133 @@ +package otus.homework.customview + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import kotlin.math.atan2 +import kotlin.math.min +import kotlin.math.sqrt + +class PieChart @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + var onSliceClick: ((String) -> Unit)? = null +): View(context, attrs) { + + val Int.dp: Float + get() = this * resources.displayMetrics.density + + val pieWith = 200.dp + val pieHeight = 200.dp + + private var data: List = emptyList() + private val sectors = mutableListOf() + + fun setData(items: List) { + data = items + invalidate() + } + + init{ + if (isInEditMode) { + //setValues(listOf(1,2,3,4,5)) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val wMode = MeasureSpec.getMode(widthMeasureSpec) + val hMode = MeasureSpec.getMode(heightMeasureSpec) + val wSize = MeasureSpec.getSize(widthMeasureSpec) + val hSize = MeasureSpec.getSize(heightMeasureSpec) + + when (wMode) { + MeasureSpec.EXACTLY -> { + setMeasuredDimension(wSize, hSize) + } + MeasureSpec.AT_MOST -> { + val newW = min(pieWith.toInt(), wSize) + setMeasuredDimension(newW, hSize) + } + MeasureSpec.UNSPECIFIED -> { + setMeasuredDimension(pieWith.toInt(), hSize) + } + } + + when (hMode) { + MeasureSpec.EXACTLY -> { + setMeasuredDimension(wSize, hSize) + } + MeasureSpec.AT_MOST -> { + val newH = min(pieHeight.toInt(), wSize) + setMeasuredDimension(wSize, newH) + } + MeasureSpec.UNSPECIFIED -> { + setMeasuredDimension(wSize, pieHeight.toInt()) + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + val paint = Paint().apply { + style = Paint.Style.FILL + } + + val rect = RectF(0f, 0f, pieWith, pieHeight) + + @SuppressLint("DrawAllocation") + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + sectors.clear() + + if(data.isEmpty()) return + + val total = data.sumOf { it.amount.toDouble() }.toFloat() + var startAngle = 0f + + data.forEach {item -> + paint.color = item.color + val swipeAngle = (item.amount/total) * 360f + + canvas.drawArc(rect, startAngle, swipeAngle, true, paint) + + sectors.add( + Sector( + category = item.category, + start = startAngle, + end = startAngle + swipeAngle + ) + ) + + startAngle += swipeAngle + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + + if (event.action != MotionEvent.ACTION_DOWN) return true + + val dx = event.x - rect.centerX() + val dy = event.y - rect.centerY() + + val distance = sqrt(dx * dx + dy * dy) + + if (distance > rect.width() / 2f) return false + + val angle = ((Math.toDegrees( + atan2(dy.toDouble(), dx.toDouble()) + ) + 360) % 360).toFloat() + + sectors.forEach { sector -> + if (angle in sector.start..sector.end) { + onSliceClick?.invoke(sector.category) + return true + } + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/customview/PieViewModel.kt b/app/src/main/java/otus/homework/customview/PieViewModel.kt new file mode 100644 index 00000000..7c88099b --- /dev/null +++ b/app/src/main/java/otus/homework/customview/PieViewModel.kt @@ -0,0 +1,40 @@ +package otus.homework.customview + +import android.content.Context +import android.graphics.Color +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import otus.homework.customview.data.JsonMapper + +class PieViewModel( + private val jsonMapper: JsonMapper +) : ViewModel() { + + val data = jsonMapper.mapJson() + .map { + CategoryModel( + id = it.id, + name = it.name, + amount = it.amount, + category = it.category, + color = randomColor() + ) + } + } + +private fun randomColor(): Int { + return Color.rgb( + (0..255).random(), + (0..255).random(), + (0..255).random(), + ) +} + +class PieViewModelFactory( + private val context: Context +) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + val jsonMapper = JsonMapper(context.applicationContext) + return PieViewModel(jsonMapper) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/customview/Sector.kt b/app/src/main/java/otus/homework/customview/Sector.kt new file mode 100644 index 00000000..12cf3c1c --- /dev/null +++ b/app/src/main/java/otus/homework/customview/Sector.kt @@ -0,0 +1,7 @@ +package otus.homework.customview + +data class Sector( + val category: String, + val start: Float, + val end: Float +) diff --git a/app/src/main/java/otus/homework/customview/data/JsonMapper.kt b/app/src/main/java/otus/homework/customview/data/JsonMapper.kt new file mode 100644 index 00000000..eb68c10e --- /dev/null +++ b/app/src/main/java/otus/homework/customview/data/JsonMapper.kt @@ -0,0 +1,20 @@ +package otus.homework.customview.data + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import otus.homework.customview.R +import otus.homework.customview.dto.CategoryDTO + +class JsonMapper(private val context: Context) { + + fun mapJson(): List { + val json = context.resources + .openRawResource(R.raw.payload) + .bufferedReader() + .use { it.readText() } + + val type = object : TypeToken>() {}.type + return Gson().fromJson(json, type) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/customview/dto/CategoryDTO.kt b/app/src/main/java/otus/homework/customview/dto/CategoryDTO.kt new file mode 100644 index 00000000..9cf38fde --- /dev/null +++ b/app/src/main/java/otus/homework/customview/dto/CategoryDTO.kt @@ -0,0 +1,8 @@ +package otus.homework.customview.dto + +data class CategoryDTO( + val id: Int, + val name: String, + val amount: Int, + val category: String, +) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 79ae6993..e1f81d2d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,21 @@ - - + + + android:id="@+id/categoryName" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"/> - \ No newline at end of file + \ No newline at end of file From 83b5777104c4fc7602f7eaaad81108722194317b Mon Sep 17 00:00:00 2001 From: "a.batrakov" Date: Sun, 3 May 2026 23:52:46 +0300 Subject: [PATCH 2/2] fix review comments --- .../otus/homework/customview/CategoryModel.kt | 6 +- .../java/otus/homework/customview/PieChart.kt | 153 +++++++++++------- .../otus/homework/customview/PieViewModel.kt | 12 +- 3 files changed, 98 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/otus/homework/customview/CategoryModel.kt b/app/src/main/java/otus/homework/customview/CategoryModel.kt index dd1da121..7da45eff 100644 --- a/app/src/main/java/otus/homework/customview/CategoryModel.kt +++ b/app/src/main/java/otus/homework/customview/CategoryModel.kt @@ -1,9 +1,11 @@ package otus.homework.customview +import java.io.Serializable + + data class CategoryModel( val id: Int, val name: String, val amount: Int, val category: String, - val color: Int, -) +) : Serializable diff --git a/app/src/main/java/otus/homework/customview/PieChart.kt b/app/src/main/java/otus/homework/customview/PieChart.kt index 2a8351d9..83eacb1f 100644 --- a/app/src/main/java/otus/homework/customview/PieChart.kt +++ b/app/src/main/java/otus/homework/customview/PieChart.kt @@ -1,15 +1,16 @@ 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.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 import kotlin.math.sqrt class PieChart @JvmOverloads constructor( @@ -21,14 +22,13 @@ class PieChart @JvmOverloads constructor( val Int.dp: Float get() = this * resources.displayMetrics.density - val pieWith = 200.dp - val pieHeight = 200.dp - - private var data: List = emptyList() + private var data: Map> = emptyMap() private val sectors = mutableListOf() + private lateinit var rect: RectF - fun setData(items: List) { + fun setData(items: Map>) { data = items + calculateSectors() invalidate() } @@ -38,72 +38,64 @@ class PieChart @JvmOverloads constructor( } } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val wMode = MeasureSpec.getMode(widthMeasureSpec) - val hMode = MeasureSpec.getMode(heightMeasureSpec) - val wSize = MeasureSpec.getSize(widthMeasureSpec) - val hSize = MeasureSpec.getSize(heightMeasureSpec) - - when (wMode) { - MeasureSpec.EXACTLY -> { - setMeasuredDimension(wSize, hSize) - } - MeasureSpec.AT_MOST -> { - val newW = min(pieWith.toInt(), wSize) - setMeasuredDimension(newW, hSize) - } - MeasureSpec.UNSPECIFIED -> { - setMeasuredDimension(pieWith.toInt(), hSize) - } - } + override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() - when (hMode) { - MeasureSpec.EXACTLY -> { - setMeasuredDimension(wSize, hSize) - } - MeasureSpec.AT_MOST -> { - val newH = min(pieHeight.toInt(), wSize) - setMeasuredDimension(wSize, newH) - } - MeasureSpec.UNSPECIFIED -> { - setMeasuredDimension(wSize, pieHeight.toInt()) - } + val bundle = Bundle() + bundle.putParcelable("super_state", superState) + bundle.putSerializable(STATE_DATE, HashMap(data)) + return bundle + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is Bundle) { + val superState = state.getParcelable("super_state") + super.onRestoreInstanceState(superState) + + data = state.getSerializable("data") as HashMap> + invalidate() + } else { + super.onRestoreInstanceState(state) } - super.onMeasure(widthMeasureSpec, heightMeasureSpec) } - val paint = Paint().apply { - style = Paint.Style.FILL + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + + setMeasuredDimension(width, height) } - val rect = RectF(0f, 0f, pieWith, pieHeight) + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) - @SuppressLint("DrawAllocation") - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) + val size = minOf(w, h).toFloat() - sectors.clear() + val left = (w - size) / 2f + val top = (h - size) / 2f - if(data.isEmpty()) return + rect = RectF(left, top, left + size, top + size) + } - val total = data.sumOf { it.amount.toDouble() }.toFloat() - var startAngle = 0f + val paint = Paint().apply { + style = Paint.Style.FILL + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) - data.forEach {item -> - paint.color = item.color - val swipeAngle = (item.amount/total) * 360f + if (sectors.isEmpty()) return - canvas.drawArc(rect, startAngle, swipeAngle, true, paint) + sectors.forEach { + paint.color = randomColor() - sectors.add( - Sector( - category = item.category, - start = startAngle, - end = startAngle + swipeAngle - ) + canvas.drawArc( + rect, + it.start, + it.end - it.start, + true, + paint ) - - startAngle += swipeAngle } } @@ -116,7 +108,7 @@ class PieChart @JvmOverloads constructor( val distance = sqrt(dx * dx + dy * dy) - if (distance > rect.width() / 2f) return false + if (distance > rect.width() / 2f) return true val angle = ((Math.toDegrees( atan2(dy.toDouble(), dx.toDouble()) @@ -130,4 +122,45 @@ class PieChart @JvmOverloads constructor( } return true } + + private fun randomColor(): Int { + return Color.rgb( + (0..255).random(), + (0..255).random(), + (0..255).random(), + ) + } + + private fun calculateSectors() { + sectors.clear() + + val total = data.values.sumOf { list -> + list.sumOf { it.amount } + }.toFloat() + + var startAngle = 0f + + data.forEach { (category, items) -> + val amount = items.sumOf { it.amount }.toFloat() + + val sweep = + if (amount != 0f) { + (amount / total) * 360f + } else 0f + + sectors.add( + Sector( + category = category, + start = startAngle, + end = startAngle + sweep + ) + ) + + startAngle += sweep + } + } + + companion object { + private const val STATE_DATE = "data" + } } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/customview/PieViewModel.kt b/app/src/main/java/otus/homework/customview/PieViewModel.kt index 7c88099b..d1da1994 100644 --- a/app/src/main/java/otus/homework/customview/PieViewModel.kt +++ b/app/src/main/java/otus/homework/customview/PieViewModel.kt @@ -1,7 +1,6 @@ package otus.homework.customview import android.content.Context -import android.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import otus.homework.customview.data.JsonMapper @@ -17,17 +16,8 @@ class PieViewModel( name = it.name, amount = it.amount, category = it.category, - color = randomColor() ) - } - } - -private fun randomColor(): Int { - return Color.rgb( - (0..255).random(), - (0..255).random(), - (0..255).random(), - ) + }.groupBy { it.category } } class PieViewModelFactory(