1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-22 19:02:16 +00:00

feat: Add daily Steps goal

This commit is contained in:
Florian Bouillon 2022-12-17 21:15:20 +01:00
parent 9d3585c42c
commit 1e4267731b
Signed by: Florian Bouillon
GPG Key ID: BEEAF3722D0EBF64
21 changed files with 336 additions and 64 deletions

View File

@ -58,7 +58,7 @@ android {
targetSdk = sdkTarget
// Semantic Versioning
versionName = "1.0.0"
versionName = "0.1.0"
versionCode = 1
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -82,6 +82,7 @@ android {
getByName("release") {
// Slimmer version
isMinifyEnabled = true
isDebuggable = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("release")
}
@ -129,7 +130,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.7.0-alpha01")
implementation("javax.inject:javax.inject:1")
implementation("com.google.android.material:material:1.8.0-alpha03")
implementation("com.google.android.material:material:1.8.0-beta01")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
@ -173,13 +174,13 @@ dependencies {
kapt("com.google.dagger:hilt-compiler:2.43.2")
// Google Fit
implementation("com.google.android.gms:play-services-fitness:21.1.0")
implementation("com.google.android.gms:play-services-auth:20.4.0")
implementation("androidx.health.connect:connect-client:1.0.0-alpha07")
// implementation("com.google.android.gms:play-services-fitness:21.1.0")
// implementation("com.google.android.gms:play-services-auth:20.4.0")
// implementation("androidx.health.connect:connect-client:1.0.0-alpha08")
// Samsung Health
implementation(files("libs/samsung-health-data-1.5.0.aar"))
implementation("com.google.code.gson:gson:2.9.1")
// implementation(files("libs/samsung-health-data-1.5.0.aar"))
// implementation("com.google.code.gson:gson:2.9.1")
// ROOM
implementation("androidx.room:room-runtime:2.4.3")
@ -195,6 +196,7 @@ dependencies {
// OSS Licenses
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
// Open Food Fact
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")

View File

@ -26,4 +26,9 @@ object Settings {
* format in which the weight the user want it to be displayed as
*/
const val MASS_UNIT = "com.dzeio.open-health.unit.mass"
/**
* Goal number of steps each days
*/
const val STEPS_GOAL = "com.dzeio.open-health.steps.goal-daily"
}

View File

@ -19,7 +19,7 @@ class StepsAdapter() : BaseAdapter<Step, LayoutItemListBinding>() {
item: Step,
position: Int
) {
holder.binding.value.text = "${item.value}steps"
holder.binding.value.text = "${item.value} steps"
holder.binding.datetime.text = item.formatTimestamp()
holder.binding.edit.setOnClickListener {
onItemClick?.invoke(item)

View File

@ -8,7 +8,7 @@ data class OFFProduct(
@SerializedName("product_name")
var name: String,
@SerializedName("serving_quantity")
@SerializedName("serving_size")
var serving: String,
@SerializedName("nutriments")

View File

@ -11,13 +11,16 @@ interface StepDao : BaseDao<Step> {
@Query("SELECT * FROM Step ORDER BY timestamp DESC")
fun getAll(): Flow<List<Step>>
@Query("SELECT * FROM Step where id = :weightId")
@Query("SELECT * FROM Step WHERE timestamp >= :time")
fun getAfter(time: Long): Flow<List<Step>>
@Query("SELECT * FROM Step WHERE id = :weightId")
fun getOne(weightId: Long): Flow<Step?>
@Query("Select count(*) from Step")
@Query("SELECT count(*) FROM Step")
fun getCount(): Flow<Int>
@Query("Select * FROM Step ORDER BY timestamp DESC LIMIT 1")
@Query("SELECT * FROM Step ORDER BY timestamp DESC LIMIT 1")
fun last(): Flow<Step?>
@Query("DELETE FROM Step where source = :source")

View File

@ -2,7 +2,8 @@ package com.dzeio.openhealth.data.step
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withTimeoutOrNull
import java.util.Calendar
import java.util.TimeZone
import javax.inject.Inject
import javax.inject.Singleton
@ -23,7 +24,13 @@ class StepRepository @Inject constructor(
suspend fun deleteFromSource(value: String) = stepDao.deleteFromSource(value)
suspend fun todaySteps(): Int {
val steps = getSteps().firstOrNull()
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
cal.set(Calendar.HOUR, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0)
val steps = stepDao.getAfter(cal.timeInMillis).firstOrNull()
if (steps == null) {
return 0
}

View File

@ -27,7 +27,7 @@ class AboutFragment : BaseStaticFragment<FragmentAboutBinding>() {
resources.getString(R.string.version_number, BuildConfig.VERSION_NAME)
binding.contactUs.setOnClickListener {
openLink("mailto:context.openhealth@dze.io")
openLink("mailto:contact.openhealth@dze.io")
}
binding.github.setOnClickListener {

View File

@ -92,18 +92,29 @@ class BrowseFragment :
}
viewModel.steps.observe(viewLifecycleOwner) {
binding.stepsText.setText("$it of xxx steps")
updateStepsText(it, viewModel.stepsGoal.value)
}
viewModel.stepsGoal.observe(viewLifecycleOwner) {
updateStepsText(viewModel.steps.value, it)
}
viewModel.weight.observe(viewLifecycleOwner) {
binding.weightText.setText(
String.format(
resources.getString(R.string.weight_current),
it,
resources.getString(R.string.unit_mass_kilogram_unit)
String.format(resources.getString(R.string.unit_mass_kilogram_unit), it)
)
)
}
}
private fun updateStepsText(numberOfSteps: Int?, goal: Int?) {
var text = "${numberOfSteps ?: 0} steps"
if (goal != null) {
text = "${numberOfSteps ?: 0} of $goal steps"
}
binding.stepsText.setText(text)
}
}

View File

@ -3,9 +3,11 @@ package com.dzeio.openhealth.ui.browse
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.dzeio.openhealth.Settings
import com.dzeio.openhealth.core.BaseViewModel
import com.dzeio.openhealth.data.step.StepRepository
import com.dzeio.openhealth.data.weight.WeightRepository
import com.dzeio.openhealth.utils.Configuration
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -14,12 +16,16 @@ import javax.inject.Inject
@HiltViewModel
class BrowseViewModel @Inject internal constructor(
stepRepository: StepRepository,
weightRepository: WeightRepository
weightRepository: WeightRepository,
private val config: Configuration
) : BaseViewModel() {
private val _steps = MutableLiveData(0)
val steps: LiveData<Int> = _steps
private val _stepsGoal: MutableLiveData<Int?> = MutableLiveData()
val stepsGoal: LiveData<Int?> = _stepsGoal
private val _weight = MutableLiveData(0f)
val weight: LiveData<Float> = _weight
@ -36,5 +42,9 @@ class BrowseViewModel @Inject internal constructor(
_weight.postValue(it.weight)
}
}
this._stepsGoal.postValue(
config.getInt(Settings.STEPS_GOAL).value
)
}
}

View File

@ -91,5 +91,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
return@setOnPreferenceChangeListener true
}
val stepsGoalPreference = findPreference<EditTextPreference>(Settings.STEPS_GOAL)
stepsGoalPreference.apply {
}
}
}

View File

@ -64,12 +64,16 @@ class StepsHomeFragment :
)
}
val errorColor = MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorError
)
chart.apply {
series = arrayListOf(serie)
// debug = true
yAxis.apply {
setYMax(500f)
textLabel.color = MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorOnPrimaryContainer
@ -78,14 +82,15 @@ class StepsHomeFragment :
requireView(),
com.google.android.material.R.attr.colorOnPrimaryContainer
)
goalLinePaint.color = errorColor
//
onValueFormat = { value -> "${value.toInt()}" }
}
xAxis.apply {
increment = 3600000.0
increment = 86400000.0
// displayCount = 168
displayCount = 10
// displayCount = 10
textPaint.color = MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorOnPrimaryContainer
@ -103,6 +108,11 @@ class StepsHomeFragment :
}
}
viewModel.goal.observe(viewLifecycleOwner) {
chart.yAxis.setGoalLine(it?.toFloat())
chart.refresh()
}
viewModel.items.observe(viewLifecycleOwner) { list ->
adapter.set(list)
@ -110,12 +120,7 @@ class StepsHomeFragment :
return@observe
}
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
cal.set(Calendar.HOUR, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0)
// chart.animation.enabled = false
// chart.animation.refreshRate = 60
@ -125,11 +130,33 @@ class StepsHomeFragment :
// chart.xAxis.labels.size = 32f
serie.entries = list.reversed().map {
return@map Entry(it.timestamp.toDouble(), it.value.toFloat())
} as ArrayList<Entry>
val entries: HashMap<Long, Entry> = HashMap()
chart.xAxis.x = serie.entries.first().x
list.forEach {
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
cal.timeInMillis = it.timestamp
cal.set(Calendar.HOUR, 0)
cal.set(Calendar.AM_PM, Calendar.AM)
val ts = cal.timeInMillis
if (!entries.containsKey(ts)) {
entries[ts] = Entry((ts).toDouble(), 0F, errorColor)
}
entries[ts]!!.y += it.value.toFloat()
if (viewModel.goal.value != null) {
if (entries[ts]!!.y > viewModel.goal.value!!) {
entries[ts]!!.color = null
}
} else {
entries[ts]!!.color = null
}
}
serie.entries = ArrayList(entries.values)
chart.xAxis.x = chart.xAxis.getXMax()
chart.refresh()

