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..7da45eff --- /dev/null +++ b/app/src/main/java/otus/homework/customview/CategoryModel.kt @@ -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 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..83eacb1f --- /dev/null +++ b/app/src/main/java/otus/homework/customview/PieChart.kt @@ -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( + 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> = emptyMap() + private val sectors = mutableListOf() + private lateinit var rect: RectF + + fun setData(items: Map>) { + 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("super_state") + super.onRestoreInstanceState(superState) + + data = state.getSerializable("data") as HashMap> + 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" + } +} \ 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..d1da1994 --- /dev/null +++ b/app/src/main/java/otus/homework/customview/PieViewModel.kt @@ -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() + .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 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