@momo-kits/calculator-keyboard 0.150.2-phuc.15 → 0.151.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -3
- package/android/src/main/java/com/calculatorkeyboard/CalculatorKeyboardPackage.kt +1 -1
- package/android/src/main/java/com/calculatorkeyboard/CustomKeyboardView.kt +242 -108
- package/android/src/main/java/com/calculatorkeyboard/Event.kt +129 -0
- package/android/src/main/java/com/calculatorkeyboard/{RCTInputCalculator.kt → InputCalculatorViewManager.kt} +105 -33
- package/ios/CalculatorKeyboardView.h +32 -0
- package/ios/CalculatorKeyboardView.mm +274 -0
- package/ios/NativeInputCalculator.h +11 -0
- package/ios/NativeInputCalculator.mm +500 -0
- package/ios/Utils.h +10 -0
- package/ios/Utils.mm +25 -0
- package/package.json +21 -131
- package/react-native-calculator-keyboard.podspec +5 -4
- package/src/InputCalculatorNativeComponent.ts +71 -0
- package/src/index.tsx +93 -43
- package/ios/CalculatorKeyboardView.swift +0 -153
- package/ios/InputCalculator-Bridging-Header.h +0 -23
- package/ios/InputCalculator.m +0 -85
- package/ios/InputCalculator.swift +0 -158
- package/ios/extension.swift +0 -23
- package/lib/commonjs/index.js +0 -72
- package/lib/commonjs/index.js.map +0 -1
- package/lib/module/index.js +0 -67
- package/lib/module/index.js.map +0 -1
- package/lib/typescript/commonjs/package.json +0 -1
- package/lib/typescript/commonjs/src/index.d.ts +0 -23
- package/lib/typescript/commonjs/src/index.d.ts.map +0 -1
- package/lib/typescript/module/package.json +0 -1
- package/lib/typescript/module/src/index.d.ts +0 -23
- package/lib/typescript/module/src/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -10,15 +10,57 @@ npm install react-native-calculator-keyboard
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
```js
|
|
15
|
-
import
|
|
14
|
+
import InputCalculator from '@momo-kits/calculator-keyboard';
|
|
16
15
|
|
|
17
16
|
// ...
|
|
18
17
|
|
|
19
|
-
<
|
|
18
|
+
<InputCalculator
|
|
19
|
+
mode="NumDefault"
|
|
20
|
+
customKeyText="Next"
|
|
21
|
+
onCustomKeyEvent={() => console.log('Custom key pressed')}
|
|
22
|
+
/>;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
**React Native 0.80+** with **Fabric (New Architecture) enabled**.
|
|
28
|
+
|
|
29
|
+
This library is **pure Fabric** implementation with:
|
|
30
|
+
|
|
31
|
+
- ✅ Zero RCTBridge dependencies
|
|
32
|
+
- ✅ Native C++ ComponentView on iOS
|
|
33
|
+
- ✅ Fabric ViewManager with codegen delegates on Android
|
|
34
|
+
- ✅ All Props, Events, Commands auto-generated by codegen
|
|
35
|
+
- ❌ No Paper (old architecture) support
|
|
36
|
+
|
|
37
|
+
### Android Setup
|
|
38
|
+
|
|
39
|
+
Add to your `gradle.properties`:
|
|
40
|
+
|
|
41
|
+
```properties
|
|
42
|
+
newArchEnabled=true
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then rebuild:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd android && ./gradlew clean && cd ..
|
|
49
|
+
npx react-native run-android
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### iOS Setup
|
|
53
|
+
|
|
54
|
+
**Required**: Set the environment variable before installing pods:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd ios
|
|
58
|
+
RCT_NEW_ARCH_ENABLED=1 pod install
|
|
59
|
+
cd ..
|
|
60
|
+
npx react-native run-ios
|
|
20
61
|
```
|
|
21
62
|
|
|
63
|
+
**Note**: This library uses Fabric ComponentView (`.mm` files) and will not work without `RCT_NEW_ARCH_ENABLED=1`.
|
|
22
64
|
|
|
23
65
|
## Contributing
|
|
24
66
|
|
|
@@ -4,70 +4,73 @@ import android.annotation.SuppressLint
|
|
|
4
4
|
import android.content.res.ColorStateList
|
|
5
5
|
import android.graphics.Color
|
|
6
6
|
import android.graphics.drawable.GradientDrawable
|
|
7
|
+
import android.text.Editable
|
|
7
8
|
import android.view.Gravity
|
|
8
9
|
import android.widget.Button
|
|
10
|
+
import android.widget.EditText
|
|
9
11
|
import android.widget.ImageButton
|
|
10
|
-
import androidx.appcompat.app.AppCompatActivity
|
|
11
12
|
import androidx.constraintlayout.widget.ConstraintLayout
|
|
13
|
+
import androidx.core.graphics.toColorInt
|
|
14
|
+
import com.calculatorkeyboard.InputCalculatorViewManager.Companion.calculatorHeight
|
|
12
15
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
13
16
|
import org.mariuszgromada.math.mxparser.Expression
|
|
14
|
-
import androidx.core.graphics.toColorInt
|
|
15
|
-
import com.calculatorkeyboard.RCTInputCalculator.Companion.calculatorHeight
|
|
16
|
-
import com.facebook.react.bridge.Arguments
|
|
17
|
-
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
18
17
|
|
|
19
18
|
@SuppressLint("SetTextI18n", "ViewConstructor")
|
|
20
19
|
class CustomKeyboardView(
|
|
21
20
|
context: ThemedReactContext,
|
|
22
21
|
private val editText: CalculatorEditText
|
|
23
22
|
) : ConstraintLayout(context) {
|
|
24
|
-
private val
|
|
23
|
+
private val numWithCTAKeys = listOf(
|
|
25
24
|
listOf("1", "2", "3", "÷", "back"),
|
|
26
25
|
listOf("4", "5", "6", "×", "="),
|
|
27
|
-
listOf("7", "8", "9", "-", "
|
|
26
|
+
listOf("7", "8", "9", "-", "Tiếp"),
|
|
28
27
|
listOf("000", "0", "+")
|
|
29
28
|
)
|
|
30
|
-
private val
|
|
29
|
+
private val defaultKeys = listOf(
|
|
30
|
+
listOf("1", "2", "3", "÷", "AC"),
|
|
31
|
+
listOf("4", "5", "6", "×", "back"),
|
|
32
|
+
listOf("7", "8", "9", "-", "="),
|
|
33
|
+
listOf("000", "0", "+")
|
|
34
|
+
)
|
|
35
|
+
private val specialKeys = listOf("=", "-", "×", "÷", "back", "+", "AC")
|
|
31
36
|
private val separatorWidth = 8f
|
|
32
37
|
private var specialButtonColor: Int = "#D8D8D8".toColorInt()
|
|
33
38
|
|
|
39
|
+
private var keyboardMode: String = "NumDefault"
|
|
40
|
+
|
|
34
41
|
private var customKeyButton: Button? = null
|
|
35
42
|
private var customKeyButtonBackground: Int = "#D8D8D8".toColorInt()
|
|
36
43
|
private var customKeyButtonTextColor: Int = Color.BLACK
|
|
37
44
|
private var customKeyButtonState: String = "default"
|
|
38
45
|
|
|
39
46
|
init {
|
|
40
|
-
val activity = context.currentActivity as? AppCompatActivity
|
|
41
|
-
if (activity != null) {
|
|
42
|
-
val displayMetrics = resources.displayMetrics
|
|
43
|
-
val widthButton = (displayMetrics.widthPixels - separatorWidth * 2 - 4 * separatorWidth) / 5f
|
|
44
|
-
val heightButton = (calculatorHeight - separatorWidth * 2 - 3 * separatorWidth) / 4
|
|
45
|
-
|
|
46
47
|
isClickable = false
|
|
47
48
|
isFocusable = false
|
|
48
49
|
isFocusableInTouchMode = false
|
|
49
50
|
clipToPadding = false
|
|
50
51
|
clipChildren = false
|
|
51
|
-
|
|
52
|
-
renderUI(widthButton, heightButton)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
private fun renderUI(
|
|
54
|
+
private fun renderUI() {
|
|
55
|
+
val displayMetrics = resources.displayMetrics
|
|
56
|
+
val buttonWidth = (displayMetrics.widthPixels - separatorWidth * 2 - 4 * separatorWidth) / 5f
|
|
57
|
+
val buttonHeight = (calculatorHeight - separatorWidth * 2 - 3 * separatorWidth) / 4
|
|
58
|
+
|
|
58
59
|
var yOffset = separatorWidth
|
|
60
|
+
val keys = if (keyboardMode == "NumWithCTA") numWithCTAKeys else defaultKeys
|
|
59
61
|
for ((rowIndex, row) in keys.withIndex()) {
|
|
60
62
|
var xOffset = separatorWidth
|
|
61
63
|
for ((colIndex, key) in row.withIndex()) {
|
|
62
|
-
val
|
|
64
|
+
val isMainKey = rowIndex == 2 && colIndex == 4
|
|
65
|
+
val isMainCTAKey = isMainKey && keyboardMode == "NumWithCTA"
|
|
63
66
|
val width = if (key == "000") buttonWidth * 2 + separatorWidth else buttonWidth
|
|
64
|
-
val height = if (
|
|
67
|
+
val height = if (isMainKey) buttonHeight * 2 + separatorWidth else buttonHeight
|
|
65
68
|
|
|
66
69
|
val button = if (key == "back") {
|
|
67
70
|
createImageButton(key, xOffset, yOffset, buttonWidth.toInt(), buttonHeight.toInt())
|
|
68
71
|
} else {
|
|
69
|
-
createButton(key, xOffset, yOffset, width.toInt(), height.toInt(),
|
|
70
|
-
if (
|
|
72
|
+
createButton(key, xOffset, yOffset, width.toInt(), height.toInt(), isMainKey, isMainCTAKey).also { b ->
|
|
73
|
+
if (isMainCTAKey) customKeyButton = b
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -85,7 +88,8 @@ class CustomKeyboardView(
|
|
|
85
88
|
yOffset: Float,
|
|
86
89
|
buttonWidth: Int,
|
|
87
90
|
buttonHeight: Int,
|
|
88
|
-
|
|
91
|
+
isMainKey: Boolean,
|
|
92
|
+
isMainCTAKey: Boolean
|
|
89
93
|
): Button {
|
|
90
94
|
return Button(context).apply {
|
|
91
95
|
val shapeInit = GradientDrawable().apply {
|
|
@@ -98,7 +102,7 @@ class CustomKeyboardView(
|
|
|
98
102
|
background = shapeInit
|
|
99
103
|
text = key
|
|
100
104
|
setTypeface(typeface)
|
|
101
|
-
textSize = (if (
|
|
105
|
+
textSize = (if (isMainCTAKey) 18 else 24).toFloat()
|
|
102
106
|
setTextColor(Color.BLACK)
|
|
103
107
|
stateListAnimator = null
|
|
104
108
|
maxLines = 1
|
|
@@ -110,7 +114,7 @@ class CustomKeyboardView(
|
|
|
110
114
|
constrainedWidth = false
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
if (specialKeys.contains(key)) {
|
|
117
|
+
if (specialKeys.contains(key) || isMainKey) {
|
|
114
118
|
background = GradientDrawable().apply {
|
|
115
119
|
shape = GradientDrawable.RECTANGLE
|
|
116
120
|
cornerRadius = 24f
|
|
@@ -125,7 +129,7 @@ class CustomKeyboardView(
|
|
|
125
129
|
|
|
126
130
|
translationX = xOffset.toInt().toFloat()
|
|
127
131
|
translationY = yOffset.toInt().toFloat()
|
|
128
|
-
setOnClickListener { onKeyPress(key,
|
|
132
|
+
setOnClickListener { onKeyPress(key, isMainCTAKey) }
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -163,132 +167,192 @@ class CustomKeyboardView(
|
|
|
163
167
|
}
|
|
164
168
|
}
|
|
165
169
|
|
|
166
|
-
fun
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
val key = child.text.toString()
|
|
172
|
-
if (specialKeys.contains(key)) {
|
|
173
|
-
if (key == "=") {
|
|
174
|
-
child.background = GradientDrawable().apply {
|
|
175
|
-
shape = GradientDrawable.RECTANGLE
|
|
176
|
-
cornerRadius = 24f
|
|
177
|
-
setColor(specialButtonColor)
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
child.background = GradientDrawable().apply {
|
|
181
|
-
shape = GradientDrawable.RECTANGLE
|
|
182
|
-
cornerRadius = 24f
|
|
183
|
-
setColor(specialButtonColor)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
child.setTextColor(Color.BLACK)
|
|
187
|
-
}
|
|
188
|
-
} else if (child is ImageButton) {
|
|
189
|
-
child.background = GradientDrawable().apply {
|
|
190
|
-
shape = GradientDrawable.RECTANGLE
|
|
191
|
-
cornerRadius = 24f
|
|
192
|
-
setColor(specialButtonColor)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
170
|
+
private fun String.endsWithAny(vararg tails: String): Boolean =
|
|
171
|
+
tails.any { endsWith(it) }
|
|
172
|
+
|
|
173
|
+
private fun Editable.insertAtCaret(editText: EditText, s: String) {
|
|
174
|
+
insert(editText.selectionStart.coerceAtLeast(0), s)
|
|
196
175
|
}
|
|
197
176
|
|
|
198
|
-
private fun onKeyPress(key: String,
|
|
199
|
-
if (
|
|
200
|
-
emitCustomKey()
|
|
177
|
+
private fun onKeyPress(key: String, isMainCTAKey: Boolean) {
|
|
178
|
+
if (isMainCTAKey) {
|
|
179
|
+
Event.emitCustomKey(context, editText.id)
|
|
201
180
|
return
|
|
202
181
|
}
|
|
203
182
|
|
|
204
|
-
emitKeyPress(key)
|
|
183
|
+
Event.emitKeyPress(context, editText.id, key)
|
|
184
|
+
|
|
185
|
+
val text = editText.text ?: return
|
|
186
|
+
val plain = text.toString()
|
|
187
|
+
val isEmpty = plain.isEmpty()
|
|
188
|
+
val isJustZero = plain == "0"
|
|
189
|
+
val endsWithOperator = plain.endsWithAny(" + ", " - ", " × ", " ÷ ")
|
|
190
|
+
val endsWithSingleZeroAfterOp = plain.endsWith(" 0")
|
|
191
|
+
|
|
205
192
|
when (key) {
|
|
193
|
+
"AC" -> clearText()
|
|
206
194
|
"back" -> onBackSpace()
|
|
207
195
|
"=" -> calculateResult()
|
|
208
|
-
"×", "+", "-", "÷" -> keyDidPress(" $key ")
|
|
209
|
-
|
|
196
|
+
"×", "+", "-", "÷" -> if (!text.isEmpty()) keyDidPress(" $key ")
|
|
197
|
+
"0", "000" -> {
|
|
198
|
+
when {
|
|
199
|
+
isEmpty -> {}
|
|
200
|
+
key == "000" && endsWithOperator -> text.insertAtCaret(editText, "0")
|
|
201
|
+
endsWithSingleZeroAfterOp || isJustZero -> {}
|
|
202
|
+
else -> text.insertAtCaret(editText, key)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else -> {
|
|
206
|
+
when {
|
|
207
|
+
isJustZero -> {
|
|
208
|
+
text.replace(0, 1, key)
|
|
209
|
+
}
|
|
210
|
+
endsWithSingleZeroAfterOp -> {
|
|
211
|
+
val i = text.length - 1
|
|
212
|
+
text.replace(i, i + 1, key)
|
|
213
|
+
}
|
|
214
|
+
else -> text.insertAtCaret(editText, key)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
210
217
|
}
|
|
218
|
+
|
|
219
|
+
reformatAndKeepSelection(editText)
|
|
211
220
|
}
|
|
212
221
|
|
|
213
222
|
private fun keyDidPress(key: String) {
|
|
214
|
-
|
|
215
|
-
editText.
|
|
223
|
+
val editable = editText.text ?: return
|
|
224
|
+
val selStart = editText.selectionStart.coerceAtLeast(0)
|
|
225
|
+
val selEnd = editText.selectionEnd.coerceAtLeast(selStart)
|
|
226
|
+
|
|
227
|
+
if (selStart != selEnd) {
|
|
228
|
+
editable.replace(selStart, selEnd, key)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
val s = editable.toString()
|
|
233
|
+
val ops3 = setOf(" + ", " - ", " × ", " ÷ ")
|
|
234
|
+
|
|
235
|
+
if (selStart >= 3 && ops3.contains(s.substring(selStart - 3, selStart))) {
|
|
236
|
+
editable.replace(selStart - 3, selStart, key)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
editable.replace(selStart, selEnd, key)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private fun clearText() {
|
|
244
|
+
editText.text?.clear()
|
|
216
245
|
}
|
|
217
246
|
|
|
218
247
|
private fun onBackSpace() {
|
|
219
|
-
val
|
|
220
|
-
val
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
val formatted = editText.text?.toString().orEmpty()
|
|
249
|
+
val caretFmt = editText.selectionStart.coerceAtLeast(0)
|
|
250
|
+
|
|
251
|
+
val rawBefore = stripGroupDots(formatted)
|
|
252
|
+
val caretRaw = formattedCaretToRaw(formatted, caretFmt)
|
|
253
|
+
|
|
254
|
+
if (caretRaw <= 0) return
|
|
255
|
+
|
|
256
|
+
val ops = setOf(" + ", " - ", " × ", " ÷ ")
|
|
257
|
+
val len = rawBefore.length
|
|
258
|
+
|
|
259
|
+
var delStart = (caretRaw - 1).coerceAtLeast(0)
|
|
260
|
+
var delEnd = caretRaw
|
|
261
|
+
|
|
262
|
+
fun matches(start: Int, end: Int): Boolean {
|
|
263
|
+
if (start < 0 || end > len) return false
|
|
264
|
+
if (end - start != 3) return false
|
|
265
|
+
return ops.contains(rawBefore.substring(start, end))
|
|
225
266
|
}
|
|
226
|
-
}
|
|
227
267
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
} else {
|
|
240
|
-
println("Invalid expression")
|
|
268
|
+
if (matches(caretRaw - 3, caretRaw)) {
|
|
269
|
+
delStart = caretRaw - 3
|
|
270
|
+
delEnd = caretRaw
|
|
271
|
+
}
|
|
272
|
+
else if (matches(caretRaw - 2, caretRaw + 1)) {
|
|
273
|
+
delStart = caretRaw - 2
|
|
274
|
+
delEnd = caretRaw + 1
|
|
275
|
+
}
|
|
276
|
+
else if (matches(caretRaw - 1, caretRaw + 2)) {
|
|
277
|
+
delStart = caretRaw - 1
|
|
278
|
+
delEnd = caretRaw + 2
|
|
241
279
|
}
|
|
242
|
-
}
|
|
243
280
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return if (e.checkSyntax()) {
|
|
248
|
-
e.calculate().toLong()
|
|
249
|
-
} else {
|
|
250
|
-
null
|
|
281
|
+
val rawAfter = buildString(len - (delEnd - delStart)) {
|
|
282
|
+
append(rawBefore, 0, delStart)
|
|
283
|
+
append(rawBefore, delEnd, len)
|
|
251
284
|
}
|
|
285
|
+
|
|
286
|
+
val formattedAfter = formatNumberGroups(rawAfter)
|
|
287
|
+
editText.setText(formattedAfter)
|
|
288
|
+
|
|
289
|
+
val newCaretFmt = rawCaretToFormatted(delStart, formattedAfter)
|
|
290
|
+
editText.setSelection(newCaretFmt.coerceIn(0, formattedAfter.length))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private fun calculateResult(){
|
|
294
|
+
val result = editText.calculate()
|
|
295
|
+
if (result == "invalid") return
|
|
296
|
+
editText.setTextKeepState(formatNumberGroups(result))
|
|
297
|
+
editText.setSelection(editText.text?.length ?: 0)
|
|
252
298
|
}
|
|
253
299
|
|
|
254
|
-
|
|
255
|
-
val
|
|
256
|
-
|
|
257
|
-
|
|
300
|
+
fun formatNumberGroups(input: String): String {
|
|
301
|
+
val noSep = stripGroupDots(input)
|
|
302
|
+
return Regex("\\d+").replace(noSep) { m ->
|
|
303
|
+
val s = m.value
|
|
304
|
+
val rev = s.reversed().chunked(3).joinToString(".")
|
|
305
|
+
rev.reversed()
|
|
258
306
|
}
|
|
259
|
-
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
260
|
-
.receiveEvent(editText.id, "onKeyPress", params)
|
|
261
307
|
}
|
|
262
308
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
309
|
+
|
|
310
|
+
fun stripGroupDots(input: String): String {
|
|
311
|
+
val out = StringBuilder(input.length)
|
|
312
|
+
for (i in input.indices) {
|
|
313
|
+
val c = input[i]
|
|
314
|
+
if (c == '.' && isGroupDotAt(input, i)) {
|
|
315
|
+
} else {
|
|
316
|
+
out.append(c)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return out.toString()
|
|
320
|
+
}
|
|
321
|
+
private fun isGroupDotAt(s: String, i: Int): Boolean {
|
|
322
|
+
if (i < 0 || i >= s.length) return false
|
|
323
|
+
if (s[i] != '.') return false
|
|
324
|
+
val leftIsDigit = i - 1 >= 0 && s[i - 1].isDigit()
|
|
325
|
+
val rightIsDigit = i + 1 < s.length && s[i + 1].isDigit()
|
|
326
|
+
return leftIsDigit && rightIsDigit
|
|
267
327
|
}
|
|
268
328
|
|
|
269
329
|
fun setCustomKeyText(text: String) {
|
|
270
330
|
customKeyButton?.text = text
|
|
271
331
|
}
|
|
272
332
|
|
|
333
|
+
fun setMode(mode: String) {
|
|
334
|
+
keyboardMode = mode
|
|
335
|
+
renderUI()
|
|
336
|
+
}
|
|
337
|
+
|
|
273
338
|
fun setCustomKeyBackground(background: Int) {
|
|
274
339
|
customKeyButtonBackground = background
|
|
275
|
-
updateCustomKeyUI(background, customKeyButtonTextColor
|
|
340
|
+
updateCustomKeyUI(background, customKeyButtonTextColor)
|
|
276
341
|
}
|
|
277
342
|
|
|
278
343
|
fun setCustomKeyTextColor(textColor: Int) {
|
|
279
344
|
customKeyButtonTextColor = textColor
|
|
280
|
-
updateCustomKeyUI(customKeyButtonBackground, textColor
|
|
345
|
+
updateCustomKeyUI(customKeyButtonBackground, textColor)
|
|
281
346
|
}
|
|
282
347
|
|
|
283
348
|
|
|
284
349
|
fun setCustomKeyState(state: String) {
|
|
285
350
|
customKeyButtonState = state
|
|
286
351
|
customKeyButton?.isEnabled = state != "disable"
|
|
287
|
-
updateCustomKeyUI(customKeyButtonBackground, customKeyButtonTextColor
|
|
352
|
+
updateCustomKeyUI(customKeyButtonBackground, customKeyButtonTextColor)
|
|
288
353
|
}
|
|
289
354
|
|
|
290
|
-
private fun updateCustomKeyUI(background: Int, textColor: Int
|
|
291
|
-
|
|
355
|
+
private fun updateCustomKeyUI(background: Int, textColor: Int){
|
|
292
356
|
customKeyButton?.background = GradientDrawable().apply {
|
|
293
357
|
shape = GradientDrawable.RECTANGLE
|
|
294
358
|
cornerRadius = 24f
|
|
@@ -297,5 +361,75 @@ class CustomKeyboardView(
|
|
|
297
361
|
customKeyButton?.setTextColor(textColor)
|
|
298
362
|
}
|
|
299
363
|
|
|
364
|
+
fun reformatAndKeepSelection(editText: CalculatorEditText) {
|
|
365
|
+
val formattedBefore = editText.text?.toString() ?: return
|
|
366
|
+
val caretFmtBefore = editText.selectionStart.coerceAtLeast(0)
|
|
300
367
|
|
|
368
|
+
val caretRaw = formattedCaretToRaw(formattedBefore, caretFmtBefore)
|
|
369
|
+
|
|
370
|
+
val formattedAfter = formatNumberGroups(formattedBefore)
|
|
371
|
+
|
|
372
|
+
if (formattedAfter != formattedBefore) {
|
|
373
|
+
editText.setText(formattedAfter)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
val caretFmtAfter = rawCaretToFormatted(caretRaw, formattedAfter)
|
|
377
|
+
editText.setSelection(caretFmtAfter.coerceIn(0, formattedAfter.length))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private fun formattedCaretToRaw(formatted: String, caretFmt: Int): Int {
|
|
381
|
+
var rawIdx = 0
|
|
382
|
+
var i = 0
|
|
383
|
+
while (i < caretFmt && i < formatted.length) {
|
|
384
|
+
val c = formatted[i]
|
|
385
|
+
if (!(c == '.' && isGroupDotAt(formatted, i))) {
|
|
386
|
+
rawIdx++
|
|
387
|
+
}
|
|
388
|
+
i++
|
|
389
|
+
}
|
|
390
|
+
return rawIdx
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private fun rawCaretToFormatted(rawCaret: Int, formatted: String): Int {
|
|
394
|
+
var rawSeen = 0
|
|
395
|
+
var i = 0
|
|
396
|
+
while (i < formatted.length) {
|
|
397
|
+
val c = formatted[i]
|
|
398
|
+
if (!(c == '.' && isGroupDotAt(formatted, i))) {
|
|
399
|
+
if (rawSeen == rawCaret) return i
|
|
400
|
+
rawSeen++
|
|
401
|
+
}
|
|
402
|
+
i++
|
|
403
|
+
}
|
|
404
|
+
return formatted.length
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
companion object {
|
|
408
|
+
fun CalculatorEditText.calculate() : String {
|
|
409
|
+
val invalid = "invalid"
|
|
410
|
+
val raw = this.text?.toString().orEmpty()
|
|
411
|
+
val normalized = raw.replace(".", "")
|
|
412
|
+
.replace("×", "*")
|
|
413
|
+
.replace("÷", "/")
|
|
414
|
+
|
|
415
|
+
val pattern = "^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$"
|
|
416
|
+
if (Regex(pattern).matches(normalized)) {
|
|
417
|
+
try {
|
|
418
|
+
val result = eval(normalized)?.toString() ?: return invalid
|
|
419
|
+
return result
|
|
420
|
+
} catch (_: Exception) { return invalid }
|
|
421
|
+
} else {
|
|
422
|
+
return invalid
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
private fun eval(str: String): Long? {
|
|
426
|
+
val e = Expression(str)
|
|
427
|
+
println("Expression: $e")
|
|
428
|
+
return if (e.checkSyntax()) {
|
|
429
|
+
e.calculate().toLong()
|
|
430
|
+
} else {
|
|
431
|
+
null
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
301
435
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
package com.calculatorkeyboard
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.facebook.react.bridge.Arguments
|
|
5
|
+
import com.facebook.react.bridge.ReactContext
|
|
6
|
+
import com.facebook.react.bridge.WritableMap
|
|
7
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
8
|
+
import com.facebook.react.uimanager.events.Event
|
|
9
|
+
|
|
10
|
+
class OnChangeEvent(
|
|
11
|
+
surfaceId: Int,
|
|
12
|
+
viewId: Int,
|
|
13
|
+
private val payload: WritableMap
|
|
14
|
+
) : Event<OnChangeEvent>(surfaceId, viewId) {
|
|
15
|
+
override fun getEventName() = "onChange"
|
|
16
|
+
|
|
17
|
+
override fun getEventData() = payload
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class OnKeyPressEvent(
|
|
21
|
+
surfaceId: Int,
|
|
22
|
+
viewId: Int,
|
|
23
|
+
private val payload: WritableMap
|
|
24
|
+
) : Event<OnKeyPressEvent>(surfaceId, viewId) {
|
|
25
|
+
override fun getEventName() = "onKeyPress"
|
|
26
|
+
|
|
27
|
+
override fun getEventData() = payload
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class OnResultEvent(
|
|
31
|
+
surfaceId: Int,
|
|
32
|
+
viewId: Int,
|
|
33
|
+
private val payload: WritableMap
|
|
34
|
+
) : Event<OnResultEvent>(surfaceId, viewId) {
|
|
35
|
+
override fun getEventName() = "onResult"
|
|
36
|
+
|
|
37
|
+
override fun getEventData() = payload
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class OnFocusEvent(
|
|
41
|
+
surfaceId: Int,
|
|
42
|
+
viewId: Int
|
|
43
|
+
) : Event<OnFocusEvent>(surfaceId, viewId) {
|
|
44
|
+
override fun getEventName() = "onFocus"
|
|
45
|
+
|
|
46
|
+
override fun getEventData() = null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class OnBlurEvent(
|
|
50
|
+
surfaceId: Int,
|
|
51
|
+
viewId: Int
|
|
52
|
+
) : Event<OnBlurEvent>(surfaceId, viewId) {
|
|
53
|
+
override fun getEventName() = "onBlur"
|
|
54
|
+
|
|
55
|
+
override fun getEventData() = null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class OnCustomKeyPressEvent(
|
|
59
|
+
surfaceId: Int,
|
|
60
|
+
viewId: Int
|
|
61
|
+
) : Event<OnCustomKeyPressEvent>(surfaceId, viewId) {
|
|
62
|
+
override fun getEventName() = "onCustomKeyEvent"
|
|
63
|
+
|
|
64
|
+
override fun getEventData() = null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
object Event {
|
|
68
|
+
fun emitKeyPress(context: Context, tag: Int, key: String) {
|
|
69
|
+
val reactContext = context as ReactContext
|
|
70
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
71
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
72
|
+
val payload =
|
|
73
|
+
Arguments.createMap().apply {
|
|
74
|
+
putString("key", key)
|
|
75
|
+
}
|
|
76
|
+
val event = OnKeyPressEvent(surfaceId, tag, payload)
|
|
77
|
+
|
|
78
|
+
eventDispatcher?.dispatchEvent(event)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun emitCustomKey(context: Context, tag: Int) {
|
|
82
|
+
val reactContext = context as ReactContext
|
|
83
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
84
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
85
|
+
|
|
86
|
+
val event = OnCustomKeyPressEvent(surfaceId, tag)
|
|
87
|
+
|
|
88
|
+
eventDispatcher?.dispatchEvent(event)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fun emitOnChange(context: Context, tag: Int, text: String) {
|
|
92
|
+
val reactContext = context as ReactContext
|
|
93
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
94
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
95
|
+
val payload =
|
|
96
|
+
Arguments.createMap().apply {
|
|
97
|
+
putString("text", text)
|
|
98
|
+
}
|
|
99
|
+
val event = OnChangeEvent(surfaceId, tag, payload)
|
|
100
|
+
|
|
101
|
+
eventDispatcher?.dispatchEvent(event)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fun emitOnBlurFocus(context: Context, tag: Int, isFocus: Boolean) {
|
|
105
|
+
val reactContext = context as ReactContext
|
|
106
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
107
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
108
|
+
|
|
109
|
+
val event = if (isFocus) OnFocusEvent(surfaceId, tag) else OnBlurEvent(surfaceId, tag)
|
|
110
|
+
|
|
111
|
+
eventDispatcher?.dispatchEvent(event)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun emitOnResult(context: Context, tag: Int, result: String) {
|
|
115
|
+
val reactContext = context as ReactContext
|
|
116
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
117
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
118
|
+
val payload =
|
|
119
|
+
Arguments.createMap().apply {
|
|
120
|
+
putString("result", result)
|
|
121
|
+
}
|
|
122
|
+
val event = OnResultEvent(surfaceId, tag, payload)
|
|
123
|
+
|
|
124
|
+
eventDispatcher?.dispatchEvent(event)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|