Skip to content

Commit

Permalink
#18 - US05 - android list improved design
Browse files Browse the repository at this point in the history
  • Loading branch information
Harold Jose committed Apr 13, 2024
1 parent e7e966c commit 5fda4b5
Show file tree
Hide file tree
Showing 42 changed files with 1,109 additions and 287 deletions.
10 changes: 2 additions & 8 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.navigation.compose)
debugImplementation(libs.compose.ui.tooling)
implementation(libs.coil.compose)

implementation(libs.camera.camera2)
implementation(libs.camera.core)
Expand All @@ -63,12 +64,5 @@ dependencies {

implementation(libs.kotlinx.datetime)

//implementation(libs.kotlinx.coroutines.core)
//implementation(libs.kotlinx.serialization.core)
//implementation(libs.kotlinx.serialization.json)
//implementation(libs.ktor.client.core)
//implementation(libs.ktor.client.serialization)
//implementation(libs.ktor.serialization.kotlinx.json)
//implementation(libs.ktor.client.content.negotiation)
//implementation(libs.ktor.client.okhttp)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dev.haroldjose.familysharedlist.android.extensions

import java.text.NumberFormat

/**
* Returns a string with only the digits of the double
*/
fun Double.toStringWithOnlyDigits(): String = this.toString().filter { it.isDigit() }


/**
* Returns a string with the currency format
*/
fun Double.ToCurrencyFormat(fractionDigits: Int = 2): String {
val numberFormat = NumberFormat.getCurrencyInstance()
numberFormat.setMaximumFractionDigits(fractionDigits);
return numberFormat.format(this)
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.haroldjose.familysharedlist.android.extensions

import androidx.core.text.isDigitsOnly
import java.text.DecimalFormat

/**
* used to validate if a string is a valid amount
*/
fun String.isValidFormattableAmount(maxLength: Int = 6): Boolean {
return isNotBlank() && isDigitsOnly() && length <= maxLength
}

/**
* used to return the currency symbol
*/
fun getCurrencySymbol(): String {
val symbols = DecimalFormat().decimalFormatSymbols
return symbols.currencySymbol
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package dev.haroldjose.familysharedlist.android.presentationLayer.components

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import dev.haroldjose.familysharedlist.Logger
import dev.haroldjose.familysharedlist.android.app.MyApplicationTheme
import dev.haroldjose.familysharedlist.android.extensions.getCurrencySymbol
import dev.haroldjose.familysharedlist.android.extensions.isValidFormattableAmount
import dev.haroldjose.familysharedlist.android.extensions.toStringWithOnlyDigits
import kotlinx.coroutines.CoroutineScope
import java.lang.Integer.max
import java.text.DecimalFormat

@Composable
fun CurrencyAmountInput(
value: Double,
modifier: Modifier = Modifier.fillMaxWidth(),
maxLength: Int = 7,
onValueChanged: (newValue: Double) -> Unit
) {
var currencyValue by remember { mutableStateOf(value.toStringWithOnlyDigits()) }

OutlinedTextField(
value = currencyValue,
singleLine = true,
onValueChange = {
if (it.length <= maxLength) {
currencyValue = if (it.startsWith("0")) "" else it
onValueChanged(currencyValue.toCurrencyAmount())
}
},
visualTransformation = CurrencyAmountInputVisualTransformation(
fixedCursorAtTheEnd = true
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.NumberPassword
),
prefix = {
Text(text = getCurrencySymbol())
},
placeholder = {
Text(text = "${getCurrencySymbol()} 0.00")
},
label = { Text("alterando preço") },
modifier = modifier
)
}

/**
* Transform a strint like 123456789 to 1234567.89
*/
private fun String.toCurrencyAmount(): Double {
return (this.toDoubleOrNull() ?: 0.0) / 100
}

private class CurrencyAmountInputVisualTransformation(
private val fixedCursorAtTheEnd: Boolean = true,
private val numberOfDecimals: Int = 2
) : VisualTransformation {

private val symbols = DecimalFormat().decimalFormatSymbols

override fun filter(text: AnnotatedString): TransformedText {
val thousandsSeparator = symbols.groupingSeparator
val decimalSeparator = symbols.decimalSeparator
val zero = symbols.zeroDigit

val inputText = text.text

val intPart = inputText
.dropLast(numberOfDecimals)
.reversed()
.chunked(3)
.joinToString(thousandsSeparator.toString())
.reversed()
.ifEmpty {
zero.toString()
}

val fractionPart = inputText.takeLast(numberOfDecimals).let {
if (it.length != numberOfDecimals) {
List(numberOfDecimals - it.length) {
zero
}.joinToString("") + it
} else {
it
}
}

val formattedNumber = intPart + decimalSeparator + fractionPart

val newText = AnnotatedString(
text = formattedNumber,
spanStyles = text.spanStyles,
paragraphStyles = text.paragraphStyles
)

val offsetMapping = if (fixedCursorAtTheEnd) {
FixedCursorOffsetMapping(
contentLength = inputText.length,
formattedContentLength = formattedNumber.length
)
} else {
MovableCursorOffsetMapping(
unmaskedText = text.toString(),
maskedText = newText.toString(),
decimalDigits = numberOfDecimals
)
}

return TransformedText(newText, offsetMapping)
}

private class FixedCursorOffsetMapping(
private val contentLength: Int,
private val formattedContentLength: Int,
) : OffsetMapping {
override fun originalToTransformed(offset: Int): Int = formattedContentLength
override fun transformedToOriginal(offset: Int): Int = contentLength
}

private class MovableCursorOffsetMapping(
private val unmaskedText: String,
private val maskedText: String,
private val decimalDigits: Int
) : OffsetMapping {
override fun originalToTransformed(offset: Int): Int =
when {
unmaskedText.length <= decimalDigits -> {
maskedText.length - (unmaskedText.length - offset)
}
else -> {
offset + offsetMaskCount(offset, maskedText)
}
}

override fun transformedToOriginal(offset: Int): Int =
when {
unmaskedText.length <= decimalDigits -> {
max(unmaskedText.length - (maskedText.length - offset), 0)
}
else -> {
offset - maskedText.take(offset).count { !it.isDigit() }
}
}

private fun offsetMaskCount(offset: Int, maskedText: String): Int {
var maskOffsetCount = 0
var dataCount = 0
for (maskChar in maskedText) {
if (!maskChar.isDigit()) {
maskOffsetCount++
} else if (++dataCount > offset) {
break
}
}
return maskOffsetCount
}
}
}

@Preview
@Composable
fun CurrencyAmountInput_Preview() {
MyApplicationTheme {
var valueResult = remember { mutableStateOf(0.0) }
CurrencyAmountInput(
valueResult.value,
) {
valueResult.value = it
}
}
}
Loading

0 comments on commit 5fda4b5

Please sign in to comment.