@bm-fe/react-native-ui-components 1.0.1 → 1.1.4

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.
Files changed (81) hide show
  1. package/android/design/build.gradle +51 -0
  2. package/android/design/consumer-rules.pro +1 -0
  3. package/android/design/proguard-rules.pro +1 -0
  4. package/android/design/src/main/AndroidManifest.xml +2 -0
  5. package/android/design/src/main/java/com/bitmart/react/design/DataUriFetcher.kt +42 -0
  6. package/android/design/src/main/java/com/bitmart/react/design/DesignComponentPackage.kt +35 -0
  7. package/android/design/src/main/java/com/bitmart/react/design/PrimaryXLargeViewManager.kt +785 -0
  8. package/android/design/src/main/java/com/bitmart/react/design/TextButtonViewManager.kt +294 -0
  9. package/ios/DemoProject/NativeDesign/PrimaryButtonViewManager.m +25 -0
  10. package/ios/DemoProject/NativeDesign/PrimaryButtonViewManager.swift +321 -0
  11. package/ios/DemoProject/NativeDesign/TextButtonViewManager.m +21 -0
  12. package/ios/DemoProject/NativeDesign/TextButtonViewManager.swift +184 -0
  13. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Bitmart Card.imageset/Contents.json +21 -0
  14. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Bitmart Card.imageset/Property 1=Bitmart Card.svg +3 -0
  15. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Contents.json +6 -0
  16. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Credit Debit Card.imageset/Contents.json +21 -0
  17. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Credit Debit Card.imageset/Property 1=Credit Debit Card.svg +3 -0
  18. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Crypto Prepaid Card.imageset/Contents.json +21 -0
  19. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Crypto Prepaid Card.imageset/Property 1=Crypto Prepaid Card.svg +3 -0
  20. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Mobile Recharge.imageset/Contents.json +21 -0
  21. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Mobile Recharge.imageset/Mobile Recharge.svg +3 -0
  22. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/P2P Trading.imageset/Contents.json +21 -0
  23. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/P2P Trading.imageset/Property 1=P2P Trading.svg +3 -0
  24. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/SEPA Deposit.imageset/Contents.json +21 -0
  25. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/SEPA Deposit.imageset/SEPA Deposit.svg +3 -0
  26. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Third-Party Payment.imageset/Contents.json +21 -0
  27. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/BuyCrypto/Third-Party Payment.imageset/Property 1=Third-Party Payment.svg +3 -0
  28. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Components/Contents.json +6 -0
  29. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/Contents.json +6 -0
  30. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/checkmark.imageset/Contents.json +21 -0
  31. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/checkmark.imageset/checkmark.pdf +0 -0
  32. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/close_icon.imageset/Contents.json +22 -0
  33. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/close_icon.imageset/close_icon@2x.png +0 -0
  34. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/close_icon.imageset/close_icon@3x.png +0 -0
  35. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/cross.imageset/Contents.json +21 -0
  36. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/cross.imageset/cross.pdf +0 -0
  37. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/progress.imageset/Contents.json +21 -0
  38. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/progress.imageset/progress.pdf +0 -0
  39. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/progress_circular.imageset/Contents.json +21 -0
  40. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/progress_circular.imageset/progress_circular.pdf +0 -0
  41. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/refresh_footer_dark.imageset/Contents.json +22 -0
  42. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/refresh_footer_dark.imageset/refresh_footer_dark@2x.png +0 -0
  43. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/refresh_footer_dark.imageset/refresh_footer_dark@3x.png +0 -0
  44. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/refresh_footer_light.imageset/Contents.json +22 -0
  45. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/refresh_footer_light.imageset/refresh_footer_light@2x.png +0 -0
  46. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/refresh_footer_light.imageset/refresh_footer_light@3x.png +0 -0
  47. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/search_icon.imageset/Contents.json +21 -0
  48. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/search_icon.imageset/Group 13994.svg +7 -0
  49. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_dark_chose.imageset/Contents.json +21 -0
  50. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_dark_chose.imageset/Frame.svg +3 -0
  51. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_list_arrow.imageset/Contents.json +22 -0
  52. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_list_arrow.imageset/sheet_list_arrow@2x.png +0 -0
  53. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_list_arrow.imageset/sheet_list_arrow@3x.png +0 -0
  54. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_list_cell_checkbox.imageset/Contents.json +21 -0
  55. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_list_cell_checkbox.imageset/Frame (1).svg +3 -0
  56. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_list_chose.imageset/Contents.json +21 -0
  57. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/sheet_list_chose.imageset/Frame.png +0 -0
  58. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/slider_bubble.imageset/Contents.json +22 -0
  59. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/slider_bubble.imageset/slider_bubble@2x.png +0 -0
  60. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/slider_bubble.imageset/slider_bubble@3x.png +0 -0
  61. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/spot_second_floor_refresh_arrow.imageset/Contents.json +21 -0
  62. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Assets.xcassets/spot_second_floor_refresh_arrow.imageset/spot_second_floor_refresh_arrow.svg +8 -0
  63. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Font/Alexandria-Medium.ttf +0 -0
  64. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Font/Alexandria-Regular.ttf +0 -0
  65. package/ios/Modules/BMUIComponents/BMUIComponents/Assets/Font/Alexandria-SemiBold.ttf +0 -0
  66. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/BMFont/BMFont.swift +82 -0
  67. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/BMFont/UIFontExtensions.swift +120 -0
  68. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/Components/AlertView/BMComponentAlertController.swift +574 -0
  69. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/Components/Buttons/BMComponentButton+Examples.swift +77 -0
  70. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/Components/Buttons/BMComponentButton.swift +373 -0
  71. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/Components/Buttons/BMComponentButtonConfiguration.swift +181 -0
  72. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/Components/Popup/BMComponentPopupController.swift +312 -0
  73. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/Components/SegmentView/BMComponentSegmentedTitleCell.swift +294 -0
  74. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/Components/SegmentView/BMComponentSegmentedTitleDataSource.swift +49 -0
  75. package/ios/Modules/BMUIComponents/BMUIComponents/Classes/Components/SegmentView/BMComponentSegmentedView.swift +292 -0
  76. package/ios/Modules/BMUIComponents/LICENSE +19 -0
  77. package/ios/Modules/BMUIComponents/README.md +29 -0
  78. package/package.json +10 -1
  79. package/react-native-ui-components.podspec +52 -0
  80. package/react-native.config.js +16 -0
  81. package/src/screens/NativeButtonsScreen.tsx +0 -335
