Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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")
}
11 changes: 11 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,11 @@
package otus.homework.customview

import java.io.Serializable


data class CategoryModel(
val id: Int,
val name: String,
val amount: Int,
val category: String,
) : Serializable
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)
}
}
166 changes: 166 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,166 @@
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.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

private var data: Map<String, List<CategoryModel>> = emptyMap()
private val sectors = mutableListOf<Sector>()
private lateinit var rect: RectF

fun setData(items: Map<String, List<CategoryModel>>) {
data = items
calculateSectors()
invalidate()
}

init{
if (isInEditMode) {
//setValues(listOf(1,2,3,4,5))
}
}

override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()

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<Parcelable>("super_state")
super.onRestoreInstanceState(superState)

data = state.getSerializable("data") as HashMap<String, List<CategoryModel>>
invalidate()
} else {
super.onRestoreInstanceState(state)
}
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)

setMeasuredDimension(width, height)
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)

val size = minOf(w, h).toFloat()

val left = (w - size) / 2f
val top = (h - size) / 2f

rect = RectF(left, top, left + size, top + size)
}

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

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

if (sectors.isEmpty()) return

sectors.forEach {
paint.color = randomColor()

canvas.drawArc(
rect,
it.start,
it.end - it.start,
true,
paint
)
}
}

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 true

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
}

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"
}
}
30 changes: 30 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,30 @@
package otus.homework.customview

import android.content.Context
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,
)
}.groupBy { it.category }
}

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>