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

Canvas and custome view and viewgroup HW #60

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
30 changes: 15 additions & 15 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'
alias(libs.plugins.androidApplication)
alias(libs.plugins.kotlinAndroid)
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
namespace 'otus.homework.canvas'
compileSdk 33

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

Expand All @@ -34,12 +34,12 @@ android {

dependencies {

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'
implementation libs.core.ktx
implementation libs.appcompat
implementation libs.material
implementation libs.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.androidx.test.ext.junit
androidTestImplementation libs.espresso.core
implementation libs.gson
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package otus.homework.customview
package otus.homework.canvas

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
Expand All @@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("otus.homework.customview", appContext.packageName)
assertEquals("otus.homework.canvas", appContext.packageName)
}
}
16 changes: 10 additions & 6 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="otus.homework.customview">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.VIBRATE" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CustomView">
<activity android:name=".MainActivity">
android:theme="@style/Theme.HW_Canvas"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Expand Down
71 changes: 71 additions & 0 deletions app/src/main/java/otus/homework/canvas/JsonData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import android.content.Context
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import otus.homework.canvas.LineGraphData
import otus.homework.canvas.PieChartSectorData
import otus.homework.canvas.R
import java.text.SimpleDateFormat

class JsonData(private val context: Context) {

data class JsonData(
val id: Int,
val name: String,
val amount: Int,
val category: String,
val time: Int
)

fun getJsonData(): List<JsonData>? {
try {
val jsonFile = context.resources.openRawResource(R.raw.payload).bufferedReader()
val arrayTutorialType = object : TypeToken<List<JsonData>>() {}.type
val purchases = Gson().fromJson<List<JsonData>>(jsonFile, arrayTutorialType)
return purchases
} catch (error: Exception) {
return null
}
}

fun getPieChartSectorData(): List<PieChartSectorData>? {
val jsonDataArray = getJsonData() ?: return null
val pieChartSectorDataMap = mutableMapOf<String, PieChartSectorData>()
for (data in jsonDataArray) {
if (pieChartSectorDataMap.containsKey(data.category)) {
pieChartSectorDataMap[data.category]?.angle = pieChartSectorDataMap[data.category]?.angle!! + data.amount.toFloat()
}
else {
pieChartSectorDataMap += data.category to
PieChartSectorData(
angle = data.amount.toFloat(),
text = data.category,
category = data.category
)
}
}
val pieChartSectorData = mutableListOf<PieChartSectorData>()
for ((key, value) in pieChartSectorDataMap) {
pieChartSectorData += value
}
return pieChartSectorData
}


//private fun timeToString_(time: Int) = "$time" // TODO
private fun timeToString(time: Float) = SimpleDateFormat.getDateTimeInstance().format(time.toInt())

fun getLineGraphData(): MutableMap<String, List<LineGraphData>>? {
val jsonDataArray = getJsonData() ?: return null
var lineGraphData = mutableMapOf<String, MutableList<LineGraphData>>()
for (data in jsonDataArray) {
val addData = LineGraphData(data.time.toFloat(), data.amount.toFloat(), ::timeToString)
if (lineGraphData.containsKey(data.category)) {
lineGraphData[data.category]!!.add(addData)
} else {
lineGraphData += data.category to mutableListOf(addData)
}
}
return lineGraphData as MutableMap<String, List<LineGraphData>>
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/otus/homework/canvas/LineGraphData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package otus.homework.canvas

data class LineGraphData(
val valueX: Float,
val valueY: Float,
val xToString: (Float) -> String = {v -> "$v"}
)
169 changes: 169 additions & 0 deletions app/src/main/java/otus/homework/canvas/LineGraphView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package otus.homework.canvas

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Point
import android.graphics.PointF
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View


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

private var myBackgroundColor: Int = Color.rgb(50, 150, 50) // TO_DO xml app:backgroundColor
private var padding = 50F // TO_DO xml app:padding
private var graphData: MutableMap<String, List<LineGraphData>>? = null
private var scale = 1F
private var offset = PointF(0F, 0F)
private var scaleGestureDetector = ScaleGestureDetector(context, MyOnScaleGestureListener())
private var maxX = 0F
private var minX = 0F
private var maxY = 0F
private var minY = 0F
private val presetColors: IntArray = context.resources.getIntArray(R.array.presetColors)
private val textSize = 35F
private val strokeWidth = 10F
private var touchPosition: PointF? = null
private var touchSavedOffset = PointF(offset.x, offset.y)

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
scaleGestureDetector.onTouchEvent(event)
return when (event.action) {
MotionEvent.ACTION_DOWN -> {
touchSavedOffset = PointF(offset.x, offset.y)
touchPosition = PointF(event.x, event.y)
Log.d("***[", "touchPosition=$touchPosition")
true
}
MotionEvent.ACTION_MOVE -> {
offset.x = touchSavedOffset.x + event.x - touchPosition!!.x
offset.y = touchSavedOffset.y + event.y - touchPosition!!.y
invalidate();
Log.d("***[", "offset=$offset touchPosition=$touchPosition" )
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
touchPosition = null
Log.d("***[", "touchPosition=null")
true
}
else -> {
// TO?DO Нужно обрабатывать евенты 261 и 262
// Иначе при двойном касании когда первый и последний пальцы разные, дергается offset.
// offset резко меняется на разницу позиций пальцев. Пока TO?DO, хотя может это такая фича, а не бага :)
Log.d("***[", "action=${event.action}")
false
}
}
}

inner class MyOnScaleGestureListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
invalidate();
scale *= detector.scaleFactor
Log.d("***[", "scale=$scale")
return true
}
}

