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
38 changes: 22 additions & 16 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'org.jetbrains.kotlin.android'
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
namespace 'otus.homework.customview'
compileSdk 35

defaultConfig {
applicationId "otus.homework.customview"
minSdkVersion 23
targetSdkVersion 30
minSdk 23
targetSdk 34
versionCode 1
versionName "1.0"

Expand All @@ -23,23 +23,29 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '17'
}

buildFeatures {
viewBinding true
}
}

dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.code.gson:gson:2.10.1'

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import org.junit.Assert.*
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("otus.homework.customview", appContext.packageName)
}
Expand Down
7 changes: 4 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="otus.homework.customview">

<application
Expand All @@ -10,7 +9,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CustomView">
<activity android:name=".MainActivity">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
55 changes: 53 additions & 2 deletions app/src/main/java/otus/homework/customview/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,62 @@
package otus.homework.customview

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import otus.homework.customview.databinding.ActivityMainBinding
import otus.homework.customview.view.PieChartView
import android.graphics.Color

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

val data = loadExpenses()
initPieChartView(data)
initLineChartView(data)
}

private fun loadExpenses(): List<PriceData> {
val json = resources.openRawResource(R.raw.payload)
.bufferedReader()
.use { it.readText() }
val type = object : TypeToken<List<PriceData>>() {}.type
return Gson().fromJson(json, type)
}

private fun initLineChartView(data: List<PriceData>) {
binding.lineChart.setData(data, getColorsForGradient())
}

private fun initPieChartView(data: List<PriceData>) {
binding.pieChart.setData(data, getColorsForGradient())
binding.pieChart.applyCallback(object : PieChartView.Callback {
override fun onSectorClick(priceData: PriceData) {
Toast.makeText(
this@MainActivity,
"${priceData.name} - ${priceData.amount} USD",
Toast.LENGTH_SHORT
).show()
}
})
}

private fun getColorsForGradient() = listOf(
Color.RED to Color.rgb(255, 165, 0),
Color.BLUE to Color.CYAN,
Color.rgb(138, 43, 226) to Color.rgb(255, 105, 180),
Color.GREEN to Color.rgb(50, 205, 50),
Color.YELLOW to Color.rgb(255, 215, 0),
Color.rgb(255, 0, 0) to Color.rgb(255, 182, 193),
Color.rgb(64, 224, 208) to Color.rgb(0, 191, 255),
Color.BLACK to Color.rgb(169, 169, 169),
Color.rgb(0, 0, 139) to Color.rgb(135, 206, 250),
Color.rgb(128, 0, 128) to Color.rgb(230, 230, 250)
).shuffled()
}
9 changes: 9 additions & 0 deletions app/src/main/java/otus/homework/customview/PriceData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package otus.homework.customview

data class PriceData(
val id: Long,
val name: String,
val amount: Long,
val category: String,
val time: Long,
)
202 changes: 202 additions & 0 deletions app/src/main/java/otus/homework/customview/view/CategoryChartView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package otus.homework.customview.view

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.view.View
import otus.homework.customview.PriceData
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import androidx.core.graphics.toColorInt

class CategoryChartView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = "#9C27B0".toColorInt()
strokeWidth = 4f
style = Paint.Style.STROKE
}

private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = "#9C27B0".toColorInt()
alpha = 50
style = Paint.Style.FILL
}

private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
textSize = 30f
textAlign = Paint.Align.CENTER
}

private val axisLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.GRAY
strokeWidth = 2f
}

private var data: List<PriceData> = listOf()
private var category: String = ""
private var maxAmount = 0.0
private var dateFormat = SimpleDateFormat("dd.MM", Locale.getDefault())
private var chartPath = Path()
private var fillPath = Path()

private var paddingHorizontal = 100f
private var paddingVertical = 100f
private var chartWidth = 0f
private var chartHeight = 0f

