Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

custom view homework #54

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
7 changes: 5 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
plugins {
id 'com.android.application'
id 'kotlin-android'

id 'kotlin-parcelize'
}

android {
compileSdkVersion 30
compileSdkVersion 33
buildToolsVersion "30.0.3"

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

Expand All @@ -33,6 +35,7 @@ android {
}

dependencies {
implementation 'com.google.code.gson:gson:2.8.5'

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
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
File renamed without changes.
13 changes: 13 additions & 0 deletions app/src/main/java/otus/homework/customview/Expense.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package otus.homework.customview

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class Expense(
val id: Int,
val name: String,
val amount: Int,
val category: String,
val time: Int
) : Parcelable
51 changes: 50 additions & 1 deletion app/src/main/java/otus/homework/customview/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
package otus.homework.customview

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import otus.homework.customview.view.linechart.LineChartView
import otus.homework.customview.view.piechart.PieChartView
import java.io.IOException

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

/**
* Чтобы читать данные из файла payload.json нужно поставить isRead = true,
* чтобы сгенерировать случайные записи, поставить isRead = false
*/
val isRead = true

val expensesList = if (isRead) {
readDataFromJson()
} else {
RandomDataGenerator.generateRandomData()
}

INSTANCE = this

setContentView(R.layout.activity_main)
val pieChartView = findViewById<PieChartView>(R.id.pieChartView)
val lineChartView = findViewById<LineChartView>(R.id.lineChartView)

pieChartView.setData(expensesList)
pieChartView.setOnSliceClick { category ->
Toast.makeText(this, "category: $category", Toast.LENGTH_SHORT).show()
lineChartView.setItems(expensesList.filter { it.category == category })
}
}

private fun readDataFromJson(): List<Expense> {
lateinit var jsonString: String
try {
jsonString = resources.assets.open("payload.json")
.bufferedReader()
.use { it.readText() }
} catch (e: IOException) {
Log.d(this::class.java.name, e.message ?: "reading json error")
}

val expensesType = object : TypeToken<List<Expense>>() {}.type

return Gson().fromJson(jsonString, expensesType)
}

companion object {
lateinit var INSTANCE: MainActivity
}
}
74 changes: 74 additions & 0 deletions app/src/main/java/otus/homework/customview/RandomDataGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package otus.homework.customview

import java.util.*
import kotlin.random.Random

object RandomDataGenerator {

private const val CATEGORIES_AMOUNT_MIN = 5
private const val CATEGORIES_AMOUNT_MAX = 10

private const val PLACES_AMOUNT_MIN = 5
private const val PLACES_AMOUNT_MAX = 10

private const val MONEY_AMOUNT_MIN = 100
private const val MONEY_AMOUNT_MAX = 1000

private const val POINTS_AMOUNT_MIN = 50
private const val POINTS_AMOUNT_MAX = 100



private var expenseId = 0

fun generateRandomData(): MutableList<Expense> {

val randomDataList = mutableListOf<Expense>()

val randomHashMap = generateRandomMap()

repeat((POINTS_AMOUNT_MIN..POINTS_AMOUNT_MAX).random()) {
val category = randomHashMap.keys.random()
randomDataList.add(
Expense(
id = expenseId++,
name = randomHashMap[category]!!.random(),
amount = (MONEY_AMOUNT_MIN..MONEY_AMOUNT_MAX).random(),
category = category,
time = getRandomTime()
)
)
}
return randomDataList
}

private fun getRandomTime(): Int {
return (Date().time / 1000 + (-30..30).random() * 86400).toInt()
}

private fun generateRandomMap(): HashMap<String, List<String>> {
val randomMap = hashMapOf<String, List<String>>()
val randomStringList = generateRandomList()
randomStringList.map { randomString ->
repeat((PLACES_AMOUNT_MIN..PLACES_AMOUNT_MAX).random()) {
randomMap[randomString] = generateRandomList()
}
}
return randomMap
}

private fun generateRandomList(): List<String> {
val randomStringList = mutableListOf<String>()
repeat((CATEGORIES_AMOUNT_MIN..CATEGORIES_AMOUNT_MAX).random()) {
randomStringList.add(generateRandomString())
}
return randomStringList
}

private fun generateRandomString(): String {
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
return (1..5)
.map { Random.nextInt(0, charPool.size).let { charPool[it] } }
.joinToString("")
}
}
63 changes: 63 additions & 0 deletions app/src/main/java/otus/homework/customview/view/PaintGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package otus.homework.customview.view

