Skip to content

Commit a6f02e8

Browse files
committed
Add basic NumberInputListener
1 parent 925a636 commit a6f02e8

File tree

6 files changed

+276
-6
lines changed

6 files changed

+276
-6
lines changed

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ android {
55

66
defaultConfig {
77
applicationId "com.redmadrobot.sample"
8-
minSdkVersion 17
8+
minSdkVersion 30
99
targetSdkVersion 33
1010
versionCode 1
1111
versionName "1.0"

app/src/main/java/com/redmadrobot/sample/MainActivity.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import android.graphics.Paint;
44
import android.graphics.Rect;
5+
import android.icu.number.NumberFormatter;
6+
import android.icu.util.Currency;
57
import android.os.Bundle;
68
import android.util.Log;
79
import android.view.ViewGroup;
@@ -12,6 +14,7 @@
1214
import androidx.annotation.NonNull;
1315
import androidx.appcompat.app.AppCompatActivity;
1416
import com.redmadrobot.inputmask.MaskedTextChangedListener;
17+
import com.redmadrobot.inputmask.NumberInputListener;
1518
import com.redmadrobot.inputmask.PhoneInputListener;
1619
import com.redmadrobot.inputmask.helper.AffinityCalculationStrategy;
1720
import com.redmadrobot.inputmask.model.Country;
@@ -40,7 +43,25 @@ protected void onCreate(final Bundle savedInstanceState) {
4043
}
4144

4245
private void setupCurrencySample() {
43-
// TODO
46+
final EditText editText = findViewById(R.id.currency_edit_text);
47+
final CheckBox checkBox = findViewById(R.id.currency_check_box);
48+
49+
final NumberInputListener listener = NumberInputListener.Companion.installOn(
50+
editText,
51+
new MaskedTextChangedListener.ValueListener() {
52+
@Override
53+
public void onTextChanged(boolean maskFilled, @NonNull String extractedValue, @NonNull String formattedValue, @NonNull String tailPlaceholder) {
54+
logValueListener(maskFilled, extractedValue, formattedValue);
55+
checkBox.setChecked(maskFilled);
56+
}
57+
}
58+
);
59+
60+
listener.setFormatter(
61+
listener.getFormatter().unit(Currency.getInstance("USD"))
62+
);
63+
64+
editText.setHint(listener.placeholder());
4465
}
4566

4667
private void setupDateSample() {

app/src/main/res/layout/activity_main.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
android:layout_height="wrap_content"
2121
android:digits="1234567890., $€"
2222
android:inputType="numberDecimal"
23+
android:gravity="end"
2324
android:text="" />
2425

2526
<CheckBox

gradle/dependencies.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ ext.addRepos = this.&addRepos
88
//region Build versions
99
def build_versions = [:]
1010

11-
build_versions.min_sdk = 14
12-
build_versions.target_sdk = 30
13-
build_versions.compile_sdk = 30
11+
build_versions.min_sdk = 30
12+
build_versions.target_sdk = 33
13+
build_versions.compile_sdk = 33
1414

1515
ext.build_versions = build_versions
1616
//endregion

inputmask/src/main/kotlin/com/redmadrobot/inputmask/MaskedTextChangedListener.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ open class MaskedTextChangedListener(
178178
*
179179
* @return Placeholder string.
180180
*/
181-
fun placeholder(): String = this.primaryMask.placeholder()
181+
open fun placeholder(): String = this.primaryMask.placeholder()
182182

183183
/**
184184
* Minimal length of the text inside the field to fill all mandatory characters in the mask.
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package com.redmadrobot.inputmask
2+
3+
import android.icu.number.LocalizedNumberFormatter
4+
import android.icu.number.NumberFormatter
5+
import android.text.TextWatcher
6+
import android.widget.EditText
7+
import com.redmadrobot.inputmask.helper.AffinityCalculationStrategy
8+
import com.redmadrobot.inputmask.helper.Mask
9+
import com.redmadrobot.inputmask.model.CaretString
10+
import com.redmadrobot.inputmask.model.Notation
11+
import java.math.RoundingMode
12+
import java.util.*
13+
14+
open class NumberInputListener(
15+
primaryFormat: String,
16+
affineFormats: List<String> = emptyList(),
17+
customNotations: List<Notation> = emptyList(),
18+
affinityCalculationStrategy: AffinityCalculationStrategy = AffinityCalculationStrategy.WHOLE_STRING,
19+
autocomplete: Boolean = true,
20+
autoskip: Boolean = false,
21+
field: EditText,
22+
listener: TextWatcher? = null,
23+
valueListener: ValueListener? = null,
24+
rightToLeft: Boolean = false
25+
): MaskedTextChangedListener(
26+
primaryFormat,
27+
affineFormats,
28+
customNotations,
29+
affinityCalculationStrategy,
30+
autocomplete,
31+
autoskip,
32+
field,
33+
listener,
34+
valueListener,
35+
rightToLeft
36+
) {
37+
/**
38+
* Convenience constructor.
39+
*/
40+
constructor(field: EditText):
41+
this(field, null)
42+
43+
/**
44+
* Convenience constructor.
45+
*/
46+
constructor(field: EditText, valueListener: ValueListener?):
47+
this(field, null, valueListener)
48+
49+
/**
50+
* Convenience constructor.
51+
*/
52+
constructor(field: EditText, listener: TextWatcher?, valueListener: ValueListener?):
53+
this(true, false, field, listener, valueListener)
54+
55+
/**
56+
* Convenience constructor.
57+
*/
58+
constructor(
59+
autocomplete: Boolean,
60+
autoskip: Boolean,
61+
field: EditText,
62+
listener: TextWatcher?,
63+
valueListener: ValueListener?
64+
):
65+
this(
66+
"",
67+
emptyList(),
68+
emptyList(),
69+
AffinityCalculationStrategy.WHOLE_STRING,
70+
autocomplete,
71+
autoskip,
72+
field,
73+
listener,
74+
valueListener
75+
)
76+
77+
open var formatter: LocalizedNumberFormatter =
78+
NumberFormatter
79+
.withLocale(Locale("en_us"))
80+
.roundingMode(RoundingMode.FLOOR)
81+
82+
override fun placeholder(): String {
83+
val text = "0"
84+
val mask = pickMask(
85+
CaretString(text, text.length, CaretString.CaretGravity.FORWARD(autocomplete))
86+
)
87+
return mask.placeholder()
88+
}
89+
90+
override fun pickMask(text: CaretString): Mask {
91+
val sanitisedNumberString = extractNumberAndDecimalSeparator(formatter, text.string)
92+
93+
val intNum = sanitisedNumberString.intPart.toLong()
94+
val intMaskFormat = formatter.format(intNum)
95+
96+
// guard let intNum = NumberFormatter().number(from: sanitisedNumberString.intPart), let intMaskFormat = formatter.string(from: intNum)
97+
// else {
98+
// return try! Mask.getOrCreate(withFormat: "[…]")
99+
// }
100+
101+
val intZero = intNum == 0.toLong()
102+
val notationChar = assignNonZeroNumberNotation()
103+
104+
var maskFormat = ""
105+
var first = true
106+
intMaskFormat.toString().forEach { c: Char ->
107+
if (c.isDigit()) {
108+
if (first && !intZero) {
109+
maskFormat += "[$notationChar]"
110+
first = false
111+
} else {
112+
maskFormat += "[0]"
113+
}
114+
} else {
115+
maskFormat += "{$c}"
116+
}
117+
}
118+
119+
if (sanitisedNumberString.numberOfOccurrencesOfDecimalSeparator > 0) {
120+
maskFormat += "{${sanitisedNumberString.expectedDecimalSeparator}}"
121+
}
122+
123+
sanitisedNumberString.decPart.forEach { c: Char ->
124+
maskFormat += "[0]"
125+
}
126+
127+
primaryFormat = maskFormat
128+
return super.pickMask(text)
129+
}
130+
131+
private data class SanitisedNumberString(
132+
val intPart: String,
133+
val decPart: String,
134+
val expectedDecimalSeparator: String,
135+
val numberOfOccurrencesOfDecimalSeparator: Int
136+
)
137+
138+
private fun extractNumberAndDecimalSeparator(
139+
formatter: LocalizedNumberFormatter,
140+
text: String
141+
): SanitisedNumberString {
142+
// val appliedDecimalSeparator = formatter.decimalSeparator ?? NumberInputListener.decimalSeparator
143+
// val appliedCurrencyDecimalSeparator = formatter.currencyDecimalSeparator ?? NumberInputListener.decimalSeparator
144+
145+
val expectedDecimalSeparator: String = decimalSeparator
146+
// if (text.contains(appliedCurrencyDecimalSeparator)) {
147+
// expectedDecimalSeparator = appliedCurrencyDecimalSeparator
148+
// }
149+
150+
// var digitsAndDecimalSeparators = text.replace(ep)
151+
// .replacingOccurrences(of: appliedDecimalSeparator, with: NumberInputListener.decimalSeparator)
152+
// .replacingOccurrences(of: appliedCurrencyDecimalSeparator, with: NumberInputListener.decimalSeparator)
153+
// .filter { c in
154+
// return CharacterSet.decimalDigits.isMember(character: c) || String(c) == NumberInputListener.decimalSeparator
155+
// }
156+
157+
158+
var digitsAndDecimalSeparators = text.filter { c: Char -> c.isDigit() || c.toString() == decimalSeparator }
159+
160+
// val numberOfOccurencesOfDecimalSeparator = digitsAndDecimalSeparators.numberOfOccurencesOf(NumberInputListener.decimalSeparator)
161+
val numberOfOccurencesOfDecimalSeparator = digitsAndDecimalSeparators.count { c: Char -> c.toString() == decimalSeparator }
162+
if (numberOfOccurencesOfDecimalSeparator > 1) {
163+
// digitsAndDecimalSeparators =
164+
// digitsAndDecimalSeparators
165+
// .reversed()
166+
// .replace()
167+
// .repl(decimalSeparator, with: "", maxReplacements: numberOfOccurencesOfDecimalSeparator - 1)
168+
// .reversed
169+
}
170+
171+
val components = digitsAndDecimalSeparators.split(decimalSeparator)
172+
173+
var intStr = ""
174+
var decStr = ""
175+
176+
if (components.size > 1) {
177+
intStr = components.first()
178+
decStr = components.last()
179+
} else {
180+
intStr = components.first()
181+
}
182+
183+
intStr = if (intStr.isEmpty()) "0" else intStr
184+
// intStr = String(intStr.prefix(formatter.maximumIntegerDigits))
185+
// decStr = String(decStr.prefix(formatter.maximumFractionDigits))
186+
187+
return SanitisedNumberString(
188+
intStr,
189+
decStr,
190+
expectedDecimalSeparator,
191+
numberOfOccurencesOfDecimalSeparator
192+
)
193+
}
194+
195+
private fun assignNonZeroNumberNotation(): Char {
196+
val character = '1'
197+
customNotations = listOf(
198+
Notation(
199+
character,
200+
"123456789",
201+
false
202+
)
203+
)
204+
return character
205+
}
206+
207+
companion object {
208+
val decimalSeparator = "."
209+
210+
/**
211+
* Create a `NumberInputListener` instance and assign it as a field's
212+
* `TextWatcher` and `onFocusChangeListener`.
213+
*/
214+
fun installOn(
215+
editText: EditText,
216+
valueListener: ValueListener? = null
217+
): NumberInputListener = installOn(
218+
editText,
219+
true,
220+
false,
221+
null,
222+
valueListener
223+
)
224+
225+
/**
226+
* Create a `NumberInputListener` instance and assign it as a field's
227+
* `TextWatcher` and `onFocusChangeListener`.
228+
*/
229+
fun installOn(
230+
editText: EditText,
231+
autocomplete: Boolean = true,
232+
autoskip: Boolean = false,
233+
listener: TextWatcher? = null,
234+
valueListener: ValueListener? = null
235+
): NumberInputListener {
236+
val maskedListener = NumberInputListener(
237+
autocomplete,
238+
autoskip,
239+
editText,
240+
listener,
241+
valueListener
242+
)
243+
editText.addTextChangedListener(maskedListener)
244+
editText.onFocusChangeListener = maskedListener
245+
return maskedListener
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)