Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
9 changes: 9 additions & 0 deletions app/src/main/java/otus/homework/customview/CategoryModel.kt
Original file line number Diff line number Diff line change
@@ -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,
)
15 changes: 15 additions & 0 deletions app/src/main/java/otus/homework/customview/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<PieChart>(R.id.pieChartView)
val contentNameTextView = findViewById<TextView>(R.id.categoryName)

val data = viewModel.data
pieChartView.onSliceClick = { categoryName ->
contentNameTextView.text = categoryName
}

pieChartView.setData(data)
}
}
133 changes: 133 additions & 0 deletions app/src/main/java/otus/homework/customview/PieChart.kt
Original file line number Diff line number Diff line change
@@ -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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не реализован механизм сохранения состояния, нужно сделать onSaveInstanceState и onRestoreInstanceState

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил, спасибо

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<CategoryModel> = emptyList()
private val sectors = mutableListOf<Sector>()

fun setData(items: List<CategoryModel>) {
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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не нужно вызывать setMeasuredDimension отдельно для высоты и ширины, плюс можно все упростить через resolveSize, примерно так:

    val measuredWidth = resolveSize(desiredWidth, widthMeasureSpec)
    val measuredHeight = resolveSize(desiredHeight, heightMeasureSpec)
    val size = min(measuredWidth, measuredHeight)
    setMeasuredDimension(size, size)

desiredWidth и desiredHeight желаемые размеры

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

это не надо вызывать

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил

}

val paint = Paint().apply {
style = Paint.Style.FILL
}

val rect = RectF(0f, 0f, pieWith, pieHeight)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

график всегда 200 на 200 и не подстраивается под размеры вью, нужно реализовать onSizeChanged и в нем определить размеры rect

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил


@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут при нулевом total можем получить деление на 0

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил


canvas.drawArc(rect, startAngle, swipeAngle, true, paint)

sectors.add(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

в onDraw нельзя вычислять сектора, это нужно сделать заранее, он вызывается очень часто

@SuppressLint("DrawAllocation") не нужно подавлять, это реальная проблема производительности

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут лучше возвращать true иначе событие может уйти родителю

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил


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
}
}
40 changes: 40 additions & 0 deletions app/src/main/java/otus/homework/customview/PieViewModel.kt
Original file line number Diff line number Diff line change
@@ -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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут каждый payload превращается в отдельный сектор, а данные должны агрегироваться по категориям

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил

.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 <T : ViewModel> create(modelClass: Class<T>): T {
val jsonMapper = JsonMapper(context.applicationContext)
return PieViewModel(jsonMapper) as T
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/otus/homework/customview/Sector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package otus.homework.customview

data class Sector(
val category: String,
val start: Float,
val end: Float
)
20 changes: 20 additions & 0 deletions app/src/main/java/otus/homework/customview/data/JsonMapper.kt
Original file line number Diff line number Diff line change
@@ -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<CategoryDTO> {
val json = context.resources
.openRawResource(R.raw.payload)
.bufferedReader()
.use { it.readText() }

val type = object : TypeToken<List<CategoryDTO>>() {}.type
return Gson().fromJson(json, type)
}
}
8 changes: 8 additions & 0 deletions app/src/main/java/otus/homework/customview/dto/CategoryDTO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package otus.homework.customview.dto

data class CategoryDTO(
val id: Int,
val name: String,
val amount: Int,
val category: String,
)
24 changes: 13 additions & 11 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<otus.homework.customview.PieChart
android:id="@+id/pieChartView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:id="@+id/categoryName"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>