import android.graphics.Paint
import androidx.core.content.ContextCompat
import otus.homework.customview.MainActivity
import otus.homework.customview.R

object PaintGenerator {

private val colorList =
listOf(
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_0
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_1
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_2
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_3
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_4
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_5
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_6
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_7
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_8
),
ContextCompat.getColor(
MainActivity.INSTANCE.applicationContext,
R.color.pie_chart_color_9
)
)

fun getPaint(index: Int): Paint {
return Paint().apply {
isAntiAlias = true
color = colorList[index % colorList.size]
style = Paint.Style.STROKE
strokeWidth = MainActivity.INSTANCE.applicationContext.resources
.getDimensionPixelSize(R.dimen.pie_chart_stroke_width).toFloat()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package otus.homework.customview.view.linechart

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.View
import otus.homework.customview.Expense
import otus.homework.customview.R
import otus.homework.customview.view.PaintGenerator.getPaint

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

private var sizeX = MIN_SIZE
private var sizeY = MIN_SIZE

private var groupedItems: List<Expense> = emptyList()

private var rectF: RectF = RectF()
private val strokeSize = context.resources
.getDimensionPixelSize(R.dimen.line_chart_stroke_width).toFloat()

private val axisPaint = getPaint(0).apply {
isDither = true
color = Color.BLACK
strokeWidth = strokeSize / 4
}
private val linePaint = getPaint(1).apply {
isDither = true
strokeWidth = strokeSize / 4
}
private val dotPaint = getPaint(2).apply {
isDither = true
style = Paint.Style.FILL_AND_STROKE
strokeWidth = strokeSize
}

fun setItems(list: List<Expense>) {
groupedItems = list
invalidate()
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize = calculateSize(widthMeasureSpec)
val heightSize = calculateSize(heightMeasureSpec)

sizeX = resolveSize(widthSize, widthMeasureSpec)
sizeY = resolveSize(heightSize, heightMeasureSpec)

setMeasuredDimension(sizeX, sizeY)
}

private fun calculateSize(measureSpec: Int): Int {
val size = MeasureSpec.getSize(measureSpec)

return when (MeasureSpec.getMode(measureSpec)) {
MeasureSpec.UNSPECIFIED -> MIN_SIZE
MeasureSpec.EXACTLY, MeasureSpec.AT_MOST -> Integer.max(MIN_SIZE, size)
else -> throw IllegalStateException("Invalid MeasureSpec mode")
}
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val startTop = 0f
val startLeft = 0f
val endBottom = height.toFloat()
val rightBottom = height.toFloat()

rectF.apply {
top = startTop
left = startLeft + strokeSize
bottom = endBottom
right = rightBottom - strokeSize
}

canvas.drawLine(0f, 0f, 0f, height.toFloat(), axisPaint)
canvas.drawLine(0f, height.toFloat(), width.toFloat(), height.toFloat(), axisPaint)

if (groupedItems.isNotEmpty()) {
val maxTime = groupedItems.maxOf { it.time }
val maxAmount = groupedItems.maxOf { it.amount }

val minTime = groupedItems.minOf { it.time }
val minAmount = groupedItems.minOf { it.amount }

var startX = 0f
var startY = height.toFloat()
groupedItems.sortedBy { it.time }.forEach {
val pxX =
width.toFloat() * (it.time - minTime).toFloat() / (maxTime - minTime).toFloat()
val pxY =
height.toFloat() * (it.amount - minAmount).toFloat() / (maxAmount - minAmount).toFloat()
canvas.drawLine(startX, startY, pxX, pxY, linePaint)
canvas.drawPoint(pxX, pxY, dotPaint)
startX = pxX
startY = pxY
}
}

}

override fun onSaveInstanceState(): Parcelable {
val bundle = Bundle().apply {
putParcelableArrayList(EXPENSES_TAG, ArrayList(groupedItems))
putParcelable(BUNDLE_TAG, super.onSaveInstanceState())
}
return bundle
}

override fun onRestoreInstanceState(state: Parcelable?) {
if (state is Bundle) {
groupedItems = state.getParcelableArrayList<Expense>(EXPENSES_TAG) as List<Expense>

super.onRestoreInstanceState(state.getParcelable(BUNDLE_TAG))
}
}

companion object {
private const val MIN_SIZE = 300
private const val STR = 300

private const val EXPENSES_TAG = "expenses_tag"
private const val BUNDLE_TAG = "bundle_tag"
}
}
Loading