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

add charts #44

Open
wants to merge 1 commit 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
5 changes: 3 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ plugins {
}

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 Down Expand Up @@ -42,4 +42,5 @@ dependencies {
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.google.code.gson:gson:2.9.0'
}
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
6 changes: 6 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,16 @@ package otus.homework.customview

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import otus.homework.customview.chart.pie.PieChart

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

findViewById<PieChart>(R.id.pie_chart).setOnClickListener {
Log.d(this::class.java.name, "$it")
}
}
}
11 changes: 11 additions & 0 deletions app/src/main/java/otus/homework/customview/PayloadItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package otus.homework.customview

data class PayloadItem(
val id: Int,
val name: String,
val amount: Int,
val category: String,
val time: Long,
val date: String = "",
val numberDay: Int = 0
)
249 changes: 249 additions & 0 deletions app/src/main/java/otus/homework/customview/chart/line/LineChart.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package otus.homework.customview.chart.line

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.CornerPathEffect
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import otus.homework.customview.PayloadItem
import otus.homework.customview.R
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Random

class LineChart(context: Context, attrs: AttributeSet) : View(context, attrs) {

private val minWidth = 400f
private val minHeight = 200f

private var width = minWidth
private var height = minHeight

private var countColumns = 0
private val countRows = 10
private val offsetBeforeFirstDate = 1
private val offsetAfterLastDate = 2

private var stepColumn = 0f
private var stepRow = 0f
private var maxPrice = 0
private var firstDay = 0L

private val lineChartItem = mutableListOf<LineChartItem>()

private val axisPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 5f

Choose a reason for hiding this comment

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

лучше переводить в dp

color = Color.GRAY
pathEffect = DashPathEffect(floatArrayOf(5f, 10f), 0f)
}

private val linePaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 10f
pathEffect = CornerPathEffect(8f)
}

private val priceLabels = Paint().apply {
textSize = 20f
color = Color.BLACK
textAlign = Paint.Align.RIGHT
}

private val dateLabels = Paint().apply {
textSize = 20f
color = Color.BLACK
textAlign = Paint.Align.LEFT
}

private val strokeOffset = axisPaint.strokeWidth / 2

init {

Choose a reason for hiding this comment

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

лучше перенести логику в onFinishInflate , при init может вызывать "лаги" при открытии экрана

val payloads = getPayloads()
val categories = payloads.groupBy(PayloadItem::category)
maxPrice = payloads.maxOf(PayloadItem::amount)
firstDay = payloads.minOf(PayloadItem::time)
categories.entries.forEach { category ->
val points = mutableListOf<PointF>()
val categoryByDays = category.value.groupBy { it.numberDay }
categoryByDays.entries.forEachIndexed { index, entry ->
val offsetStartColum = entry.key + offsetBeforeFirstDate
if (index == 0) {
val firstDay = if (offsetStartColum == 1) 0f
else offsetStartColum - 1
points.add(PointF(firstDay.toFloat(), 0f))
}
val sum = entry.value.sumBy { it.amount }
points.add(PointF(offsetStartColum.toFloat(), sum.toFloat()))
}

points.getOrNull(points.lastIndex)?.let {
points.add(PointF(it.x + 1, 0f))
}

lineChartItem.add(
LineChartItem(
categoryName = category.key,
points = points,
color = getColor()
)
)
}
val countDates = payloads.groupBy(PayloadItem::date)
countColumns = countDates.size + offsetBeforeFirstDate + offsetAfterLastDate
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)

val width = calcMeasureSize(widthMode, widthSize, minWidth.toInt())
val height = calcMeasureSize(heightMode, heightSize, minHeight.toInt()) - OFFSET_LABELS

this.width = width.toFloat()
this.height = height.toFloat()

stepColumn = (width / countColumns).toFloat()
stepRow = (height / countRows).toFloat()

setMeasuredDimension(width, height)
}


override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
drawAxis(canvas)
drawLines(canvas)
drawLabels(canvas)
}

