From 1f72b68a136bd724ae256b86a2eba3801b38b860 Mon Sep 17 00:00:00 2001 From: Avior Date: Wed, 20 Jul 2022 00:21:02 +0200 Subject: [PATCH] feat: Add Custom Chart Library --- charts/.gitignore | 1 + charts/README.md | 3 + charts/build.gradle | 40 ++++ charts/consumer-rules.pro | 0 charts/proguard-rules.pro | 21 ++ .../dzeio/charts/ExampleInstrumentedTest.kt | 24 +++ charts/src/main/AndroidManifest.xml | 5 + .../src/main/java/com/dzeio/charts/BarView.kt | 199 ++++++++++++++++++ .../java/com/dzeio/charts/ExampleUnitTest.kt | 17 ++ settings.gradle | 2 + 10 files changed, 312 insertions(+) create mode 100644 charts/.gitignore create mode 100644 charts/README.md create mode 100644 charts/build.gradle create mode 100644 charts/consumer-rules.pro create mode 100644 charts/proguard-rules.pro create mode 100644 charts/src/androidTest/java/com/dzeio/charts/ExampleInstrumentedTest.kt create mode 100644 charts/src/main/AndroidManifest.xml create mode 100644 charts/src/main/java/com/dzeio/charts/BarView.kt create mode 100644 charts/src/test/java/com/dzeio/charts/ExampleUnitTest.kt diff --git a/charts/.gitignore b/charts/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/charts/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/charts/README.md b/charts/README.md new file mode 100644 index 0000000..624acfa --- /dev/null +++ b/charts/README.md @@ -0,0 +1,3 @@ +# Dzeio Charts + +Originally from https://github.com/HackPlan/AndroidCharts but We wanted more options :D diff --git a/charts/build.gradle b/charts/build.gradle new file mode 100644 index 0000000..75a307e --- /dev/null +++ b/charts/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_6 + targetCompatibility JavaVersion.VERSION_1_6 + } + kotlinOptions { + jvmTarget = '1.6' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'com.google.android.material:material:1.6.1' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} diff --git a/charts/consumer-rules.pro b/charts/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/charts/proguard-rules.pro b/charts/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/charts/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/charts/src/androidTest/java/com/dzeio/charts/ExampleInstrumentedTest.kt b/charts/src/androidTest/java/com/dzeio/charts/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..ae841cb --- /dev/null +++ b/charts/src/androidTest/java/com/dzeio/charts/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.dzeio.charts + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.dzeio.charts.test", appContext.packageName) + } +} diff --git a/charts/src/main/AndroidManifest.xml b/charts/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e47d5d6 --- /dev/null +++ b/charts/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/charts/src/main/java/com/dzeio/charts/BarView.kt b/charts/src/main/java/com/dzeio/charts/BarView.kt new file mode 100644 index 0000000..a397137 --- /dev/null +++ b/charts/src/main/java/com/dzeio/charts/BarView.kt @@ -0,0 +1,199 @@ +package com.dzeio.charts + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.util.AttributeSet +import android.util.Log +import android.view.View +import kotlin.math.abs + +class BarView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : + View(context, attrs) { + + companion object { + const val TAG = "DzeioCharts/BarView" + } + + /** + * Nunber of entries displayed at the same time + */ + val numberOfEntries = 5 + + /** + * Number of labels displayed at the same time + */ + val numberOfLabels = 3 + + /** + * Spacing between entries + */ + val spacing = 22 + + /** + * top margin from the canvas + */ + @Deprecated("Not needed anymore, Use the parent Padding/Margin") + private val topMargin: Int = 5 + + private val textTopMargin = 5 + + private var barWidth: Int = 0 + + private val textColor = Color.parseColor("#9B9A9B") + + private val foregroundColor = Color.parseColor("#FC496D") + private val percentList: ArrayList = ArrayList() + private var targetPercentList: ArrayList = ArrayList() + private val textPaint: Paint = Paint().also { + it.isAntiAlias = true + it.color = textColor + it.textSize = 25f + it.textAlign = Paint.Align.CENTER + } + + private val fgPaint: Paint = Paint().also { + it.isAntiAlias = true + it.color = foregroundColor + } + private val rect: Rect = Rect() + private var bottomTextDescent = 0 + private var bottomTextHeight = 0 + private var bottomTextList: ArrayList? = ArrayList() + private val animator: Runnable = object : Runnable { + override fun run() { + var needNewFrame = false + for (i in targetPercentList.indices) { + if (percentList[i] < targetPercentList[i]) { + percentList[i] = percentList[i] + 0.02f + needNewFrame = true + } else if (percentList[i] > targetPercentList[i]) { + percentList[i] = percentList[i] - 0.02f + needNewFrame = true + } + if (abs(targetPercentList[i] - percentList[i]) < 0.02f) { + percentList[i] = targetPercentList[i] + } + } + if (needNewFrame) { + postDelayed(this, 20) + } + invalidate() + } + } + + /** + * dataList will be reset when called is method. + * + * @param bottomStringList The String ArrayList in the bottom. + */ + fun setBottomTextList(bottomStringList: ArrayList?) { + barWidth = measuredWidth / numberOfEntries - spacing + bottomTextList = bottomStringList + val r = Rect() + bottomTextDescent = 0 + for (s in bottomTextList!!) { + textPaint.getTextBounds(s, 0, s.length, r) + if (bottomTextHeight < r.height()) { + bottomTextHeight = r.height() + } + Log.d(TAG, measuredWidth.toString()) +// if (autoSetWidth && barWidth < r.width()) { +// barWidth = r.width() +// } + if (bottomTextDescent < abs(r.bottom)) { + bottomTextDescent = abs(r.bottom) + } + } + postInvalidate() + } + + /** + * @param list The ArrayList of Integer with the range of [0-max]. + */ + fun setDataList(list: ArrayList) { + barWidth = measuredWidth / numberOfEntries - spacing + // Calculate max + val max = list.reduce { acc, i -> if (acc > i) return@reduce acc else return@reduce i } + + targetPercentList = ArrayList() + for (integer in list) { + targetPercentList.add(1 - integer.toFloat() / max.toFloat()) + } + + // Make sure percentList.size() == targetPercentList.size() + if (percentList.isEmpty() || percentList.size < targetPercentList.size) { + val temp = targetPercentList.size - percentList.size + for (i in 0 until temp) { + percentList.add(1f) + } + } else if (percentList.size > targetPercentList.size) { + val temp = percentList.size - targetPercentList.size + for (i in 0 until temp) { + percentList.removeAt(percentList.size - 1) + } + } + + removeCallbacks(animator) + post(animator) + } + + override fun onDraw(canvas: Canvas) { + if (percentList.isNotEmpty()) { + for (i in 1 until percentList.size) { + val left = spacing * i + barWidth * (i - 1) + Log.d(TAG, "$spacing, $i, $barWidth = $left") + val right = (spacing + barWidth) * i + val bottom = height - bottomTextHeight - textTopMargin + val top = topMargin + ((bottom - topMargin) * percentList[i - 1]) + + rect.set(left, top.toInt(), right, bottom) + + canvas.drawRect(rect, fgPaint) + } + } + if (bottomTextList != null && !bottomTextList!!.isEmpty()) { + var i = 1 + for (s in bottomTextList!!) { + canvas.drawText( + s, + (spacing * i + barWidth * (i - 1) + barWidth / 2).toFloat(), + (height - bottomTextDescent).toFloat(), + textPaint + ) + i++ + } + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val mViewWidth = measureWidth(widthMeasureSpec) + val mViewHeight = measureHeight(heightMeasureSpec) + setMeasuredDimension(mViewWidth, mViewHeight) + } + + private fun measureWidth(measureSpec: Int): Int { + var preferred = 0 + if (bottomTextList != null) { + preferred = bottomTextList!!.size * (barWidth + spacing) + } + return getMeasurement(measureSpec, preferred) + } + + private fun measureHeight(measureSpec: Int): Int { + val preferred = 222 + return getMeasurement(measureSpec, preferred) + } + + private fun getMeasurement(measureSpec: Int, preferred: Int): Int { + val specSize = MeasureSpec.getSize(measureSpec) + val measurement = when (MeasureSpec.getMode(measureSpec)) { + MeasureSpec.EXACTLY -> specSize + MeasureSpec.AT_MOST -> Math.min(preferred, specSize) + else -> preferred + } + return measurement + } +} diff --git a/charts/src/test/java/com/dzeio/charts/ExampleUnitTest.kt b/charts/src/test/java/com/dzeio/charts/ExampleUnitTest.kt new file mode 100644 index 0000000..cf39879 --- /dev/null +++ b/charts/src/test/java/com/dzeio/charts/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.dzeio.charts + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/settings.gradle b/settings.gradle index b83db14..367a633 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,4 +15,6 @@ dependencyResolutionManagement { } } rootProject.name = "OpenHealth" + include ':app' +include ':charts'