From 087fc38e2b12a9ab057007e44b739e69633c63b3 Mon Sep 17 00:00:00 2001 From: Ivan Toropygin Date: Sun, 8 Feb 2026 10:26:55 +0300 Subject: [PATCH 1/2] work with errors --- .../otus/homework/customview/CategoryData.kt | 7 + .../otus/homework/customview/MainActivity.kt | 70 ++++ .../otus/homework/customview/PieChartView.kt | 300 ++++++++++++++++++ .../otus/homework/customview/Transaction.kt | 9 + app/src/main/res/layout/activity_main.xml | 25 +- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 406 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/otus/homework/customview/CategoryData.kt create mode 100644 app/src/main/java/otus/homework/customview/PieChartView.kt create mode 100644 app/src/main/java/otus/homework/customview/Transaction.kt diff --git a/app/src/main/java/otus/homework/customview/CategoryData.kt b/app/src/main/java/otus/homework/customview/CategoryData.kt new file mode 100644 index 00000000..1b9f7a85 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/CategoryData.kt @@ -0,0 +1,7 @@ +package otus.homework.customview + +data class CategoryData( + val category: String, + val amount: Double, + 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..d94911eb 100644 --- a/app/src/main/java/otus/homework/customview/MainActivity.kt +++ b/app/src/main/java/otus/homework/customview/MainActivity.kt @@ -2,10 +2,80 @@ package otus.homework.customview import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.google.gson.Gson class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + val pieChartView = findViewById(R.id.pieChartView) + + val jsonString = loadJsonFromRaw() + val transactions = parseTransactions(jsonString) + val categoriesData = groupTransactionsByCategory(transactions) + + pieChartView.setData(categoriesData) + pieChartView.setOnSectorClickListener(object : PieChartView.OnSectorClickListener { + override fun onSectorClick(category: String) { + Toast.makeText( + this@MainActivity, + "Выбрана категория: $category", + Toast.LENGTH_SHORT + ).show() + } + }) + } + + private fun loadJsonFromRaw(): String { + return try { + resources.openRawResource(R.raw.payload).bufferedReader().use { it.readText() } + } catch (e: Exception) { + e.printStackTrace() + "[]" + } + } + + private fun parseTransactions(jsonString: String): List { + return try { + Gson().fromJson(jsonString, Array::class.java).toList() + } catch (e: Exception) { + emptyList() + } + } + + private fun groupTransactionsByCategory(transactions: List): List { + val categoriesMap = mutableMapOf() + + transactions.forEach { transaction -> + val currentAmount = categoriesMap[transaction.category] ?: 0.0 + categoriesMap[transaction.category] = currentAmount + transaction.amount + } + + val colors = arrayOf( + ContextCompat.getColor(this, android.R.color.holo_red_dark), + ContextCompat.getColor(this, android.R.color.holo_blue_dark), + ContextCompat.getColor(this, android.R.color.holo_green_dark), + ContextCompat.getColor(this, android.R.color.holo_orange_dark), + ContextCompat.getColor(this, android.R.color.holo_purple), + ContextCompat.getColor(this, android.R.color.darker_gray), + ContextCompat.getColor(this, android.R.color.holo_blue_light), + ContextCompat.getColor(this, android.R.color.holo_green_light), + ContextCompat.getColor(this, android.R.color.holo_orange_light), + ContextCompat.getColor(this, android.R.color.holo_red_light), + ContextCompat.getColor(this, R.color.colorPrimary), + ContextCompat.getColor(this, R.color.colorAccent) + ) + + return categoriesMap.entries.mapIndexed { index, (category, amount) -> + CategoryData( + category = category, + amount = amount, + color = colors[index % colors.size] + ) + }.sortedByDescending { it.amount } } } \ 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..168f3537 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/PieChartView.kt @@ -0,0 +1,300 @@ +package otus.homework.customview + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.os.Parcel +import android.os.Parcelable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.sqrt + +class PieChartView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + interface OnSectorClickListener { + fun onSectorClick(category: String) + } + + private var onSectorClickListener: OnSectorClickListener? = null + + fun setOnSectorClickListener(listener: OnSectorClickListener) { + this.onSectorClickListener = listener + } + + private var categoriesData = mutableListOf() + private val colors = arrayOf( + Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW, Color.CYAN, + Color.MAGENTA, Color.GRAY, Color.parseColor("#FFA500"), // Orange + Color.parseColor("#800080"), // Purple + Color.parseColor("#008080"), // Teal + Color.parseColor("#FF1493"), // Deep Pink + Color.parseColor("#00CED1") // Dark Turquoise + ) + + private var totalAmount = 0.0 + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val selectedPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val centerPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private var centerText = "" + + private var centerX = 0f + private var centerY = 0f + private var radius = 0f + private var selectedSector = -1 + + init { + setupPaints() + } + + private fun setupPaints() { + paint.style = Paint.Style.FILL + paint.strokeWidth = 2f + + textPaint.color = Color.WHITE + textPaint.textSize = 36f + textPaint.textAlign = Paint.Align.CENTER + + selectedPaint.style = Paint.Style.FILL + selectedPaint.color = Color.parseColor("#CCCCCC") + selectedPaint.strokeWidth = 3f + + centerPaint.color = Color.WHITE + centerPaint.style = Paint.Style.FILL + } + + fun setData(categories: List) { + categoriesData.clear() + categoriesData.addAll(categories) + totalAmount = categoriesData.sumOf { it.amount } + updateCenterText() + invalidate() + requestLayout() + } + + private fun updateCenterText() { + if (categoriesData.isNotEmpty()) { + val topCategory = categoriesData.maxByOrNull { it.amount } + centerText = topCategory?.category ?: "" + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val minSize = 400 + + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width: Int = when (widthMode) { + MeasureSpec.EXACTLY -> widthSize + MeasureSpec.AT_MOST -> minOf(widthSize, minSize) + else -> minSize + } + + val height: Int = when (heightMode) { + MeasureSpec.EXACTLY -> heightSize + MeasureSpec.AT_MOST -> minOf(heightSize, minSize) + else -> minSize + } + + val size = minOf(width, height) + setMeasuredDimension(size, size) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + centerX = w / 2f + centerY = h / 2f + radius = minOf(w, h) * 0.4f + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (categoriesData.isEmpty() || totalAmount == 0.0) { + drawEmptyState(canvas) + return + } + + var startAngle = 0f + + for ((index, category) in categoriesData.withIndex()) { + val sweepAngle = (category.amount / totalAmount * 360).toFloat() + + paint.color = category.color + + if (index == selectedSector) { + val offsetRadius = radius * 1.1f + val offsetX = + (cos(Math.toRadians((startAngle + sweepAngle / 2).toDouble())) * offsetRadius * 0.1).toFloat() + val offsetY = + (sin(Math.toRadians((startAngle + sweepAngle / 2).toDouble())) * offsetRadius * 0.1).toFloat() + + canvas.drawArc( + centerX - offsetRadius + offsetX, + centerY - offsetRadius + offsetY, + centerX + offsetRadius + offsetX, + centerY + offsetRadius + offsetY, + startAngle, + sweepAngle, + true, + paint + ) + } else { + canvas.drawArc( + centerX - radius, + centerY - radius, + centerX + radius, + centerY + radius, + startAngle, + sweepAngle, + true, + paint + ) + } + + if (sweepAngle > 15) { + val angle = startAngle + sweepAngle / 2 + val textRadius = radius * 0.7f + val x = centerX + cos(Math.toRadians(angle.toDouble())).toFloat() * textRadius + val y = centerY + sin(Math.toRadians(angle.toDouble())).toFloat() * textRadius + + canvas.save() + canvas.rotate(angle, x, y) + canvas.drawText( + category.category, + x, + y, + textPaint + ) + canvas.restore() + } + + startAngle += sweepAngle + } + + canvas.drawCircle(centerX, centerY, radius * 0.3f, centerPaint) + + canvas.drawText( + centerText, + centerX, + centerY + textPaint.textSize / 3, + textPaint.apply { color = Color.BLACK } + ) + } + + private fun drawEmptyState(canvas: Canvas) { + paint.color = Color.LTGRAY + canvas.drawCircle(centerX, centerY, radius, paint) + + textPaint.color = Color.DKGRAY + canvas.drawText( + "Нет данных", + centerX, + centerY, + textPaint + ) + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + val x = event.x + val y = event.y + + val distance = sqrt((x - centerX).pow(2) + (y - centerY).pow(2)) + + if (distance <= radius) { + var angle = (Math.toDegrees( + atan2( + (y - centerY).toDouble(), + (x - centerX).toDouble() + ) + ) + 360) % 360 + + angle = (angle + 90) % 360 + + var startAngle = 0f + for ((index, category) in categoriesData.withIndex()) { + val sweepAngle = (category.amount / totalAmount * 360).toFloat() + + if (angle >= startAngle && angle < startAngle + sweepAngle) { + selectedSector = index + onSectorClickListener?.onSectorClick(category.category) + invalidate() + return true + } + startAngle += sweepAngle + } + } + selectedSector = -1 + invalidate() + } + } + return super.onTouchEvent(event) + } + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + return SavedState(superState).apply { + this.selectedSector = this@PieChartView.selectedSector + this.centerText = this@PieChartView.centerText + } + } + + override fun onRestoreInstanceState(state: Parcelable?) { + val savedState = state as? SavedState + if (savedState != null) { + super.onRestoreInstanceState(savedState.superState) + selectedSector = savedState.selectedSector + centerText = savedState.centerText + } else { + super.onRestoreInstanceState(state) + } + } + + private class SavedState : BaseSavedState { + var selectedSector: Int = -1 + var centerText: String = "" + + constructor(superState: Parcelable?) : super(superState) + + constructor(parcel: Parcel) : super(parcel) { + selectedSector = parcel.readInt() + centerText = parcel.readString() ?: "" + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeInt(selectedSector) + out.writeString(centerText) + } + + companion object { + @JvmField + val CREATOR = object : 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/Transaction.kt b/app/src/main/java/otus/homework/customview/Transaction.kt new file mode 100644 index 00000000..080a8ca7 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/Transaction.kt @@ -0,0 +1,9 @@ +package otus.homework.customview + +data class Transaction( + val id: Int, + val name: String, + val amount: Double, + val category: String, + val time: Long +) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 79ae6993..628ff428 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,26 @@ - + android:layout_marginBottom="24dp" + android:text="@string/spending_chart_by_category" + android:textSize="20sp" + android:textStyle="bold" /> - \ No newline at end of file + + + \ 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..335c1b73 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,7 @@ #FF018786 #FF000000 #FFFFFFFF + #3F51B5 + #303F9F + #FF4081 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9213c339..be259411 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Custom View + Диаграмма расходов по категориям \ No newline at end of file From e2bfcccdffe85557979d45820c06352b86b7fbf7 Mon Sep 17 00:00:00 2001 From: Ivan Toropygin Date: Sun, 8 Feb 2026 10:46:36 +0300 Subject: [PATCH 2/2] fix --- .../otus/homework/customview/CategoryData.kt | 3 +- .../otus/homework/customview/MainActivity.kt | 70 ++++++-- .../otus/homework/customview/PieChartView.kt | 151 +++++++++--------- app/src/main/res/layout/activity_main.xml | 23 ++- app/src/main/res/values/strings.xml | 1 + 5 files changed, 153 insertions(+), 95 deletions(-) diff --git a/app/src/main/java/otus/homework/customview/CategoryData.kt b/app/src/main/java/otus/homework/customview/CategoryData.kt index 1b9f7a85..c2ef1d8e 100644 --- a/app/src/main/java/otus/homework/customview/CategoryData.kt +++ b/app/src/main/java/otus/homework/customview/CategoryData.kt @@ -3,5 +3,6 @@ package otus.homework.customview data class CategoryData( val category: String, val amount: Double, - val color: Int + val color: Int, + val transactions: List = emptyList() ) diff --git a/app/src/main/java/otus/homework/customview/MainActivity.kt b/app/src/main/java/otus/homework/customview/MainActivity.kt index d94911eb..e859568b 100644 --- a/app/src/main/java/otus/homework/customview/MainActivity.kt +++ b/app/src/main/java/otus/homework/customview/MainActivity.kt @@ -1,35 +1,70 @@ package otus.homework.customview -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import android.widget.Toast +import android.text.format.DateFormat +import android.widget.TextView +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import com.google.gson.Gson +import java.util.Date class MainActivity : AppCompatActivity() { + private lateinit var detailsTextView: TextView + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } - val pieChartView = findViewById(R.id.pieChartView) + detailsTextView = findViewById(R.id.detailsTextView) val jsonString = loadJsonFromRaw() val transactions = parseTransactions(jsonString) val categoriesData = groupTransactionsByCategory(transactions) + val pieChartView = findViewById(R.id.pieChartView) pieChartView.setData(categoriesData) pieChartView.setOnSectorClickListener(object : PieChartView.OnSectorClickListener { - override fun onSectorClick(category: String) { - Toast.makeText( - this@MainActivity, - "Выбрана категория: $category", - Toast.LENGTH_SHORT - ).show() + override fun onSectorClick(categoryData: CategoryData) { + showCategoryDetails(categoryData) } }) } + private fun showCategoryDetails(categoryData: CategoryData) { + val stringBuilder = StringBuilder() + + stringBuilder.append("Категория: ${categoryData.category}\n") + stringBuilder.append("Общая сумма: ${String.format("%.2f", categoryData.amount)} руб.\n") + stringBuilder.append("Количество транзакций: ${categoryData.transactions.size}\n\n") + + stringBuilder.append("Детали транзакций:\n") + stringBuilder.append("-------------------\n") + + categoryData.transactions.forEach { transaction -> + stringBuilder.append("Название: ${transaction.name}\n") + stringBuilder.append("Сумма: ${String.format("%.2f", transaction.amount)} руб.\n") + stringBuilder.append("Время: ${formatTime(transaction.time)}\n") + stringBuilder.append("---\n") + } + + detailsTextView.text = stringBuilder.toString() + } + + private fun formatTime(timestamp: Long): String { + val date = Date(timestamp * 1000L) + return DateFormat.format("dd.MM.yyyy HH:mm", date).toString() + } + private fun loadJsonFromRaw(): String { return try { resources.openRawResource(R.raw.payload).bufferedReader().use { it.readText() } @@ -43,16 +78,19 @@ class MainActivity : AppCompatActivity() { return try { Gson().fromJson(jsonString, Array::class.java).toList() } catch (e: Exception) { + e.printStackTrace() emptyList() } } private fun groupTransactionsByCategory(transactions: List): List { - val categoriesMap = mutableMapOf() + val categoriesMap = mutableMapOf>() transactions.forEach { transaction -> - val currentAmount = categoriesMap[transaction.category] ?: 0.0 - categoriesMap[transaction.category] = currentAmount + transaction.amount + if (!categoriesMap.containsKey(transaction.category)) { + categoriesMap[transaction.category] = mutableListOf() + } + categoriesMap[transaction.category]?.add(transaction) } val colors = arrayOf( @@ -70,11 +108,13 @@ class MainActivity : AppCompatActivity() { ContextCompat.getColor(this, R.color.colorAccent) ) - return categoriesMap.entries.mapIndexed { index, (category, amount) -> + return categoriesMap.entries.mapIndexed { index, (category, transList) -> + val totalAmount = transList.sumOf { it.amount } CategoryData( category = category, - amount = amount, - color = colors[index % colors.size] + amount = totalAmount, + color = colors[index % colors.size], + transactions = transList ) }.sortedByDescending { it.amount } } diff --git a/app/src/main/java/otus/homework/customview/PieChartView.kt b/app/src/main/java/otus/homework/customview/PieChartView.kt index 168f3537..3399a69c 100644 --- a/app/src/main/java/otus/homework/customview/PieChartView.kt +++ b/app/src/main/java/otus/homework/customview/PieChartView.kt @@ -4,8 +4,11 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Typeface import android.os.Parcel import android.os.Parcelable +import android.text.TextPaint import android.util.AttributeSet import android.view.MotionEvent import android.view.View @@ -14,6 +17,7 @@ import kotlin.math.cos import kotlin.math.pow import kotlin.math.sin import kotlin.math.sqrt +import androidx.core.graphics.withSave class PieChartView @JvmOverloads constructor( context: Context, @@ -22,7 +26,7 @@ class PieChartView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) { interface OnSectorClickListener { - fun onSectorClick(category: String) + fun onSectorClick(categoryData: CategoryData) } private var onSectorClickListener: OnSectorClickListener? = null @@ -32,26 +36,20 @@ class PieChartView @JvmOverloads constructor( } private var categoriesData = mutableListOf() - private val colors = arrayOf( - Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW, Color.CYAN, - Color.MAGENTA, Color.GRAY, Color.parseColor("#FFA500"), // Orange - Color.parseColor("#800080"), // Purple - Color.parseColor("#008080"), // Teal - Color.parseColor("#FF1493"), // Deep Pink - Color.parseColor("#00CED1") // Dark Turquoise - ) - private var totalAmount = 0.0 + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) - private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) private val selectedPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val centerPaint = Paint(Paint.ANTI_ALIAS_FLAG) - private var centerText = "" + private val legendPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) private var centerX = 0f private var centerY = 0f private var radius = 0f private var selectedSector = -1 + private var textSize = 0f + private var legendTextSize: Int = 0 init { setupPaints() @@ -61,13 +59,17 @@ class PieChartView @JvmOverloads constructor( paint.style = Paint.Style.FILL paint.strokeWidth = 2f - textPaint.color = Color.WHITE - textPaint.textSize = 36f + textPaint.color = Color.BLACK textPaint.textAlign = Paint.Align.CENTER + textPaint.typeface = Typeface.DEFAULT_BOLD + + legendPaint.color = Color.DKGRAY + legendPaint.textAlign = Paint.Align.LEFT + legendPaint.textSize = 36f - selectedPaint.style = Paint.Style.FILL - selectedPaint.color = Color.parseColor("#CCCCCC") - selectedPaint.strokeWidth = 3f + selectedPaint.style = Paint.Style.STROKE + selectedPaint.color = Color.BLACK + selectedPaint.strokeWidth = 4f centerPaint.color = Color.WHITE centerPaint.style = Paint.Style.FILL @@ -77,20 +79,12 @@ class PieChartView @JvmOverloads constructor( categoriesData.clear() categoriesData.addAll(categories) totalAmount = categoriesData.sumOf { it.amount } - updateCenterText() invalidate() requestLayout() } - private fun updateCenterText() { - if (categoriesData.isNotEmpty()) { - val topCategory = categoriesData.maxByOrNull { it.amount } - centerText = topCategory?.category ?: "" - } - } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val minSize = 400 + val minSize = dpToPx(400) val widthMode = MeasureSpec.getMode(widthMeasureSpec) val widthSize = MeasureSpec.getSize(widthMeasureSpec) @@ -119,7 +113,13 @@ class PieChartView @JvmOverloads constructor( centerX = w / 2f centerY = h / 2f - radius = minOf(w, h) * 0.4f + radius = minOf(w, h) * 0.3f + + textSize = radius * 0.1f + legendTextSize = dpToPx(12) + + textPaint.textSize = textSize + legendPaint.textSize = legendTextSize.toFloat() } override fun onDraw(canvas: Canvas) { @@ -130,31 +130,25 @@ class PieChartView @JvmOverloads constructor( return } - var startAngle = 0f + var startAngle = -90f for ((index, category) in categoriesData.withIndex()) { val sweepAngle = (category.amount / totalAmount * 360).toFloat() paint.color = category.color - if (index == selectedSector) { - val offsetRadius = radius * 1.1f - val offsetX = - (cos(Math.toRadians((startAngle + sweepAngle / 2).toDouble())) * offsetRadius * 0.1).toFloat() - val offsetY = - (sin(Math.toRadians((startAngle + sweepAngle / 2).toDouble())) * offsetRadius * 0.1).toFloat() + canvas.drawArc( + centerX - radius, + centerY - radius, + centerX + radius, + centerY + radius, + startAngle, + sweepAngle, + true, + paint + ) - canvas.drawArc( - centerX - offsetRadius + offsetX, - centerY - offsetRadius + offsetY, - centerX + offsetRadius + offsetX, - centerY + offsetRadius + offsetY, - startAngle, - sweepAngle, - true, - paint - ) - } else { + if (index == selectedSector) { canvas.drawArc( centerX - radius, centerY - radius, @@ -163,36 +157,46 @@ class PieChartView @JvmOverloads constructor( startAngle, sweepAngle, true, - paint + selectedPaint ) } - if (sweepAngle > 15) { + if (sweepAngle > 10) { val angle = startAngle + sweepAngle / 2 - val textRadius = radius * 0.7f + val textRadius = radius * 0.6f val x = centerX + cos(Math.toRadians(angle.toDouble())).toFloat() * textRadius val y = centerY + sin(Math.toRadians(angle.toDouble())).toFloat() * textRadius - canvas.save() - canvas.rotate(angle, x, y) - canvas.drawText( - category.category, - x, - y, - textPaint - ) - canvas.restore() + val displayText = if (category.category.length > 10) { + "${category.category.substring(0, 8)}..." + } else { + category.category + } + + canvas.withSave { + if (angle > 90 && angle < 270) { + rotate(angle + 180, x, y) + drawText(displayText, x, y, textPaint) + } else { + rotate(angle, x, y) + drawText(displayText, x, y, textPaint) + } + } } startAngle += sweepAngle } - canvas.drawCircle(centerX, centerY, radius * 0.3f, centerPaint) + canvas.drawCircle(centerX, centerY, radius * 0.2f, centerPaint) + + val totalText = "Всего:\n${String.format("%.0f", totalAmount)} руб." + val totalBounds = Rect() + textPaint.getTextBounds(totalText, 0, totalText.length, totalBounds) canvas.drawText( - centerText, + totalText, centerX, - centerY + textPaint.textSize / 3, + centerY - textPaint.descent(), textPaint.apply { color = Color.BLACK } ) } @@ -219,26 +223,22 @@ class PieChartView @JvmOverloads constructor( val distance = sqrt((x - centerX).pow(2) + (y - centerY).pow(2)) if (distance <= radius) { - var angle = (Math.toDegrees( - atan2( - (y - centerY).toDouble(), - (x - centerX).toDouble() - ) - ) + 360) % 360 - + var angle = + Math.toDegrees(atan2((y - centerY).toDouble(), (x - centerX).toDouble())) + angle = (angle + 360) % 360 angle = (angle + 90) % 360 - var startAngle = 0f + var currentAngle = 0f for ((index, category) in categoriesData.withIndex()) { val sweepAngle = (category.amount / totalAmount * 360).toFloat() - if (angle >= startAngle && angle < startAngle + sweepAngle) { + if (angle >= currentAngle && angle < currentAngle + sweepAngle) { selectedSector = index - onSectorClickListener?.onSectorClick(category.category) + onSectorClickListener?.onSectorClick(category) invalidate() return true } - startAngle += sweepAngle + currentAngle += sweepAngle } } selectedSector = -1 @@ -248,11 +248,14 @@ class PieChartView @JvmOverloads constructor( return super.onTouchEvent(event) } + private fun dpToPx(dp: Int): Int { + return (dp * resources.displayMetrics.density).toInt() + } + override fun onSaveInstanceState(): Parcelable { val superState = super.onSaveInstanceState() return SavedState(superState).apply { this.selectedSector = this@PieChartView.selectedSector - this.centerText = this@PieChartView.centerText } } @@ -261,7 +264,6 @@ class PieChartView @JvmOverloads constructor( if (savedState != null) { super.onRestoreInstanceState(savedState.superState) selectedSector = savedState.selectedSector - centerText = savedState.centerText } else { super.onRestoreInstanceState(state) } @@ -269,19 +271,16 @@ class PieChartView @JvmOverloads constructor( private class SavedState : BaseSavedState { var selectedSector: Int = -1 - var centerText: String = "" constructor(superState: Parcelable?) : super(superState) constructor(parcel: Parcel) : super(parcel) { selectedSector = parcel.readInt() - centerText = parcel.readString() ?: "" } override fun writeToParcel(out: Parcel, flags: Int) { super.writeToParcel(out, flags) out.writeInt(selectedSector) - out.writeString(centerText) } companion object { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 628ff428..d2348709 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,7 @@ + + + android:layout_height="0dp" + android:layout_weight="1"> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be259411..220aa627 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ Custom View Диаграмма расходов по категориям + Нажмите на сектор диаграммы для просмотра деталей… \ No newline at end of file