fun setData(newData: List<PriceData>, category: String) {
this.data = newData.filter { it.category == category }
this.category = category
calculateChartDimensions()
invalidate()
}

private fun calculateChartDimensions() {
if (data.isEmpty()) return

maxAmount = data.groupBy { it.time * 1000L }
.mapValues { it.value.sumOf { expense -> expense.amount } }
.maxOf { it.value }.toDouble()
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom

val width = resolveSize(desiredWidth, widthMeasureSpec)
val height = resolveSize(desiredHeight, heightMeasureSpec)

setMeasuredDimension(width, height)
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
chartWidth = w - 2 * paddingHorizontal
chartHeight = h - 2 * paddingVertical
}

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

if (data.isEmpty()) return

canvas.drawLine(
paddingHorizontal,
height - paddingVertical,
width - paddingHorizontal,
height - paddingVertical,
axisLinePaint
)

canvas.drawLine(
paddingHorizontal,
paddingVertical,
paddingHorizontal,
height - paddingVertical,
axisLinePaint
)

val dailyExpenses = data.groupBy { it.time * 1000L }
.mapValues { it.value.sumOf { expense -> expense.amount } }
.toSortedMap()
Copy link
Contributor

Choose a reason for hiding this comment

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

Каждый вызов groupBy, mapValues и toSortedMap создаёт новые коллекции. Лучше выносить эту логику из onDraw в setData


if (dailyExpenses.isEmpty()) return

val timeRange = dailyExpenses.lastKey() - dailyExpenses.firstKey()
val xScale = chartWidth / timeRange
val yScale = chartHeight / maxAmount

chartPath.reset()
fillPath.reset()

var isFirst = true
dailyExpenses.forEach { (time, amount) ->
val x = paddingHorizontal + (time - dailyExpenses.firstKey()) * xScale
val y = height - paddingVertical - (amount * yScale).toFloat()

if (isFirst) {
chartPath.moveTo(x, y)
fillPath.moveTo(x, height - paddingVertical)
fillPath.lineTo(x, y)
isFirst = false
} else {
chartPath.lineTo(x, y)
fillPath.lineTo(x, y)
}
}

fillPath.lineTo(
paddingHorizontal + chartWidth,
height - paddingVertical
)
fillPath.close()

canvas.drawPath(fillPath, fillPaint)
canvas.drawPath(chartPath, paint)

val dateStep = timeRange / 5
for (i in 0..5) {
val time = dailyExpenses.firstKey() + dateStep * i
val x = paddingHorizontal + (time - dailyExpenses.firstKey()) * xScale
val date = Date(time)
Copy link
Contributor

Choose a reason for hiding this comment

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

Каждая итерация создаёт объект Date. Это тоже плохо. В onDraw нельзя создавать новые объекты.

canvas.drawText(
dateFormat.format(date),
x,
height - paddingVertical + 40,
textPaint
)
}

val amountStep = maxAmount / 5
for (i in 0..5) {
val amount = amountStep * i
val y = height - paddingVertical - (amount * yScale).toFloat()
canvas.drawText(
String.format("%.0f", amount),
paddingHorizontal - 40,
y + textPaint.textSize/2,
textPaint
)
}

textPaint.textSize = 40f
canvas.drawText(
category,
width / 2f,
paddingVertical - 40,
textPaint
)
}

override fun onSaveInstanceState(): Parcelable {
val bundle = Bundle()
bundle.putParcelable("superState", super.onSaveInstanceState())
bundle.putString("category", category)
bundle.putFloat("chartWidth", chartWidth)
bundle.putFloat("chartHeight", chartHeight)
return bundle
}

override fun onRestoreInstanceState(state: Parcelable?) {
if (state is Bundle) {
category = state.getString("category", "")
chartWidth = state.getFloat("chartWidth")
chartHeight = state.getFloat("chartHeight")
super.onRestoreInstanceState(state.getParcelable("superState"))
} else {
super.onRestoreInstanceState(state)
}
}
}
Loading