Merge pull request #177 from CrystalPony/master
Limits decimal places in amount inputs on the home and send screens
This commit is contained in:
commit
1569d770bc
|
@ -0,0 +1,133 @@
|
|||
package cash.z.ecc.android.ext
|
||||
|
||||
import cash.z.ecc.android.sdk.ext.Conversions
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZec
|
||||
import cash.z.ecc.android.sdk.ext.toZec
|
||||
import java.math.BigDecimal
|
||||
import java.math.MathContext
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
object ConversionsUniform {
|
||||
var ONE_ZEC_IN_ZATOSHI = BigDecimal(ZcashSdk.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
|
||||
var ZEC_FORMATTER = (NumberFormat.getNumberInstance(Locale("en", "UK")) as DecimalFormat).apply {
|
||||
applyPattern("###.##")
|
||||
roundingMode = RoundingMode.DOWN
|
||||
maximumFractionDigits = 6
|
||||
minimumFractionDigits = 0
|
||||
minimumIntegerDigits = 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format a Zatoshi value into ZEC with the given number of digits, represented as a string.
|
||||
* Start with Zatoshi -> End with ZEC.
|
||||
*
|
||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||
* better than USD.
|
||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||
*
|
||||
* @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at
|
||||
* most [maxDecimals]
|
||||
*/
|
||||
inline fun Long?.convertZatoshiToZecStringUniform(
|
||||
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
|
||||
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
|
||||
): String {
|
||||
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.convertZatoshiToZec(maxDecimals))
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a ZEC value into ZEC with the given number of digits, represented as a string.
|
||||
* Start with ZEC -> End with ZEC.
|
||||
*
|
||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||
* better when right.
|
||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||
*
|
||||
* @return this Double ZEC value represented as a string with at least [minDecimals] and at most
|
||||
* [maxDecimals].
|
||||
*/
|
||||
inline fun Double?.toZecStringUniform(
|
||||
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
|
||||
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
|
||||
): String {
|
||||
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.toZec(maxDecimals))
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Zatoshi value into ZEC with the given number of decimal places, represented as a string.
|
||||
* Start with ZeC -> End with ZEC.
|
||||
*
|
||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||
* better than bread.
|
||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||
*
|
||||
* @return this BigDecimal ZEC value represented as a string with at least [minDecimals] and at most
|
||||
* [maxDecimals].
|
||||
*/
|
||||
inline fun BigDecimal?.toZecStringUniform(
|
||||
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
|
||||
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
|
||||
): String {
|
||||
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.toZecUniform(maxDecimals))
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Double ZEC value as a BigDecimal ZEC value, right-padded to the given number of fraction
|
||||
* digits.
|
||||
* Start with ZEC -> End with ZEC.
|
||||
*
|
||||
* @param decimals the scale to use for the resulting BigDecimal.
|
||||
*
|
||||
* @return this Double ZEC value converted into a BigDecimal, with the proper rounding mode for use
|
||||
* with other formatting functions.
|
||||
*/
|
||||
inline fun Double?.toZec(decimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
|
||||
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(
|
||||
decimals,
|
||||
ConversionsUniform.ZEC_FORMATTER.roundingMode
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a BigDecimal ZEC value as a BigDecimal ZEC value, right-padded to the given number of
|
||||
* fraction digits.
|
||||
* Start with ZEC -> End with ZEC.
|
||||
*
|
||||
* @param decimals the scale to use for the resulting BigDecimal.
|
||||
*
|
||||
* @return this BigDecimal ZEC adjusted to the default scale and rounding mode.
|
||||
*/
|
||||
inline fun BigDecimal?.toZecUniform(decimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
|
||||
return (this ?: BigDecimal.ZERO).setScale(decimals, ConversionsUniform.ZEC_FORMATTER.roundingMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a number formatter for use with converting currency to strings. This probably isn't needed
|
||||
* externally since the other formatting functions leverage this, instead. Leverages the default
|
||||
* rounding mode for ZEC found in ZEC_FORMATTER.
|
||||
*
|
||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||
* glorious.
|
||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||
*
|
||||
* @return a currency formatter, appropriate for the default locale.
|
||||
*/
|
||||
inline fun currencyFormatterUniform(maxDecimals: Int, minDecimals: Int): DecimalFormat {
|
||||
return (ConversionsUniform.ZEC_FORMATTER.clone() as DecimalFormat).apply {
|
||||
maximumFractionDigits = maxDecimals
|
||||
minimumFractionDigits = minDecimals
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the decimal separator is the last symbol
|
||||
*/
|
||||
inline fun String.endsWithDecimalSeparator(): Boolean {
|
||||
return this.endsWith(ConversionsUniform.ZEC_FORMATTER.decimalFormatSymbols.toString())
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
|
@ -18,6 +20,59 @@ fun EditText.onEditorActionDone(block: (EditText) -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
inline fun EditText.limitDecimalPlaces(max: Int) {
|
||||
val editText = this
|
||||
addTextChangedListener(object : TextWatcher {
|
||||
var previousValue = ""
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// Cache the previous value
|
||||
previousValue = text.toString()
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
var textStr = text.toString()
|
||||
|
||||
if (textStr.isNotEmpty()) {
|
||||
val oldText = text.toString()
|
||||
val number = textStr.safelyConvertToBigDecimal()
|
||||
|
||||
if (number != null && number.scale() > 8) {
|
||||
// Prevent the user from adding a new decimal place somewhere in the middle if we're already at the limit
|
||||
if (editText.selectionStart == editText.selectionEnd && editText.selectionStart != textStr.length) {
|
||||
textStr = previousValue
|
||||
} else {
|
||||
textStr = number.toZecStringUniform(8)
|
||||
}
|
||||
}
|
||||
|
||||
// Trim leading zeroes
|
||||
textStr = textStr.trimStart('0')
|
||||
// Append a zero if this results in an empty string or if the first symbol is not a digit
|
||||
if (textStr.isEmpty() || !textStr.first().isDigit()) {
|
||||
textStr = "0$textStr"
|
||||
}
|
||||
|
||||
// Restore the cursor position
|
||||
if (oldText != textStr) {
|
||||
val cursorPosition = editText.selectionEnd;
|
||||
editText.setText(textStr)
|
||||
editText.setSelection(
|
||||
(cursorPosition - (oldText.length - textStr.length)).coerceIn(
|
||||
0,
|
||||
editText.text.toString().length
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fun TextView.convertZecToZatoshi(): Long? {
|
||||
return try {
|
||||
|
|
|
@ -45,7 +45,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
|
|||
|| (c == '<' && acc == "0")
|
||||
|| (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c")
|
||||
acc
|
||||
}
|
||||
}
|
||||
c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars")
|
||||
"0"
|
||||
}
|
||||
|
@ -55,7 +55,11 @@ class HomeViewModel @Inject constructor() : ViewModel() {
|
|||
acc == "0" && c != '.' -> {twig("triggered: 4 $typedChars")
|
||||
c.toString()
|
||||
}
|
||||
else -> {twig("triggered: 5 $typedChars")
|
||||
acc.contains('.') && acc.length - acc.indexOf('.') > 8 -> {
|
||||
twig("triggered: 5 $typedChars")
|
||||
acc
|
||||
}
|
||||
else -> {twig("triggered: 6 $typedChars")
|
||||
"$acc$c"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
|
|||
|
||||
// Apply View Model
|
||||
if (sendViewModel.zatoshiAmount > 0L) {
|
||||
sendViewModel.zatoshiAmount.convertZatoshiToZecString(8).let { amount ->
|
||||
sendViewModel.zatoshiAmount.convertZatoshiToZecStringUniform(8).let { amount ->
|
||||
binding.inputZcashAmount.setText(amount)
|
||||
}
|
||||
} else {
|
||||
|
@ -67,6 +67,8 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
|
|||
binding.inputZcashAddress.onEditorActionDone(::onSubmit).also { tapped(SEND_ADDRESS_DONE_ADDRESS) }
|
||||
binding.inputZcashAmount.onEditorActionDone(::onSubmit).also { tapped(SEND_ADDRESS_DONE_AMOUNT) }
|
||||
|
||||
binding.inputZcashAmount.limitDecimalPlaces(8)
|
||||
|
||||
binding.inputZcashAddress.apply {
|
||||
doAfterTextChanged {
|
||||
val textStr = text.toString()
|
||||
|
|
Loading…
Reference in New Issue