View File

@ -1,10 +1,13 @@
package com.dzeio.openhealth.ui.steps
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.dzeio.openhealth.Settings
import com.dzeio.openhealth.core.BaseViewModel
import com.dzeio.openhealth.data.step.Step
import com.dzeio.openhealth.data.step.StepRepository
import com.dzeio.openhealth.utils.Configuration
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -12,15 +15,23 @@ import javax.inject.Inject
@HiltViewModel
class StepsHomeViewModel@Inject internal constructor(
private val stepRepository: StepRepository
private val stepRepository: StepRepository,
private val config: Configuration
) : BaseViewModel() {
val items: MutableLiveData<List<Step>> = MutableLiveData()
private val _goal: MutableLiveData<Int?> = MutableLiveData()
val goal: LiveData<Int?> = _goal
fun init() {
viewModelScope.launch {
stepRepository.getSteps().collectLatest {
items.postValue(it)
}
}
this._goal.postValue(
config.getInt(Settings.STEPS_GOAL).value
)
}
}
}

View File

@ -0,0 +1,87 @@
package com.dzeio.openhealth.utils.fields
import android.content.Context
import android.text.InputType
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.widget.EditText
import androidx.preference.EditTextPreference
class IntEditTextPreference : EditTextPreference, EditTextPreference.OnBindEditTextListener {
private var txt: String? = null
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
setOnBindEditTextListener(this)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
setOnBindEditTextListener(this)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
setOnBindEditTextListener(this)
}
constructor(context: Context) : super(context) {
setOnBindEditTextListener(this)
}
/**
* Saves the text to the current data storage.
*
* @param text The text to save
*/
override fun setText(text: String?) {
val wasBlocking = shouldDisableDependents()
val pouet = Integer.parseInt(text.toString())
this.txt = text
pouet::class.simpleName?.let { Log.d("pouet2", it) }
persistInt(pouet)
val isBlocking = shouldDisableDependents()
if (isBlocking != wasBlocking) {
notifyDependencyChange(isBlocking)
}
notifyChanged()
}
override fun getText(): String? {
return this.txt
}
override fun onSetInitialValue(defaultValue: Any?) {
var value: Int
if (defaultValue != null) {
val strDefaultValue = defaultValue as String
val defaultIntValue = strDefaultValue.toInt()
value = getPersistedInt(defaultIntValue)
} else {
try {
value = getPersistedInt(0)
} catch (e: ClassCastException) {
value = 0
}
}
text = value.toString()
}
override fun shouldDisableDependents(): Boolean {
return TextUtils.isEmpty(text) || super.shouldDisableDependents()
}
override fun onBindEditText(editText: EditText) {
editText.inputType = InputType.TYPE_CLASS_NUMBER
}
}