fun Int.modToString() = when(this) {
MeasureSpec.UNSPECIFIED -> "UNSPECIFIED"
MeasureSpec.EXACTLY->"EXACTLY"
MeasureSpec.AT_MOST->"AT_MOST"
else -> "---"
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMod = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMod = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
//Log.d("***[", "widthMod=${widthMod.modToString()} widthSize=$widthSize heightMod=${heightMod.modToString()} heightSize=$heightSize")
// График подстраивается под размер экрана, по этому size не меняем
setMeasuredDimension(widthSize,heightSize)
}

fun setData(graphData: MutableMap<String, List<LineGraphData>>?) {
this.graphData = graphData
invalidate()
maxX = 0F
minX = Float.MAX_VALUE
maxY = 0F
minY = Float.MAX_VALUE
offset = PointF(0F, 0F)
scale = 1F
graphData ?.let {
for (g in graphData) {
graphData[g.key] = g.value.sortedBy { it.valueX }
for (d in g.value) {
maxX = maxOf(maxX, d.valueX)
minX = minOf(minX, d.valueX)
maxY = maxOf(maxY, d.valueY)
minY = minOf(minY, d.valueY)
}
}
}
}

val paint = Paint().apply {
style = Paint.Style.STROKE
textSize = [email protected]
strokeWidth = strokeWidth
}

override fun onDrawForeground(canvas: Canvas) {
canvas.apply {
drawColor(myBackgroundColor)
graphData?.let {
var num = 0
for (d in it) {
showGraph(num++, d.key, d.value)
paint.strokeWidth = 2F
drawText(d.key, padding + 10, padding + 10 + textSize * 1.3F * num, paint)
paint.strokeWidth = strokeWidth
}
}
val path = Path()
path.moveTo(padding, padding)
path.lineTo(padding, height - padding)
path.lineTo(width - padding, height - padding)
paint.color = Color.BLACK
drawPath(path, paint)
}
}

private fun LineGraphData.getReX() = (valueX.toFloat() - minX) * (width - 2 * padding) / (maxX - minX) + padding
private fun LineGraphData.getReY() = height - ((valueY - minY) * (height- 2 * padding) / (maxY - minY) + padding)

private fun LineGraphData.log(num: Int) { Log.d("***[", "num=$num XY[$valueX,$valueY] -> [${getReX()} ${getReY()}]") }

private fun Canvas.showGraph(num: Int, category: String, data: List<LineGraphData>) {
val path = Path()
if (data.size == 1) {
path.addCircle(data[0].getReX(), data[0].getReY(), 3F, Path.Direction.CW)
} else {
path.moveTo(data[0].getReX(), data[0].getReY())
//data[0].log(num)
for (i in 1 until data.size) {
//data[i].log(num)
path.lineTo(data[i].getReX(), data[i].getReY())
}
}
val numColor = num % presetColors.size
paint.color = presetColors[numColor]
val matrix = Matrix()
matrix.setScale(scale, scale)
path.transform(matrix)
path.offset(offset.x, offset.y)
drawPath(path, paint)
}
}
31 changes: 31 additions & 0 deletions app/src/main/java/otus/homework/canvas/LinearAutoChangeLayout.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package otus.homework.canvas

import android.content.Context
import android.content.res.Configuration
import android.content.res.TypedArray
import android.util.AttributeSet
import android.util.Log
import android.widget.LinearLayout

class LinearAutoChangeLayout @JvmOverloads constructor(
context: Context, private val attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

private var isVerticalOrientation: Boolean? = null

init {
val typedArray: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LinearAutoChangeLayout)
isVerticalOrientation = "vertical" == typedArray.getString(R.styleable.LinearAutoChangeLayout_baseOrientation)
}


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

val isRealyVerticalOrientation: Boolean = resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
orientation = if (isVerticalOrientation == isRealyVerticalOrientation) VERTICAL else HORIZONTAL
Log.d("***[", "width=$widthSize height=$heightSize $isVerticalOrientation $orientation $isRealyVerticalOrientation")
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
Loading