private fun drawLabels(canvas: Canvas?) {
val dt = (maxPrice + (maxPrice * 0.1f)) / countRows
(0..countRows).reversed().forEachIndexed { index, value ->
canvas?.drawText(
(dt * value).toString(),
width - 5f,
index * stepRow - 5,
priceLabels
)
}

(0..countColumns).forEachIndexed { index, value ->
canvas?.drawText(
convertLongToTime(firstDay + (index * 24 * 60 * 60)),

Choose a reason for hiding this comment

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

я бы все же вынес это из draw , SimpleDateFormat довольно дорогой

index * stepColumn,
height + OFFSET_LABELS,
dateLabels
)
}
}

private fun drawAxis(canvas: Canvas?) {
(0 until countColumns + 1).forEachIndexed { index, column ->
val borderOffset =
if (index == 0) strokeOffset else if (index == column) -strokeOffset else 0
val pointX = column * stepColumn + borderOffset.toFloat()
canvas?.drawLine(
pointX,
0f,
pointX,
height,
axisPaint
)
}

(0 until countRows + 1).forEachIndexed { index, row ->
val borderOffset =
if (index == 0) strokeOffset else if (index == row) -strokeOffset else 0
val pointY = row * stepRow + borderOffset.toFloat()
canvas?.drawLine(
0f,
pointY,
width,
pointY,
axisPaint
)
}
}

private fun drawLines(canvas: Canvas?) {
lineChartItem.forEach { item ->
val path = Path()
item.points.forEachIndexed { index, point ->
if (index == 0) {
path.moveTo(
point.x * stepColumn,
height - point.y * stepRow
)
} else {

path.lineTo(
point.x * stepColumn - linePaint.strokeWidth / 4,
height - (height * point.y) / (maxPrice + (maxPrice * 0.1f))
)
}
}
linePaint.color = item.color
canvas?.drawPath(path, linePaint)
}
}

private fun getPayloads(): List<PayloadItem> = try {
val json = context.resources.openRawResource(R.raw.payload).bufferedReader()
.use { it.readText() }
val list: List<PayloadItem> =
Gson().fromJson(json, object : TypeToken<List<PayloadItem>>() {}.type)
var startDate = ""
list.sortedBy { it.time }.mapIndexed { index, payloadItem ->
val date = convertLongToTime(payloadItem.time)
val numberDay = if (index == 0) {
startDate = date
1
} else {
getCountDaysBetweenDates(startDate, date)
}
payloadItem.copy(date = date, numberDay = numberDay)
}
} catch (e: Exception) {
throw e

Choose a reason for hiding this comment

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

конкретизируйте нейминг

}

private fun calcMeasureSize(measureSpec: Int, size: Int, minSize: Int): Int =
when (measureSpec) {
MeasureSpec.UNSPECIFIED -> minSize
MeasureSpec.EXACTLY, MeasureSpec.AT_MOST -> minSize.coerceAtLeast(size)
else -> throw java.lang.Exception("not define")
}

private fun convertLongToTime(time: Long): String {
val date = Date(time * 1000)
val format = SimpleDateFormat(DATE_PATTERN)
return format.format(date)
}

private fun getCountDaysBetweenDates(startDate: String, endDate: String): Int {
val dateFormat = SimpleDateFormat(DATE_PATTERN)
val milliseconds =
(dateFormat.parse(endDate)?.time ?: 0L) - (dateFormat.parse(startDate)?.time ?: 0L)
return (milliseconds / (24 * 60 * 60 * 1000)).toInt() + 1
}

private fun getColor(): Int {
val rnd = Random()
return Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256))
}

companion object {
const val DATE_PATTERN = "dd.MM.yyyy"
const val OFFSET_LABELS = 20
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package otus.homework.customview.chart.line

import android.graphics.PointF

data class LineChartItem(
val categoryName: String,
val points: List<PointF>,
val color: Int
)
Loading