@momo-kits/calculator-keyboard 0.150.2-beta.2 → 0.150.2-beta.20-sp.1

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 CHANGED
@@ -10,15 +10,57 @@ npm install react-native-calculator-keyboard
10
10
 
11
11
  ## Usage
12
12
 
13
-
14
13
  ```js
15
- import { CalculatorKeyboardView } from "react-native-calculator-keyboard";
14
+ import InputCalculator from '@momo-kits/calculator-keyboard';
16
15
 
17
16
  // ...
18
17
 
19
- <CalculatorKeyboardView color="tomato" />
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
 
@@ -12,6 +12,6 @@ class CalculatorKeyboardPackage : ReactPackage {
12
12
  }
13
13
 
14
14
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
15
- return listOf(RCTInputCalculator())
15
+ return listOf(InputCalculatorManager())
16
16
  }
17
17
  }
@@ -7,55 +7,71 @@ import android.graphics.drawable.GradientDrawable
7
7
  import android.view.Gravity
8
8
  import android.widget.Button
9
9
  import android.widget.ImageButton
10
- import androidx.appcompat.app.AppCompatActivity
11
10
  import androidx.constraintlayout.widget.ConstraintLayout
12
- import androidx.core.graphics.ColorUtils
13
11
  import com.facebook.react.uimanager.ThemedReactContext
14
12
  import org.mariuszgromada.math.mxparser.Expression
15
13
  import androidx.core.graphics.toColorInt
16
- import com.facebook.react.uimanager.PixelUtil.dpToPx
17
-
14
+ import com.calculatorkeyboard.RCTInputCalculator.Companion.calculatorHeight
15
+ import com.facebook.react.bridge.Arguments
16
+ 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 keys = listOf(
25
- listOf("AC", "÷", "×", "back"),
26
- listOf("7", "8", "9", "-"),
27
- listOf("4", "5", "6", "+"),
28
- listOf("1", "2", "3", "="),
29
- listOf("000", "0")
23
+ private val numWithCTAKeys = listOf(
24
+ listOf("1", "2", "3", "÷", "back"),
25
+ listOf("4", "5", "6", "×", "="),
26
+ listOf("7", "8", "9", "-", "Tiếp"),
27
+ listOf("000", "0", "+")
28
+ )
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", "+")
30
34
  )
31
- private val specialKeys = listOf("=", "-", "×", "÷", "AC", "back", "+")
35
+ private val specialKeys = listOf("=", "-", "×", "÷", "back", "+", "AC")
32
36
  private val separatorWidth = 8f
33
37
  private var specialButtonColor: Int = "#D8D8D8".toColorInt()
34
38
 
35
- init {
36
- val activity = context.currentActivity as? AppCompatActivity
37
- if (activity != null) {
38
- val displayMetrics = resources.displayMetrics
39
- val widthButton = (displayMetrics.widthPixels - separatorWidth * 2 - 3 * separatorWidth) / 4f
40
- val heightButton = (290.dpToPx() - separatorWidth * 2 - 4 * separatorWidth) / 5
39
+ private var keyboardMode: String = "NumDefault"
41
40
 
42
- renderUI(widthButton, heightButton)
43
- }
41
+ private var customKeyButton: Button? = null
42
+ private var customKeyButtonBackground: Int = "#D8D8D8".toColorInt()
43
+ private var customKeyButtonTextColor: Int = Color.BLACK
44
+ private var customKeyButtonState: String = "default"
44
45
 
46
+ init {
47
+ isClickable = false
48
+ isFocusable = false
49
+ isFocusableInTouchMode = false
50
+ clipToPadding = false
51
+ clipChildren = false
45
52
  }
46
53
 
47
- private fun renderUI(buttonWidth: Float, buttonHeight: Float) {
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
+
48
59
  var yOffset = separatorWidth
49
- for ((_, row) in keys.withIndex()) {
60
+ val keys = if (keyboardMode == "NumWithCTA") numWithCTAKeys else defaultKeys
61
+ for ((rowIndex, row) in keys.withIndex()) {
50
62
  var xOffset = separatorWidth
51
- for ((_, key) in row.withIndex()) {
63
+ for ((colIndex, key) in row.withIndex()) {
64
+ val isMainKey = rowIndex == 2 && colIndex == 4
65
+ val isMainCTAKey = isMainKey && keyboardMode == "NumWithCTA"
52
66
  val width = if (key == "000") buttonWidth * 2 + separatorWidth else buttonWidth
53
- val height = if (key == "=") buttonWidth + separatorWidth else buttonHeight
67
+ val height = if (isMainKey) buttonHeight * 2 + separatorWidth else buttonHeight
54
68
 
55
69
  val button = if (key == "back") {
56
70
  createImageButton(key, xOffset, yOffset, buttonWidth.toInt(), buttonHeight.toInt())
57
71
  } else {
58
- createButton(key, xOffset, yOffset, width.toInt(), height.toInt())
72
+ createButton(key, xOffset, yOffset, width.toInt(), height.toInt(), isMainKey, isMainCTAKey).also { b ->
73
+ if (isMainCTAKey) customKeyButton = b
74
+ }
59
75
  }
60
76
 
61
77
  addView(button)
@@ -72,8 +88,9 @@ class CustomKeyboardView(
72
88
  yOffset: Float,
73
89
  buttonWidth: Int,
74
90
  buttonHeight: Int,
91
+ isMainKey: Boolean,
92
+ isMainCTAKey: Boolean
75
93
  ): Button {
76
- val specialKeys = listOf("=", "-", "×", "÷", "AC", "back", "+")
77
94
  return Button(context).apply {
78
95
  val shapeInit = GradientDrawable().apply {
79
96
  shape = GradientDrawable.RECTANGLE
@@ -85,9 +102,11 @@ class CustomKeyboardView(
85
102
  background = shapeInit
86
103
  text = key
87
104
  setTypeface(typeface)
88
- textSize = 24.toFloat()
105
+ textSize = (if (isMainCTAKey) 18 else 24).toFloat()
89
106
  setTextColor(Color.BLACK)
90
107
  stateListAnimator = null
108
+ maxLines = 1
109
+ isAllCaps = false
91
110
  layoutParams = LayoutParams(
92
111
  buttonWidth,
93
112
  buttonHeight
@@ -95,7 +114,7 @@ class CustomKeyboardView(
95
114
  constrainedWidth = false
96
115
  }
97
116
 
98
- if (specialKeys.contains(key)) {
117
+ if (specialKeys.contains(key) || isMainKey) {
99
118
  background = GradientDrawable().apply {
100
119
  shape = GradientDrawable.RECTANGLE
101
120
  cornerRadius = 24f
@@ -104,10 +123,13 @@ class CustomKeyboardView(
104
123
  setTextColor(Color.BLACK)
105
124
  }
106
125
 
126
+ isClickable = true
127
+ isFocusable = false
128
+ isFocusableInTouchMode = false
107
129
 
108
130
  translationX = xOffset.toInt().toFloat()
109
131
  translationY = yOffset.toInt().toFloat()
110
- setOnClickListener { onKeyPress(key) }
132
+ setOnClickListener { onKeyPress(key, isMainCTAKey) }
111
133
  }
112
134
  }
113
135
 
@@ -132,11 +154,16 @@ class CustomKeyboardView(
132
154
  ).apply {
133
155
  constrainedWidth = false
134
156
  }
157
+
158
+ isClickable = true
159
+ isFocusable = false
160
+ isFocusableInTouchMode = false
161
+
135
162
  translationX = xOffset
136
163
  translationY = yOffset
137
164
  setImageResource(android.R.drawable.ic_input_delete)
138
165
  setImageTintList(ColorStateList.valueOf(Color.BLACK))
139
- setOnClickListener { onKeyPress(key) }
166
+ setOnClickListener { onKeyPress(key, false) }
140
167
  }
141
168
  }
142
169
 
@@ -172,25 +199,19 @@ class CustomKeyboardView(
172
199
  }
173
200
  }
174
201
 
175
- private fun onKeyPress(key: String) {
176
- when (key) {
177
- "AC" -> {
178
- clearText()
179
- }
180
-
181
- "back" -> {
182
- onBackSpace()
183
- }
184
-
185
- "=" -> {
186
- calculateResult()
187
- }
202
+ private fun onKeyPress(key: String, isMainCTAKey: Boolean) {
203
+ if (isMainCTAKey) {
204
+ emitCustomKey()
205
+ return
206
+ }
188
207
 
208
+ emitKeyPress(key)
209
+ when (key) {
210
+ "AC" -> clearText()
211
+ "back" -> onBackSpace()
212
+ "=" -> calculateResult()
189
213
  "×", "+", "-", "÷" -> keyDidPress(" $key ")
190
-
191
- else -> {
192
- editText.text?.insert(editText.selectionStart, key)
193
- }
214
+ else -> editText.text?.insert(editText.selectionStart, key)
194
215
  }
195
216
  }
196
217
 
@@ -239,4 +260,54 @@ class CustomKeyboardView(
239
260
  }
240
261
  }
241
262
 
263
+ private fun emitKeyPress(key: String) {
264
+ val reactContext = context as ThemedReactContext
265
+ val params = Arguments.createMap().apply {
266
+ putString("key", key)
267
+ }
268
+ reactContext.getJSModule(RCTEventEmitter::class.java)
269
+ .receiveEvent(editText.id, "onKeyPress", params)
270
+ }
271
+
272
+ private fun emitCustomKey() {
273
+ val reactContext = context as ThemedReactContext
274
+ reactContext.getJSModule(RCTEventEmitter::class.java)
275
+ .receiveEvent(editText.id, "onCustomKeyEvent", null)
276
+ }
277
+
278
+ fun setCustomKeyText(text: String) {
279
+ customKeyButton?.text = text
280
+ }
281
+
282
+ fun setMode(mode: String) {
283
+ keyboardMode = mode
284
+ renderUI()
285
+ }
286
+
287
+ fun setCustomKeyBackground(background: Int) {
288
+ customKeyButtonBackground = background
289
+ updateCustomKeyUI(background, customKeyButtonTextColor)
290
+ }
291
+
292
+ fun setCustomKeyTextColor(textColor: Int) {
293
+ customKeyButtonTextColor = textColor
294
+ updateCustomKeyUI(customKeyButtonBackground, textColor)
295
+ }
296
+
297
+
298
+ fun setCustomKeyState(state: String) {
299
+ customKeyButtonState = state
300
+ customKeyButton?.isEnabled = state != "disable"
301
+ updateCustomKeyUI(customKeyButtonBackground, customKeyButtonTextColor)
302
+ }
303
+
304
+ private fun updateCustomKeyUI(background: Int, textColor: Int){
305
+ customKeyButton?.background = GradientDrawable().apply {
306
+ shape = GradientDrawable.RECTANGLE
307
+ cornerRadius = 24f
308
+ setColor(background)
309
+ }
310
+ customKeyButton?.setTextColor(textColor)
311
+ }
312
+
242
313
  }
@@ -0,0 +1,232 @@
1
+ package com.calculatorkeyboard
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.app.Activity
5
+ import android.content.Context
6
+ import android.content.ContextWrapper
7
+ import android.util.Log
8
+ import android.view.Gravity
9
+ import android.view.MotionEvent
10
+ import android.view.View
11
+ import android.view.ViewGroup
12
+ import android.widget.FrameLayout
13
+ import androidx.core.view.ViewCompat
14
+ import androidx.core.view.WindowInsetsCompat
15
+ import androidx.core.view.doOnLayout
16
+ import androidx.core.view.updatePadding
17
+ import java.lang.ref.WeakReference
18
+ import androidx.core.view.isNotEmpty
19
+
20
+ internal class KeyboardOverlayHost {
21
+
22
+ private val tag = "KeyboardOverlayHost"
23
+
24
+ private var localRootRef: WeakReference<ViewGroup>? = null
25
+ private var containerRef: WeakReference<FrameLayout>? = null
26
+ private var paddingTargetRef: WeakReference<View>? = null
27
+
28
+ private var originalBottomPadding: Int = 0
29
+ private var isShowing = false
30
+
31
+ private class OverlayContainer(ctx: Context) : FrameLayout(ctx) {
32
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = false
33
+ }
34
+
35
+ fun show(anchorView: View, keyboardView: View, heightPx: Int) {
36
+ val localRoot = findLocalRoot(anchorView) ?: run {
37
+ Log.w(tag, "show: cannot find local root from anchorView")
38
+ return
39
+ }
40
+ val container = ensureContainer(localRoot, anchorView.context) ?: return
41
+
42
+ val paddingTarget = findPaddingTargetWithin(anchorView, localRoot)
43
+ paddingTargetRef = WeakReference(paddingTarget)
44
+ localRootRef = WeakReference(localRoot)
45
+
46
+ val bottomInset = (ViewCompat.getRootWindowInsets(container)
47
+ ?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom) ?: 0
48
+
49
+ val lp = FrameLayout.LayoutParams(
50
+ ViewGroup.LayoutParams.MATCH_PARENT,
51
+ heightPx + bottomInset
52
+ ).apply { gravity = Gravity.BOTTOM }
53
+
54
+ if (keyboardView.parent !== container) {
55
+ container.removeAllViews()
56
+ container.addView(keyboardView, lp)
57
+ } else {
58
+ keyboardView.layoutParams = lp
59
+ }
60
+
61
+ keyboardView.isClickable = true
62
+ keyboardView.isFocusable = false
63
+ keyboardView.isFocusableInTouchMode = false
64
+ keyboardView.visibility = View.VISIBLE
65
+
66
+ if (!isShowing) originalBottomPadding = paddingTarget.paddingBottom
67
+ paddingTarget.updatePadding(bottom = heightPx)
68
+
69
+ container.visibility = View.VISIBLE
70
+ container.bringToFront()
71
+ container.translationZ = 10000f
72
+ container.elevation = 10000f
73
+ localRoot.post { container.bringToFront() }
74
+
75
+ keyboardView.animate().cancel()
76
+ keyboardView.post {
77
+ container.bringToFront()
78
+ keyboardView.doOnLayout { child ->
79
+ val h = child.height.takeIf { it > 0 } ?: heightPx
80
+ child.translationY = h.toFloat()
81
+ child.animate()
82
+ .translationY(0f)
83
+ .setDuration(250L)
84
+ .withStartAction {
85
+ isShowing = true
86
+ child.setLayerType(View.LAYER_TYPE_HARDWARE, null)
87
+ container.bringToFront()
88
+ }
89
+ .withEndAction {
90
+ child.setLayerType(View.LAYER_TYPE_NONE, null)
91
+ }
92
+ .start()
93
+ }
94
+ }
95
+ }
96
+
97
+ fun hide() {
98
+ val container = containerRef?.get() ?: return
99
+ val localRoot = localRootRef?.get() ?: return
100
+ val paddingTarget = paddingTargetRef?.get() ?: localRoot
101
+
102
+ val child = if (container.isNotEmpty())
103
+ container.getChildAt(container.childCount - 1) else null
104
+
105
+ if (child == null) {
106
+ paddingTarget.updatePadding(bottom = originalBottomPadding)
107
+ isShowing = false
108
+ return
109
+ }
110
+
111
+ child.animate().cancel()
112
+ val h = child.height.takeIf { it > 0 } ?: (child.measuredHeight.takeIf { it > 0 } ?: 0)
113
+ if (h == 0) {
114
+ container.removeAllViews()
115
+ paddingTarget.updatePadding(bottom = originalBottomPadding)
116
+ isShowing = false
117
+ return
118
+ }
119
+
120
+ child.animate()
121
+ .translationY(h.toFloat())
122
+ .setDuration(250L)
123
+ .withEndAction {
124
+ container.removeAllViews()
125
+ paddingTarget.updatePadding(bottom = originalBottomPadding)
126
+ isShowing = false
127
+ }
128
+ .start()
129
+ }
130
+
131
+ fun detach() {
132
+ containerRef?.get()?.let { (it.parent as? ViewGroup)?.removeView(it) }
133
+ containerRef = null
134
+ localRootRef = null
135
+ paddingTargetRef = null
136
+ isShowing = false
137
+ originalBottomPadding = 0
138
+ }
139
+
140
+ private fun findLocalRoot(anchor: View): ViewGroup? {
141
+ var cur: View? = anchor
142
+
143
+ while (cur != null) {
144
+ if (cur is ViewGroup && isReactRoot(cur)) {
145
+ return cur
146
+ }
147
+
148
+ val parent = cur.parent
149
+ if (parent is ViewGroup) {
150
+ val parentName = parent::class.java.simpleName
151
+ if (parentName.contains("FragmentContainerView")) {
152
+ return (cur as? ViewGroup) ?: parent
153
+ }
154
+ }
155
+
156
+ cur = (cur.parent as? View)
157
+ }
158
+
159
+ return (anchor.rootView as? ViewGroup)
160
+ }
161
+
162
+ private fun isReactRoot(v: View): Boolean {
163
+ val n = v::class.java.simpleName
164
+ return n.contains("ReactRootView") ||
165
+ n.contains("RNGestureHandlerEnabledRootView") ||
166
+ (n.contains("React") && n.contains("Root"))
167
+ }
168
+
169
+ @SuppressLint("ClickableViewAccessibility")
170
+ private fun ensureContainer(localRoot: ViewGroup, ctx: Context): FrameLayout? {
171
+ var container = containerRef?.get()
172
+
173
+ if (container == null || container.parent !== localRoot) {
174
+ container?.let { (it.parent as? ViewGroup)?.removeView(it) }
175
+
176
+ container = OverlayContainer(ctx).apply {
177
+ layoutParams = ViewGroup.LayoutParams(
178
+ ViewGroup.LayoutParams.MATCH_PARENT,
179
+ ViewGroup.LayoutParams.MATCH_PARENT
180
+ )
181
+ isClickable = false
182
+ isFocusable = false
183
+ elevation = 10000f
184
+ translationZ = 10000f
185
+ visibility = View.VISIBLE
186
+ }
187
+
188
+ localRoot.addView(container)
189
+ container.bringToFront()
190
+ localRoot.requestLayout()
191
+ containerRef = WeakReference(container)
192
+ }
193
+
194
+ return container
195
+ }
196
+
197
+ private fun findPaddingTargetWithin(anchor: View, localRoot: ViewGroup): View {
198
+ fun dfs(g: ViewGroup): View? {
199
+ if (isReactRoot(g)) return g
200
+ for (i in 0 until g.childCount) {
201
+ val c = g.getChildAt(i)
202
+ if (c is ViewGroup) {
203
+ val hit = dfs(c)
204
+ if (hit != null) return hit
205
+ } else if (isReactRoot(c)) {
206
+ return c
207
+ }
208
+ }
209
+ return null
210
+ }
211
+
212
+ dfs(localRoot)?.let { return it }
213
+
214
+ var cur: View? = anchor
215
+ var last: View = anchor
216
+ while (cur != null && cur !== localRoot) {
217
+ last = cur
218
+ cur = (cur.parent as? View)
219
+ }
220
+ return last as? ViewGroup ?: localRoot
221
+ }
222
+
223
+ @Suppress("unused")
224
+ private fun findActivityFrom(view: View): Activity? {
225
+ var ctx: Context? = view.context
226
+ while (ctx is ContextWrapper) {
227
+ if (ctx is Activity) return ctx
228
+ ctx = ctx.baseContext
229
+ }
230
+ return null
231
+ }
232
+ }