@@ -0,0 +1,294 @@
1
+ package com.bitmart.react.design
2
+
3
+ import android.util.Log
4
+ import android.view.ContextThemeWrapper
5
+ import android.view.View
6
+ import androidx.appcompat.widget.AppCompatButton
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.ReactContext
9
+ import com.facebook.react.common.MapBuilder
10
+ import com.facebook.react.uimanager.PixelUtil
11
+ import com.facebook.react.uimanager.SimpleViewManager
12
+ import com.facebook.react.uimanager.ThemedReactContext
13
+ import com.facebook.react.uimanager.annotations.ReactProp
14
+ import com.facebook.react.uimanager.events.RCTEventEmitter
15
+
16
+ /**
17
+ * TextButtonViewManager - 专门用于文本按钮的 ViewManager
18
+ *
19
+ * 支持的样式:
20
+ * - Black: Widget.BitMart4.Button.TextButton.Black(黑色文本)
21
+ * - Gray: Widget.BitMart4.Button.TextButton.Gray(灰色文本)
22
+ * - Blue: Widget.BitMart4.Button.TextButton.Blue(蓝色文本)
23
+ */
24
+ class TextButtonViewManager : SimpleViewManager<AppCompatButton>() {
25
+
26
+ companion object {
27
+ private const val TAG = "TextButtonViewManager"
28
+ const val REACT_CLASS = "TextButton"
29
+ const val EVENT_ON_PRESS = "onPress"
30
+ const val EVENT_ON_SIZE_CHANGE = "onSizeChange"
31
+
32
+ /**
33
+ * TextButton 样式名称到资源 ID 的映射表
34
+ */
35
+ private val STYLE_MAP = mapOf(
36
+ "Black" to com.bitmart.design.R.style.Widget_BitMart4_Button_TextButton_Black,
37
+ "Gray" to com.bitmart.design.R.style.Widget_BitMart4_Button_TextButton_Gray,
38
+ "Blue" to com.bitmart.design.R.style.Widget_BitMart4_Button_TextButton_Blue
39
+ )
40
+
41
+ fun getStyleResId(styleName: String?): Int {
42
+ return STYLE_MAP[styleName] ?: com.bitmart.design.R.style.Widget_BitMart4_Button_TextButton_Black
43
+ }
44
+ }
45
+
46
+ // 保存按钮的样式名称和 ReactContext
47
+ private val buttonStyleMap = mutableMapOf<AppCompatButton, String>()
48
+ private val buttonContextMap = mutableMapOf<AppCompatButton, ReactContext>()
49
+
50
+ override fun getName(): String = REACT_CLASS
51
+
52
+ override fun createViewInstance(reactContext: ThemedReactContext): AppCompatButton {
53
+ // 默认使用 Black 样式
54
+ val defaultStyleName = "Black"
55
+ val styleResId = getStyleResId(defaultStyleName)
56
+
57
+ // 使用 ContextThemeWrapper 让 AppCompatButton 自动应用样式
58
+ val themedContext = ContextThemeWrapper(reactContext, styleResId)
59
+ val button = AppCompatButton(themedContext)
60
+
61
+ // 设置文本按钮特有属性
62
+ button.isAllCaps = false
63
+
64
+ // 文本按钮特有设置:无最小尺寸限制,宽度完全自适应
65
+ button.minimumWidth = 0
66
+ button.minimumHeight = 0
67
+
68
+ // 确保背景正确应用(文本按钮通常为透明背景)
69
+ applyBackground(button, styleResId)
70
+
71
+ // 保存样式名称和原始 ReactContext
72
+ buttonStyleMap[button] = defaultStyleName
73
+ buttonContextMap[button] = reactContext
74
+
75
+ return button
76
+ }
77
+
78
+ /**
79
+ * 设置按钮文本
80
+ */
81
+ @ReactProp(name = "text")
82
+ fun setText(button: AppCompatButton, text: String?) {
83
+ val oldText = button.text.toString()
84
+ val newText = text ?: ""
85
+
86
+ button.text = newText
87
+
88
+ // 文本变化后,测量并发送尺寸
89
+ if (oldText != newText) {
90
+ measureAndSendSize(button)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 设置样式名称(动态切换样式)
96
+ * @param styleName 样式名称,如 "Black", "Gray", "Blue"
97
+ */
98
+ @ReactProp(name = "styleName")
99
+ fun setStyleName(button: AppCompatButton, styleName: String?) {
100
+ if (styleName.isNullOrEmpty()) {
101
+ return
102
+ }
103
+
104
+ val currentStyle = buttonStyleMap[button]
105
+ if (currentStyle == styleName) {
106
+ return
107
+ }
108
+
109
+ val styleResId = getStyleResId(styleName)
110
+
111
+ if (STYLE_MAP.containsKey(styleName)) {
112
+ // 使用简化的样式应用方法
113
+ applyCompleteStyle(button, styleResId, styleName)
114
+ measureAndSendSize(button)
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 设置启用状态
120
+ */
121
+ @ReactProp(name = "enabled", defaultBoolean = true)
122
+ fun setEnabled(button: AppCompatButton, enabled: Boolean) {
123
+ button.isEnabled = enabled
124
+ // 强制刷新以触发 StateListDrawable 状态更新
125
+ button.refreshDrawableState()
126
+ }
127
+
128
+ /**
129
+ * 测量按钮尺寸并发送给 RN
130
+ */
131
+ private fun measureAndSendSize(button: AppCompatButton) {
132
+ button.post {
133
+ // 使用 WRAP_CONTENT 测量按钮的内在尺寸
134
+ val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
135
+ val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
136
+ button.measure(widthMeasureSpec, heightMeasureSpec)
137
+
138
+ val measuredWidth = button.measuredWidth
139
+ val measuredHeight = button.measuredHeight
140
+
141
+ // 如果 measure 返回 0,使用文字测量 + padding 作为备选方案
142
+ val finalWidth = if (measuredWidth > 0) {
143
+ measuredWidth
144
+ } else {
145
+ val textWidth = button.paint.measureText(button.text.toString())
146
+ (textWidth + button.paddingStart + button.paddingEnd).toInt()
147
+ }
148
+ val finalHeight = if (measuredHeight > 0) measuredHeight else button.height
149
+
150
+ // 转换为 dp
151
+ val widthDp = PixelUtil.toDIPFromPixel(finalWidth.toFloat())
152
+ val heightDp = PixelUtil.toDIPFromPixel(finalHeight.toFloat())
153
+
154
+ // 发送尺寸变化事件给 RN
155
+ val reactContext = buttonContextMap[button]
156
+ if (reactContext != null) {
157
+ val event = Arguments.createMap().apply {
158
+ putDouble("width", widthDp.toDouble())
159
+ putDouble("height", heightDp.toDouble())
160
+ }
161
+ reactContext.getJSModule(RCTEventEmitter::class.java)
162
+ .receiveEvent(button.id, EVENT_ON_SIZE_CHANGE, event)
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 应用背景和最小尺寸设置
169
+ * 文本按钮的背景通常为 @null(透明),需要明确设置
170
+ */
171
+ private fun applyBackground(button: AppCompatButton, styleResId: Int) {
172
+ try {
173
+ val context = button.context
174
+ val typedArray = context.obtainStyledAttributes(
175
+ styleResId,
176
+ intArrayOf(android.R.attr.background)
177
+ )
178
+
179
+ // 对于文本按钮,背景可能是 @null,需要明确设置(包括 null 值)
180
+ val background = typedArray.getDrawable(0)
181
+ button.background = background // 可能为 null,这对文本按钮是正确的
182
+
183
+ Log.d(TAG, "Applied background: ${if (background == null) "null (transparent)" else "drawable"}")
184
+
185
+ typedArray.recycle()
186
+
187
+ // 强制设置 minWidth 和 minHeight 为 0,让按钮宽度自适应文字
188
+ button.minimumWidth = 0
189
+ button.minimumHeight = 0
190
+ } catch (e: Exception) {
191
+ Log.e(TAG, "Error applying background", e)
192
+ }
193
+ }
194
+
195
+ /**
196
+ * 完整样式应用方法 - 自动应用所有样式,无需手动解析
197
+ * 通过创建临时按钮获取完整样式,然后复制到目标按钮
198
+ */
199
+ private fun applyCompleteStyle(button: AppCompatButton, styleResId: Int, styleName: String) {
200
+ try {
201
+ // 保存当前按钮状态
202
+ val currentText = button.text
203
+ val currentEnabled = button.isEnabled
204
+ val originalContext = buttonContextMap[button] ?: return
205
+
206
+ // 创建一个临时按钮来获取完整的样式属性
207
+ val themedContext = ContextThemeWrapper(originalContext, styleResId)
208
+ val tempButton = AppCompatButton(themedContext)
209
+
210
+ // 从临时按钮复制所有样式属性到当前按钮
211
+ copyButtonStyle(tempButton, button)
212
+
213
+ // 复用现有的背景应用方法(确保背景正确生效,并自动设置最小尺寸)
214
+ applyBackground(button, styleResId)
215
+
216
+ // 手动设置其他必要属性
217
+ button.isAllCaps = false
218
+
219
+ // 恢复按钮状态
220
+ button.text = currentText
221
+ button.isEnabled = currentEnabled
222
+
223
+ // 强制刷新视图以确保样式变化生效
224
+ button.invalidate()
225
+ button.requestLayout()
226
+
227
+ // 更新样式映射
228
+ buttonStyleMap[button] = styleName
229
+
230
+ } catch (e: Exception) {
231
+ Log.e(TAG, "Error applying complete style", e)
232
+ }
233
+ }
234
+
235
+ /**
236
+ * 从源按钮复制完整样式到目标按钮
237
+ */
238
+ private fun copyButtonStyle(sourceButton: AppCompatButton, targetButton: AppCompatButton) {
239
+ // 复制文本样式
240
+ targetButton.setTextColor(sourceButton.textColors)
241
+ targetButton.textSize = sourceButton.textSize / targetButton.resources.displayMetrics.scaledDensity
242
+ targetButton.typeface = sourceButton.typeface
243
+
244
+ // 复制 padding
245
+ targetButton.setPadding(
246
+ sourceButton.paddingStart,
247
+ sourceButton.paddingTop,
248
+ sourceButton.paddingEnd,
249
+ sourceButton.paddingBottom
250
+ )
251
+
252
+ // 复制 drawable padding
253
+ targetButton.compoundDrawablePadding = sourceButton.compoundDrawablePadding
254
+
255
+ // 复制 compound drawables 和 tint
256
+ val drawables = sourceButton.compoundDrawables
257
+ targetButton.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3])
258
+ if (sourceButton.compoundDrawableTintList != null) {
259
+ targetButton.compoundDrawableTintList = sourceButton.compoundDrawableTintList
260
+ }
261
+
262
+ // 注意:背景通过单独的 applyBackground 方法处理,确保正确应用
263
+ }
264
+
265
+
266
+ /**
267
+ * 导出事件映射
268
+ */
269
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
270
+ return MapBuilder.builder<String, Any>()
271
+ .put(EVENT_ON_PRESS, MapBuilder.of("registrationName", EVENT_ON_PRESS))
272
+ .put(EVENT_ON_SIZE_CHANGE, MapBuilder.of("registrationName", EVENT_ON_SIZE_CHANGE))
273
+ .build()
274
+ }
275
+
276
+ /**
277
+ * 添加点击事件监听
278
+ */
279
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: AppCompatButton) {
280
+ view.setOnClickListener {
281
+ reactContext.getJSModule(RCTEventEmitter::class.java)
282
+ .receiveEvent(view.id, EVENT_ON_PRESS, null)
283
+ }
284
+ }
285
+
286
+ /**
287
+ * 清理资源
288
+ */
289
+ override fun onDropViewInstance(view: AppCompatButton) {
290
+ super.onDropViewInstance(view)
291
+ buttonStyleMap.remove(view)
292
+ buttonContextMap.remove(view)
293
+ }
294
+ }
@@ -0,0 +1,25 @@
1
+ //
2
+ // PrimaryButtonViewManager.m
3
+ // BitMartUS
4
+ //
5
+ // Created by React Native on 2026/01/24.
6
+ //
7
+
8
+ #import <React/RCTViewManager.h>
9
+
10
+ @interface RCT_EXTERN_MODULE(PrimaryButtonViewManager, RCTViewManager)
11
+
12
+ // 导出属性
13
+ RCT_EXPORT_VIEW_PROPERTY(text, NSString)
14
+ RCT_EXPORT_VIEW_PROPERTY(styleName, NSString)
15
+ RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL)
16
+ RCT_EXPORT_VIEW_PROPERTY(loading, BOOL)
17
+ RCT_EXPORT_VIEW_PROPERTY(iconSvgString, NSString)
18
+ RCT_EXPORT_VIEW_PROPERTY(iconColor, NSString)
19
+ RCT_EXPORT_VIEW_PROPERTY(iconPosition, NSString)
20
+
21
+ // 导出事件 - 使用 RCTBubblingEventBlock 避免重复注册
22
+ RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
23
+ RCT_EXPORT_VIEW_PROPERTY(onSizeChange, RCTDirectEventBlock)
24
+
25
+ @end
@@ -0,0 +1,321 @@
1
+ //
2
+ // PrimaryButtonViewManager.swift
3
+ // BitMartUS
4
+ //
5
+ // Created by React Native on 2026/01/24.
6
+ //
7
+
8
+ import UIKit
9
+ import React
10
+ import BMUIComponents
11
+ import SnapKit
12
+ import SDWebImage
13
+ import SDWebImageSVGCoder
14
+
15
+ /// iOS 原生按钮 ViewManager - 对应 Android 的 PrimaryXLargeViewManager
16
+ /// 基于 BMComponentButton 实现,支持所有 Primary/Secondary/Green/Red/White 样式
17
+ @objc(PrimaryButtonViewManager)
18
+ class PrimaryButtonViewManager: RCTViewManager {
19
+
20
+ override static func requiresMainQueueSetup() -> Bool {
21
+ return true
22
+ }
23
+
24
+ override func view() -> UIView! {
25
+ return PrimaryButtonView()
26
+ }
27
+ }
28
+
29
+ /// iOS 原生按钮视图 - 基于 BMComponentButton
30
+ class PrimaryButtonView: UIView {
31
+
32
+ // MARK: - Properties
33
+ private var bmButton: BMComponentButton
34
+ private var currentStyleName: String = "Primary.Large"
35
+
36
+ /// RN 事件回调
37
+ @objc var onPress: RCTBubblingEventBlock?
38
+ @objc var onSizeChange: RCTDirectEventBlock?
39
+
40
+ // MARK: - Initialization
41
+ override init(frame: CGRect) {
42
+ // 默认创建 Primary.Large 按钮
43
+ bmButton = BMComponentButton.primary(size: .Large)
44
+ super.init(frame: frame)
45
+ setupButton()
46
+ }
47
+
48
+ required init?(coder: NSCoder) {
49
+ fatalError("init(coder:) has not been implemented")
50
+ }
51
+
52
+ // MARK: - Setup
53
+ private func setupButton() {
54
+ // 避免 RN 设置的 frame 产生 NSAutoresizingMaskLayoutConstraint 与 BMComponentButton 固定高度冲突
55
+ translatesAutoresizingMaskIntoConstraints = false
56
+
57
+ // 降低水平方向 content hugging,让视图愿意被拉满父级宽度(Footer 全屏宽)
58
+ setContentHuggingPriority(.defaultLow, for: .horizontal)
59
+ setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
60
+
61
+ addSubview(bmButton)
62
+
63
+ // 按钮与容器四边对齐;低 content hugging 让按钮愿意被拉满容器宽度(否则会卡在文字宽度 ~96pt)
64
+ bmButton.setContentHuggingPriority(.defaultLow, for: .horizontal)
65
+ bmButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
66
+ bmButton.snp.makeConstraints { make in
67
+ make.edges.equalToSuperview()
68
+ }
69
+
70
+ // 添加点击事件
71
+ bmButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
72
+
73
+ // 发送初始尺寸
74
+ sendSizeChangeEvent()
75
+ }
76
+
77
+ // MARK: - Event Handlers
78
+ @objc private func buttonTapped() {
79
+ onPress?([:])
80
+ }
81
+
82
+ private func sendSizeChangeEvent() {
83
+ DispatchQueue.main.async { [weak self] in
84
+ guard let self = self else { return }
85
+ let size = self.bmButton.intrinsicContentSize
86
+
87
+ self.onSizeChange?([
88
+ "width": size.width,
89
+ "height": size.height
90
+ ])
91
+ }
92
+ }
93
+
94
+ // MARK: - RN Props
95
+
96
+ /// 设置按钮文本
97
+ @objc var text: NSString = "" {
98
+ didSet {
99
+ bmButton.title = text as String
100
+ sendSizeChangeEvent()
101
+ }
102
+ }
103
+
104
+ /// 设置样式名称
105
+ @objc var styleName: NSString = "Primary.Large" {
106
+ didSet {
107
+ let styleNameStr = styleName as String
108
+ if currentStyleName != styleNameStr {
109
+ currentStyleName = styleNameStr
110
+ let config = parseStyleName(styleNameStr)
111
+ bmButton.configuration = config
112
+ sendSizeChangeEvent()
113
+ }
114
+ }
115
+ }
116
+
117
+ /// 设置启用状态
118
+ @objc var enabled: Bool = true {
119
+ didSet {
120
+ bmButton.isEnabled = enabled
121
+ }
122
+ }
123
+
124
+ /// 是否显示加载状态(进度条)。为 true 时隐藏文字/图标、显示原生 UIActivityIndicator,与 BMComponentButton 的 isLoading 一致。
125
+ @objc var loading: Bool = false {
126
+ didSet {
127
+ bmButton.isLoading = loading
128
+ }
129
+ }
130
+
131
+ // MARK: - SVG Icon Support
132
+
133
+ /// SVG图标字符串
134
+ @objc var iconSvgString: NSString = "" {
135
+ didSet { loadSvgIcon() }
136
+ }
137
+
138
+ /// 图标颜色
139
+ @objc var iconColor: NSString = "#ffffff" {
140
+ didSet { loadSvgIcon() }
141
+ }
142
+
143
+ /// 图标位置
144
+ @objc var iconPosition: NSString = "leading" {
145
+ didSet { updateButtonConfiguration() }
146
+ }
147
+
148
+ /// 图标渲染尺寸(pt)
149
+ private var iconRenderingSize: CGFloat {
150
+ let sizePart = currentStyleName.components(separatedBy: ".").last ?? "Large"
151
+ switch parseButtonSize(sizePart) {
152
+ case .XLarge, .Large: return 24
153
+ case .Medium, .Small: return 20
154
+ case .XSmall, .XXSmall: return 18
155
+ default: return 24
156
+ }
157
+ }
158
+
159
+ /// 加载并应用SVG图标(使用 SDWebImageSVGCoder,避免 SVGKit 与 RN 的 cstdint/CocoaLumberjack 冲突)
160
+ private func loadSvgIcon() {
161
+ let svgString = iconSvgString as String
162
+ let colorString = iconColor as String
163
+
164
+ guard !svgString.isEmpty else {
165
+ bmButton.icon = nil
166
+ sendSizeChangeEvent()
167
+ return
168
+ }
169
+
170
+ guard let svgData = svgString.data(using: .utf8) else { return }
171
+
172
+ let size = iconRenderingSize
173
+ let options: [SDImageCoderOption: Any] = [
174
+ .decodeThumbnailPixelSize: NSValue(cgSize: CGSize(width: size, height: size)),
175
+ .decodePreserveAspectRatio: true
176
+ ]
177
+
178
+ guard let decoded = SDImageSVGCoder.shared.decodedImage(with: svgData, options: options) else {
179
+ return
180
+ }
181
+
182
+ let finalImage: UIImage?
183
+ if let color = UIColor(hexString: colorString) {
184
+ finalImage = decoded.withTintColor(color, renderingMode: .alwaysOriginal)
185
+ } else {
186
+ finalImage = decoded
187
+ }
188
+
189
+ bmButton.icon = finalImage
190
+ sendSizeChangeEvent()
191
+ }
192
+
193
+ /// 更新按钮配置(用于iconPosition变化)
194
+ private func updateButtonConfiguration() {
195
+ let styleNameStr = styleName as String
196
+ let config = parseStyleName(styleNameStr)
197
+ bmButton.configuration = config
198
+ sendSizeChangeEvent()
199
+ }
200
+
201
+ // MARK: - Style Parsing
202
+
203
+ /// 解析样式名称转换为 BMComponentButtonConfiguration
204
+ private func parseStyleName(_ styleName: String) -> BMComponentButtonConfiguration {
205
+ let components = styleName.components(separatedBy: ".")
206
+ guard components.count == 2 else {
207
+ // 默认返回 Primary.Large
208
+ return BMComponentButtonConfiguration.primary(size: .Large, iconPosition: parseIconPosition(iconPosition as String))
209
+ }
210
+
211
+ let type = parseButtonType(components[0])
212
+ let size = parseButtonSize(components[1])
213
+ // 使用iconPosition解析图标位置
214
+ let iconPos = parseIconPosition(iconPosition as String)
215
+
216
+ switch type {
217
+ case .primary:
218
+ return BMComponentButtonConfiguration.primary(size: size, iconPosition: iconPos)
219
+ case .secondary:
220
+ return BMComponentButtonConfiguration.secondary(size: size, iconPosition: iconPos)
221
+ case .green:
222
+ return BMComponentButtonConfiguration.buy(size: size, iconPosition: iconPos)
223
+ case .red:
224
+ return BMComponentButtonConfiguration.sell(size: size, iconPosition: iconPos)
225
+ case .white:
226
+ return BMComponentButtonConfiguration.white(size: size, iconPosition: iconPos)
227
+ }
228
+ }
229
+
230
+ /// 解析图标位置
231
+ private func parseIconPosition(_ positionString: String) -> BMComponentButtonIconPosition {
232
+ switch positionString.lowercased() {
233
+ case "leading": return .leading
234
+ case "trailing": return .trailing
235
+ case "none": return .none
236
+ default: return .leading
237
+ }
238
+ }
239
+
240
+ /// 解析按钮类型
241
+ private func parseButtonType(_ typeString: String) -> BMComponentButtonType {
242
+ switch typeString.lowercased() {
243
+ case "primary": return .primary
244
+ case "secondary": return .secondary
245
+ case "green": return .green
246
+ case "red": return .red
247
+ case "white": return .white
248
+ default: return .primary
249
+ }
250
+ }
251
+
252
+ /// 解析按钮尺寸
253
+ private func parseButtonSize(_ sizeString: String) -> BMComponentButtonSize {
254
+ switch sizeString.lowercased() {
255
+ case "xlarge": return .XLarge
256
+ case "large": return .Large
257
+ case "medium": return .Medium
258
+ case "small": return .Small
259
+ case "xsmall": return .XSmall
260
+ case "xxsmall": return .XXSmall
261
+ default: return .Large
262
+ }
263
+ }
264
+
265
+ // MARK: - Layout
266
+ /// 不覆写 intrinsicContentSize,使用 UIView 默认 (noIntrinsicMetric, noIntrinsicMetric)。
267
+ /// 否则 RN 的 RCTShadowView 会把 noIntrinsicMetric 转成 0,Yoga measure 会返回 width=0,导致按钮宽度为 0 或按文字宽度。
268
+ /// 不报告内在尺寸后,由父级/样式决定尺寸,Footer 下 position:absolute + left/right/top/bottom:0 即可全屏宽。
269
+
270
+ override func layoutSubviews() {
271
+ // 兜底:先修正自身 frame,再 layout 子视图,否则 bmButton 会按旧 bounds(96pt) 布局
272
+ if let sv = superview, sv.bounds.width > 0, bounds.width < sv.bounds.width - 1 {
273
+ let b = sv.bounds
274
+ if frame.size != b.size {
275
+ frame = CGRect(origin: frame.origin, size: b.size)
276
+ }
277
+ }
278
+ super.layoutSubviews()
279
+ // 用 RN/Yoga 给容器的尺寸强制设置给内层按钮,避免 Auto Layout/内在尺寸导致按钮不铺满
280
+ if bounds.width > 0 && bounds.height > 0 && bmButton.frame.size != bounds.size {
281
+ bmButton.frame = CGRect(origin: .zero, size: bounds.size)
282
+ bmButton.setNeedsLayout()
283
+ bmButton.layoutIfNeeded()
284
+ }
285
+ }
286
+ }
287
+
288
+ // MARK: - UIColor Extension for Hex String
289
+
290
+ extension UIColor {
291
+ /// 从十六进制字符串创建UIColor
292
+ /// 支持格式:#RRGGBB 或 #RRGGBBAA
293
+ convenience init?(hexString: String) {
294
+ var hexSanitized = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
295
+ hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
296
+
297
+ var rgb: UInt64 = 0
298
+ guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else {
299
+ return nil
300
+ }
301
+
302
+ let length = hexSanitized.count
303
+ let r, g, b, a: CGFloat
304
+
305
+ if length == 6 {
306
+ r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
307
+ g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
308
+ b = CGFloat(rgb & 0x0000FF) / 255.0
309
+ a = 1.0
310
+ } else if length == 8 {
311
+ r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
312
+ g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
313
+ b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
314
+ a = CGFloat(rgb & 0x000000FF) / 255.0
315
+ } else {
316
+ return nil
317
+ }
318
+
319
+ self.init(red: r, green: g, blue: b, alpha: a)
320
+ }
321
+ }
@@ -0,0 +1,21 @@
1
+ //
2
+ // TextButtonViewManager.m
3
+ // BitMartUS
4
+ //
5
+ // Created by React Native on 2026/01/24.
6
+ //
7
+
8
+ #import <React/RCTViewManager.h>
9
+
10
+ @interface RCT_EXTERN_MODULE(TextButtonViewManager, RCTViewManager)
11
+
12
+ // 导出属性
13
+ RCT_EXPORT_VIEW_PROPERTY(text, NSString)
14
+ RCT_EXPORT_VIEW_PROPERTY(styleName, NSString)
15
+ RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL)
16
+
17
+ // 导出事件 - 使用 RCTBubblingEventBlock 避免重复注册
18
+ RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
19
+ RCT_EXPORT_VIEW_PROPERTY(onSizeChange, RCTDirectEventBlock)
20
+
21
+ @end