View File

@ -13,11 +13,11 @@
<!-- Units -->
<string name="unit_mass_kilogram_name_singular">Kilogram</string>
<string name="unit_mass_kilogram_name_plural">Kilograms</string>
<string name="unit_mass_kilogram_unit" translatable="false">kg</string>
<string name="unit_mass_kilogram_unit" translatable="false">%1$skg</string>
<string name="unit_mass_pound_name_singular">Pound</string>
<string name="unit_mass_pound_name_plural">Pounds</string>
<string name="unit_mass_pound_unit" translatable="false">lbs</string>
<string name="unit_mass_pound_unit" translatable="false">%1$slbs</string>
<string name="unit_volume_milliliter_name_singular">Milliliter</string>
<string name="unit_volume_milliliter_name_plural">Milliliters</string>
@ -52,7 +52,7 @@
<string name="permission_declined">You declined a permission, you can\'t use this extension unless you enable it manually</string>
<string name="edit_daily_goal">Modifiy daily goal</string>
<string name="menu_steps">Steps</string>
<string name="weight_current">Current weight: %1$s%2$s</string>
<string name="weight_current">Current weight: %1$s</string>
<!-- Error Activity Translations -->

View File

@ -13,6 +13,7 @@
android:key="com.dzeio.open-health.app.language"
android:title="@string/languages" />
</PreferenceCategory>
<PreferenceCategory android:title="Weight Settings">
<EditTextPreference
@ -32,9 +33,8 @@
android:entryValues="@array/mass_units"
android:key="com.dzeio.open-health.unit.mass"
android:title="Mass Unit" />
</PreferenceCategory>
<PreferenceCategory android:title="Water Settings">
<SwitchPreference
@ -55,4 +55,14 @@
android:inputType="number"
android:title="Daily Water intake" />
</PreferenceCategory>
<PreferenceCategory android:title="Steps settings">
<com.dzeio.openhealth.utils.fields.IntEditTextPreference
android:key="com.dzeio.open-health.steps.goal-daily"
android:title="Number of steps each steps"
android:selectAllOnFocus="true"
android:singleLine="true"
android:inputType="number"
/>
</PreferenceCategory>
</PreferenceScreen>

View File

@ -4,6 +4,7 @@ package com.dzeio.charts
* A Base entry for any charts
*/
data class Entry(
val x: Double,
val y: Float
var x: Double,
var y: Float,
var color: Int? = null
)

View File

@ -14,12 +14,12 @@ class XAxis(
override var x: Double = 0.0
set(value) {
val max = getXMax() - increment * displayCount
if (value > max) {
val min = getXMin()
if (value > max && min <= max) {
field = max
return
}
val min = getXMin()
if (value < min) {
field = min
return
@ -103,4 +103,4 @@ class XAxis(
override fun refresh() {
// TODO("Not yet implemented")
}
}
}

View File

@ -6,6 +6,7 @@ import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import com.dzeio.charts.ChartViewInterface
import com.dzeio.charts.utils.drawDottedLine
class YAxis(
private val view: ChartViewInterface
@ -25,6 +26,12 @@ class YAxis(
color = Color.BLUE
}
override val goalLinePaint = Paint().apply {
isAntiAlias = true
color = Color.RED
strokeWidth = 4f
}
var onValueFormat: (value: Float) -> String = { it -> it.toString() }
override var labelCount = 5
@ -49,15 +56,19 @@ class YAxis(
return max!!
}
if (view.series.isEmpty()) {
return 100f
return (this.goalLine ?: 90f) + 10f
}
return view.series
val seriesMax = view.series
.maxOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) {
return@maxOf 0f
}
return@maxOf serie.getDisplayedEntries().maxOf { entry -> entry.y }
}
if (this.goalLine != null) {
return if (seriesMax > this.goalLine!!) seriesMax else this.goalLine!! + 1000f
}
return seriesMax
}
override fun getYMin(): Float {
@ -65,7 +76,7 @@ class YAxis(
return min!!
}
if (view.series.isEmpty()) {
return 0f
return this.goalLine ?: 0f
}
return view.series
.minOf { serie ->
@ -80,6 +91,7 @@ class YAxis(
if (!enabled) {
return 0f
}
val min = getYMin()
val max = getYMax() - min
val top = space.top
@ -89,7 +101,7 @@ class YAxis(
val increment = (bottom - top) / labelCount
val valueIncrement = (max - min) / labelCount
for (index in 0 until labelCount) {
val text = onValueFormat((valueIncrement * (index + 1))).toString()
val text = onValueFormat((valueIncrement * (index + 1)))
textLabel.getTextBounds(text, 0, text.length, rect)
maxWidth = maxWidth.coerceAtLeast(rect.width().toFloat())
@ -105,10 +117,29 @@ class YAxis(
canvas.drawLine(space.left, posY, space.right - maxWidth - 32f, posY, linePaint)
}
if (this.goalLine != null) {
val pos = (1 - this.goalLine!! / max) * space.height() + space.top
canvas.drawDottedLine(
0f,
pos,
space.right - maxWidth - 32f,
pos,
space.right / 20,
goalLinePaint
)
}
return maxWidth + 32f
}
override fun refresh() {
// TODO("Not yet implemented")
}
private var goalLine: Float? = null
override fun setGoalLine(height: Float?) {
goalLine = height
}
}

View File

@ -28,6 +28,11 @@ sealed interface YAxisInterface {
*/
val linePaint: Paint
/**
* Goal line paint
*/
val goalLinePaint: Paint
/**
* run when manually refreshing the system
*
@ -72,4 +77,9 @@ sealed interface YAxisInterface {
* @return the width of the sidebar
*/
fun onDraw(canvas: Canvas, space: RectF): Float
}
/**
* Add a Goal line
*/
fun setGoalLine(height: Float?)
}

View File

@ -5,7 +5,6 @@ import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.util.Log
import com.dzeio.charts.ChartView
import com.dzeio.charts.utils.drawRoundRect
@ -47,18 +46,21 @@ class BarSerie(
for (entry in displayedEntries) {
// calculated height in percent from 0 to 100
val top = (1 - entry.y / max) * drawableSpace.height() + drawableSpace.top
var posX = drawableSpace.left + (view.xAxis.getPositionOnRect(entry, drawableSpace) - view.xAxis.getXOffset(drawableSpace)).toFloat()
var posX = drawableSpace.left + (view.xAxis.getPositionOnRect(
entry,
drawableSpace
) - view.xAxis.getXOffset(drawableSpace)).toFloat()
// Log.d(TAG, "gpor = ${view.xAxis.getPositionOnRect(entry, space)}, gxo = ${view.xAxis.getXOffset(space)}")
// Log.d(TAG, "max = $max, y = ${entry.y}, height = $height")
// Log.d(TAG, "posX: ${posX / 60 / 60 / 1000}, offsetX = ${view.xAxis.x / (1000 * 60 * 60)}, x = ${entry.x / (1000 * 60 * 60)}, pouet: ${(view.xAxis.x + view.xAxis.displayCount * view.xAxis.increment) / (1000 * 60 * 60)}")
Log.d(
TAG, """
${posX},
$top,
${(posX + barWidth)},
${drawableSpace.bottom}""".trimIndent()
)
// Log.d(
// TAG, """
// ${posX},
// $top,
// ${(posX + barWidth)},
// ${drawableSpace.bottom}""".trimIndent()
// )
val right = (posX + barWidth).coerceAtMost(drawableSpace.right)
@ -72,6 +74,13 @@ class BarSerie(
continue
}
// handle color recoloration
val paint = Paint(barPaint)
if (entry.color != null) {
paint.color = entry.color!!
}
canvas.drawRoundRect(
posX,
top,
@ -82,7 +91,7 @@ class BarSerie(
32f,
0f,
0f,
barPaint
paint
)
// handle text display

View File

@ -55,7 +55,17 @@ fun Canvas.drawDottedLine(
/**
* A more customizable drawRoundRect function
*/
fun Canvas.drawRoundRect(left: Float, top: Float, right: Float, bottom: Float, topLeft: Float, topRight: Float, bottomLeft: Float, bottomRight: Float, paint: Paint) {
fun Canvas.drawRoundRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
topLeft: Float,
topRight: Float,
bottomLeft: Float,
bottomRight: Float,
paint: Paint
) {
val maxRound = arrayOf(topLeft, topRight, bottomLeft, bottomRight).maxOf { it }
val width = right - left
val height = bottom - top
@ -81,14 +91,30 @@ fun Canvas.drawRoundRect(left: Float, top: Float, right: Float, bottom: Float, t
if (bottomLeft == 0f) {
drawRect(left, bottom - height / 2, left + width / 2, bottom, paint)
} else {
drawRoundRect(left, bottom - height / 2, left + width / 2, bottom, bottomLeft, bottomLeft, paint)
drawRoundRect(
left,
bottom - height / 2,
left + width / 2,
bottom,
bottomLeft,
bottomLeft,
paint
)
}
// bottom right border
if (bottomRight == 0f) {
drawRect(right - width / 2, bottom - height / 2, right, bottom, paint)
} else {
drawRoundRect(right - width / 2, bottom - height / 2, right, bottom, bottomRight, bottomRight, paint)
drawRoundRect(
right - width / 2,
bottom - height / 2,
right,
bottom,
bottomRight,
bottomRight,
paint
)
}
}
@ -96,6 +122,23 @@ fun Canvas.drawRoundRect(left: Float, top: Float, right: Float, bottom: Float, t
/**
* A more customizable drawRoundRect function
*/
fun Canvas.drawRoundRect(rect: RectF, topLeft: Float, topRight: Float, bottomLeft: Float, bottomRight: Float, paint: Paint) {
drawRoundRect(rect.left, rect.top, rect.right, rect.bottom, topLeft, topRight, bottomLeft, bottomRight, paint)
}
fun Canvas.drawRoundRect(
rect: RectF,
topLeft: Float,
topRight: Float,
bottomLeft: Float,
bottomRight: Float,
paint: Paint
) {
drawRoundRect(
rect.left,
rect.top,
rect.right,
rect.bottom,
topLeft,
topRight,
bottomLeft,
bottomRight,
paint
)
}